msg_tool 0.4.0-alpha.3

A command-line tool for exporting, importing, packing, and unpacking script files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
use crate::types::*;
#[allow(unused)]
use crate::utils::num_range::*;
use clap::{ArgAction, ArgGroup, Parser, Subcommand};

#[cfg(feature = "flate2")]
fn parse_compression_level(level: &str) -> Result<u32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "none" {
        return Ok(0);
    } else if lower == "best" {
        return Ok(9);
    } else if lower == "default" {
        return Ok(6);
    } else if lower == "fast" {
        return Ok(1);
    }
    number_range(level, 0, 9)
}

#[cfg(feature = "mozjpeg")]
fn parse_jpeg_quality(quality: &str) -> Result<u8, String> {
    let lower = quality.to_ascii_lowercase();
    if lower == "best" {
        return Ok(100);
    }
    number_range(quality, 0, 100)
}

#[cfg(feature = "zstd")]
fn parse_zstd_compression_level(level: &str) -> Result<i32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "default" {
        return Ok(3);
    } else if lower == "best" {
        return Ok(22);
    }
    number_range(level, 0, 22)
}

#[cfg(feature = "webp")]
fn parse_webp_quality(quality: &str) -> Result<u8, String> {
    let lower = quality.to_ascii_lowercase();
    if lower == "best" {
        return Ok(100);
    }
    number_range(quality, 0, 100)
}

#[cfg(feature = "audio-flac")]
fn parse_flac_compression_level(level: &str) -> Result<u32, String> {
    let lower = level.to_ascii_lowercase();
    if lower == "fast" {
        return Ok(0);
    } else if lower == "best" {
        return Ok(8);
    } else if lower == "default" {
        return Ok(5);
    }
    number_range(level, 0, 8)
}

#[cfg(feature = "image-jxl")]
fn parse_jxl_distance(s: &str) -> Result<f32, String> {
    let lower = s.to_ascii_lowercase();
    if lower == "lossless" {
        return Ok(0.0);
    } else if lower == "visually-lossless" {
        return Ok(1.0);
    }
    number_range(s, 0.0, 25.0)
}

#[cfg(feature = "musica-arc")]
pub fn get_musica_game_title_value_parser() -> Vec<clap::builder::PossibleValue> {
    crate::scripts::musica::archive::paz::get_supported_games_with_title()
        .iter()
        .map(|(name, title)| {
            let mut pv = clap::builder::PossibleValue::new(*name);
            if let Some(t) = title {
                pv = pv.help(t);
                let mut alias_count = 0usize;
                for i in t.split("|") {
                    pv = pv.alias(i.trim());
                    alias_count += 1;
                }
                // alias for full title
                if alias_count > 1 {
                    pv = pv.alias(t);
                }
            }
            pv
        })
        .collect()
}

#[cfg(feature = "kirikiri-arc")]
pub fn get_xp3_game_title_value_parser() -> Vec<clap::builder::PossibleValue> {
    crate::scripts::kirikiri::archive::xp3::get_supported_games_with_title()
        .iter()
        .map(|(name, title)| {
            let mut pv = clap::builder::PossibleValue::new(*name);
            if let Some(t) = title {
                pv = pv.help(t);
                let mut alias_count = 0usize;
                for i in t.split("|") {
                    pv = pv.alias(i.trim());
                    alias_count += 1;
                }
                // alias for full title
                if alias_count > 1 {
                    pv = pv.alias(t);
                }
            }
            pv
        })
        .collect()
}

/// Tools for export and import scripts
#[derive(Parser, Debug, Clone)]
#[clap(
    group = ArgGroup::new("encodingg").multiple(false),
    group = ArgGroup::new("output_encodingg").multiple(false),
    group = ArgGroup::new("archive_encodingg").multiple(false),
    group = ArgGroup::new("artemis_indentg").multiple(false),
    group = ArgGroup::new("ex_hibit_rld_xor_keyg").multiple(false),
    group = ArgGroup::new("ex_hibit_rld_def_xor_keyg").multiple(false),
    group = ArgGroup::new("webp_qualityg").multiple(false),
    group = ArgGroup::new("cat_system_int_encrypt_passwordg").multiple(false),
    group = ArgGroup::new("kirikiri_chat_jsong").multiple(false),
    group = ArgGroup::new("xp3-compression").multiple(false),
)]
#[command(
    version,
    about,
    long_about = "Tools for export and import scripts\nhttps://github.com/lifegpc/msg-tool"
)]
pub struct Arg {
    #[arg(short = 't', long, value_enum, global = true)]
    /// Script type
    pub script_type: Option<ScriptType>,
    #[arg(short = 'T', long, value_enum, global = true)]
    /// Output script type
    pub output_type: Option<OutputScriptType>,
    #[arg(short = 'n', long, global = true)]
    /// Disable extra extension when locating/export output script
    pub output_no_extra_ext: bool,
    #[cfg(feature = "image")]
    #[arg(short = 'i', long, value_enum, global = true)]
    /// Output image type
    pub image_type: Option<ImageOutputType>,
    #[arg(short = 'e', long, value_enum, global = true, group = "encodingg")]
    /// Script encoding
    pub encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(short = 'c', long, value_enum, global = true, group = "encodingg")]
    /// Script code page
    pub code_page: Option<u32>,
    #[arg(
        short = 'E',
        long,
        value_enum,
        global = true,
        group = "output_encodingg"
    )]
    /// Output text encoding
    pub output_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        short = 'C',
        long,
        value_enum,
        global = true,
        group = "output_encodingg"
    )]
    /// Output code page
    pub output_code_page: Option<u32>,
    #[arg(
        short = 'a',
        long,
        value_enum,
        global = true,
        group = "archive_encodingg"
    )]
    /// Archive filename encoding
    pub archive_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        short = 'A',
        long,
        value_enum,
        global = true,
        group = "archive_encodingg"
    )]
    /// Archive code page
    pub archive_code_page: Option<u32>,
    #[cfg(feature = "circus")]
    #[arg(long, value_enum, global = true)]
    /// Circus Game
    pub circus_mes_type: Option<CircusMesType>,
    #[arg(short, long, action = ArgAction::SetTrue, global = true)]
    /// Search for script files in the directory recursively
    pub recursive: bool,
    #[arg(global = true, action = ArgAction::SetTrue, short, long)]
    /// Print backtrace on error
    pub backtrace: bool,
    #[cfg(feature = "escude-arc")]
    #[arg(long, action = ArgAction::SetTrue, global = true)]
    /// Whether to use fake compression for Escude archive
    pub escude_fake_compress: bool,
    #[cfg(feature = "escude")]
    #[arg(long, global = true)]
    /// The path to the Escude enum script file (enum_scr.bin)
    pub escude_enum_scr: Option<String>,
    #[cfg(feature = "escude")]
    #[arg(long, global = true)]
    /// Escude game title
    pub escude_op: Option<crate::scripts::escude::script::EscudeOp>,
    #[cfg(feature = "bgi")]
    #[arg(long, action = ArgAction::SetTrue, global = true)]
    /// Duplicate same strings when importing into BGI scripts.
    /// Enable this will cause BGI scripts to become very large.
    pub bgi_import_duplicate: bool,
    #[cfg(feature = "bgi")]
    #[arg(long, action = ArgAction::SetTrue, global = true, visible_alias = "bgi-no-append")]
    /// Disable appending new strings to the end of BGI scripts.
    /// Disable may cause BGI scripts broken.
    pub bgi_disable_append: bool,
    #[cfg(all(feature = "bgi-arc", feature = "bgi-img"))]
    #[arg(long, global = true)]
    /// Detect all files in BGI archive as SysGrp Images. By default, only files which name is `sysgrp.arc` will enabled this.
    pub bgi_is_sysgrp_arc: Option<bool>,
    #[cfg(feature = "bgi-img")]
    #[arg(long, global = true)]
    /// Whether to create scrambled SysGrp images. When in import mode, the default value depends on the original image.
    /// When in creation mode, it is not enabled by default.
    pub bgi_img_scramble: Option<bool>,
    #[cfg(feature = "bgi-img")]
    #[arg(long, global = true, default_value_t = crate::types::get_default_threads())]
    /// Workers count for decode BGI compressed images v2 in parallel. Default is half of CPU cores.
    /// Set this to 1 to disable parallel decoding. 0 means same as 1.
    pub bgi_img_workers: usize,
    #[cfg(feature = "cat-system-arc")]
    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
    /// CatSystem2 engine int archive password
    pub cat_system_int_encrypt_password: Option<String>,
    #[cfg(feature = "cat-system-arc")]
    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
    /// The path to the CatSystem2 engine executable file. Used to get the int archive password.
    pub cat_system_int_exe: Option<String>,
    #[cfg(feature = "cat-system-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Draw CatSystem2 image on canvas (if canvas width and height are specified in file)
    pub cat_system_image_canvas: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Kirikiri language index in script. If not specified, the first language will be used.
    pub kirikiri_language_index: Option<usize>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Export chat message to extra json file. (for Kirikiri SCN script.)
    /// For example, CIRCUS's comu message. Yuzusoft's phone chat message.
    pub kirikiri_export_chat: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',')]
    /// Kirikiri chat message key. For example, CIRCUS's key is "comumode". Yuzusoft's key is "phonechat".
    /// If not specified, "comumode" will be used. Multiple keys can be specified separated by comma.
    pub kirikiri_chat_key: Option<Vec<String>>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
    /// Kirikiri chat message translation file. (Map<String, String>, key is original text, value is translated text.)
    pub kirikiri_chat_json: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
    /// Kirikiri chat message translation directory. All json files in this directory will be merged. (Only m3t files are supported.)
    pub kirikiri_chat_dir: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',')]
    /// Kirikiri language list. First language code is code for language index 1.
    pub kirikiri_languages: Option<Vec<String>>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-title")]
    /// Whether to handle title in Kirikiri SCN script.
    pub kirikiri_title: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-chat-no-multilang")]
    /// Disable multi-language support for Kirikiri chat messages. (for Kirikiri SCN script.)
    pub kirikiri_chat_no_multilang: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-no-empty-lines", visible_alias = "kirikiri-no-empty-lines")]
    /// Remove empty lines in Kirikiri KS script.
    pub kirikiri_remove_empty_lines: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "nm,set_title,speaker,Talk,talk,cn,name,名前"
    )]
    /// Kirikiri name commands, used to extract names from ks script.
    pub kirikiri_name_commands: Vec<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "sel01,sel02,sel03,sel04,AddSelect,ruby,exlink,e_xlink"
    )]
    /// Kirikiri message commands, used to extract more message from ks script.
    pub kirikiri_message_commands: Vec<String>,
    #[cfg(feature = "image")]
    #[arg(short = 'f', long, global = true)]
    /// Output multiple image as `<basename>_<name>.<ext>` instead of `<basename>/<name>.<ext>`
    pub image_output_flat: bool,
    #[cfg(feature = "bgi-arc")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to compress files in BGI archive when packing BGI archive.
    pub bgi_compress_file: bool,
    #[cfg(feature = "bgi-arc")]
    #[arg(long, global = true, default_value_t = 9, value_parser = crate::scripts::bgi::archive::dsc::parse_compress_level)]
    /// Compress level for BGI Dsc file. 0 means store, 10 mean best compression.
    /// 10 will use zopfli like compression method, this may cost a lot of time.
    pub bgi_compress_level: u8,
    #[cfg(feature = "bgi-arc")]
    #[arg(long, global = true, default_value_t = num_cpus::get())]
    /// Workers count for compress files in BGI archive when creating in parallel. Default is CPU cores count.
    pub bgi_arc_workers: usize,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true)]
    /// Whether to overlay PIMG images. (By default, true if all layers are not group layers.)
    pub emote_pimg_overlay: Option<bool>,
    #[cfg(feature = "artemis-arc")]
    #[arg(long, global = true)]
    /// Disable Artemis archive (.pfs) XOR encryption when packing.
    pub artemis_arc_disable_xor: bool,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, group = "artemis_indentg")]
    /// Artemis script indent size, used to format Artemis script.
    /// Default is 4 spaces.
    pub artemis_indent: Option<usize>,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, action = ArgAction::SetTrue, group = "artemis_indentg")]
    /// Disable Artemis script indent, used to format Artemis script.
    pub artemis_no_indent: bool,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, default_value_t = 100)]
    /// Max line width in Artemis script, used to format Artemis script.
    pub artemis_max_line_width: usize,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true)]
    /// Specify the language of Artemis AST script.
    /// If not specified, the first language will be used.
    pub artemis_ast_lang: Option<String>,
    #[cfg(feature = "artemis")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Do not format lua code in Artemis ASB script(.asb/.iet) when exporting.
    pub artemis_asb_no_format_lua: bool,
    // Default value is from tagFilters in macro.iet
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "背景,イベントCG,遅延背景,遅延背景予約,背景予約,遅延イベントCG,遅延イベントCG予約,イベントCG予約,遅延ポップアップ,遅延bgm_in,遅延bgm_out,遅延se_in,遅延se_out,遅延bgs_in,遅延bgs_out,立ち絵face非連動,セーブサムネイル置換終了,シネスコ,ポップアップ"
    )]
    /// Artemis Engine blacklist tag names for TXT script.
    /// This is used to ignore these tags when finding names in Artemis TXT script (ぱんみみそふと).
    pub artemis_panmimisoft_txt_blacklist_names: Vec<String>,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true)]
    /// Specify the language of Artemis TXT (ぱんみみそふと) script.
    /// If not specified, the first language will be used.
    pub artemis_panmimisoft_txt_lang: Option<String>,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true, action = ArgAction::SetTrue, requires = "artemis_panmimisoft_txt_lang")]
    /// Enable multiple language support for single language Artemis TXT (ぱんみみそふと) script.
    /// --artemis-panmimisoft-txt-lang must be set when enabling this.
    pub artemis_panmimisoft_txt_multi_lang: bool,
    #[cfg(feature = "artemis-panmimisoft")]
    #[arg(long, global = true)]
    /// The path to the tag.ini file, which contains the tags to be ignored when finding names in Artemis TXT script (ぱんみみそふと).
    pub artemis_panmimisoft_txt_tag_ini: Option<String>,
    #[cfg(feature = "cat-system")]
    #[arg(long, global = true)]
    /// CatSystem2 CSTL script language, used to extract messages from CSTL script.
    /// If not specified, the first language will be used.
    pub cat_system_cstl_lang: Option<String>,
    #[cfg(feature = "flate2")]
    #[arg(short = 'z', long, global = true, value_name = "LEVEL", value_parser = parse_compression_level, default_value_t = 6)]
    /// Zlib compression level. 0 means no compression, 9 means best compression.
    pub zlib_compression_level: u32,
    #[cfg(feature = "image")]
    #[arg(short = 'g', long, global = true, value_enum, default_value_t = PngCompressionLevel::Fast)]
    /// PNG compression level.
    pub png_compression_level: PngCompressionLevel,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Keep original BPP when importing Circus CRX images.
    pub circus_crx_keep_original_bpp: bool,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Use zstd compression for Circus CRX images. (CIRCUS Engine don't support this. Hook is required.)
    pub circus_crx_zstd: bool,
    #[cfg(feature = "zstd")]
    #[arg(short = 'Z', long, global = true, value_name = "LEVEL", value_parser = parse_zstd_compression_level, default_value_t = 3)]
    /// Zstd compression level. 0 means default compression level (3), 22 means best compression.
    pub zstd_compression_level: i32,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::circus::image::crx::CircusCrxMode::Auto)]
    /// Circus CRX image row type mode
    pub circus_crx_mode: crate::scripts::circus::image::crx::CircusCrxMode,
    #[cfg(feature = "circus-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Draw Circus CRX images on canvas (if canvas width and height are specified in file)
    pub circus_crx_canvas: bool,
    #[arg(short = 'F', long, global = true, action = ArgAction::SetTrue)]
    /// Force all files in archive to be treated as script files.
    pub force_script: bool,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "HEX",
        group = "ex_hibit_rld_xor_keyg"
    )]
    /// ExHibit xor key for rld script, in hexadecimal format. (e.g. `12345678`)
    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to find the key.
    pub ex_hibit_rld_xor_key: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        group = "ex_hibit_rld_xor_keyg"
    )]
    /// ExHibit rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
    pub ex_hibit_rld_xor_key_file: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "HEX",
        group = "ex_hibit_rld_def_xor_keyg"
    )]
    /// ExHibit rld def.rld xor key, in hexadecimal format. (e.g. `12345678`)
    pub ex_hibit_rld_def_xor_key: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        group = "ex_hibit_rld_def_xor_keyg"
    )]
    /// ExHibit rld def.rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
    pub ex_hibit_rld_def_xor_key_file: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(long, global = true, value_name = "PATH")]
    /// Path to the ExHibit rld keys file, which contains the keys in BINARY format.
    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to get this file.
    pub ex_hibit_rld_keys: Option<String>,
    #[cfg(feature = "ex-hibit")]
    #[arg(long, global = true, value_name = "PATH")]
    /// Path to the ExHibit rld def keys file, which contains the keys in BINARY format.
    pub ex_hibit_rld_def_keys: Option<String>,
    #[cfg(feature = "mozjpeg")]
    #[arg(long, global = true, default_value_t = 80, value_parser = parse_jpeg_quality)]
    /// JPEG quality for output images, 0-100. 100 means best quality.
    pub jpeg_quality: u8,
    #[cfg(feature = "webp")]
    #[arg(short = 'w', long, global = true, group = "webp_qualityg")]
    /// Use WebP lossless compression for output images.
    pub webp_lossless: bool,
    #[cfg(feature = "webp")]
    #[arg(short = 'W', long, global = true, value_name = "QUALITY", group = "webp_qualityg", value_parser = parse_webp_quality, default_value_t = 80)]
    /// WebP quality for output images, 0-100. 100 means best quality.
    pub webp_quality: u8,
    #[arg(long, global = true)]
    /// Try use YAML format instead of JSON when custom exporting.
    /// By default, this is based on output type. But can be overridden by this option.
    pub custom_yaml: Option<bool>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS srcxml script language, used to extract messages from srcxml script.
    /// If not specified, the first language will be used.
    pub entis_gls_srcxml_lang: Option<String>,
    #[cfg(feature = "will-plus")]
    #[arg(long, global = true)]
    /// Disable disassembly for WillPlus ws2 script.
    /// Use another parser to parse the script.
    /// Should only be used when the default parser not works well.
    pub will_plus_ws2_no_disasm: bool,
    #[cfg(feature = "lossless-audio")]
    #[arg(short = 'l', long, global = true, value_enum, default_value_t = LosslessAudioFormat::Wav)]
    /// Audio format for output lossless audio files.
    pub lossless_audio_fmt: LosslessAudioFormat,
    #[cfg(feature = "audio-flac")]
    #[arg(short = 'L', long, global = true, default_value_t = 5, value_parser = parse_flac_compression_level)]
    /// FLAC compression level for output FLAC audio files. 0 means fastest compression, 8 means best compression.
    pub flac_compression_level: u32,
    #[arg(long, global = true)]
    /// Add a mark to the end of each message for LLM translation.
    /// Only works on m3t format.
    pub llm_trans_mark: Option<String>,
    #[cfg(feature = "favorite")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Do not filter ascii strings in Favorite HCB script.
    pub favorite_hcb_no_filter_ascii: bool,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "jxl-no-lossless")]
    /// Disable JXL lossless compression for output images
    pub jxl_lossy: bool,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, default_value_t = 1.0, value_parser = parse_jxl_distance)]
    /// JXL distance for output images. 0 means mathematically lossless compression. 1.0 means visually lossless compression.
    /// Allowed range is 0.0-25.0. Recommended range is 0.5-3.0. Default value is 1
    pub jxl_distance: f32,
    #[cfg(feature = "image-jxl")]
    #[arg(long, global = true, default_value_t = 1, visible_alias = "jxl-jobs")]
    /// Workers count for encode JXL images in parallel. Default is 1.
    /// Set this to 1 to disable parallel encoding. 0 means same as 1
    pub jxl_workers: usize,
    #[cfg(feature = "image")]
    #[arg(short = 'J', long, global = true, default_value_t = crate::types::get_default_threads(), visible_alias = "img-jobs", visible_alias = "img-workers", visible_alias = "image-jobs")]
    /// Workers count for encode images in parallel. Default is half of CPU cores.
    /// Set this to 1 to disable parallel encoding. 0 means same as 1.
    pub image_workers: usize,
    #[cfg(feature = "jieba")]
    #[arg(long, global = true)]
    /// Path to custom jieba dictionary
    pub jieba_dict: Option<String>,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "psb-no-tlg")]
    /// Do not process TLG images in PSB files.
    pub psb_no_process_tlg: bool,
    #[cfg(feature = "softpal-img")]
    #[arg(long, global = true, visible_alias = "pgd-co")]
    /// Whether to use compression for Softpal Pgd images.
    /// WARN: Compress may cause image broken.
    pub pgd_compress: bool,
    #[arg(long, global = true)]
    /// Disable multiple messages section support.
    pub no_multi_message: bool,
    #[cfg(feature = "softpal")]
    #[arg(long, global = true, visible_alias = "softpal-idx")]
    /// Whether to add message index to Softpal src script when exporting.
    pub softpal_add_message_index: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable decrypt SimpleCrypt files in Kirikiri XP3 archive when extracting.
    pub xp3_no_simple_crypt: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable decompressing mdf files in Kirikiri XP3 archive when extracting.
    pub xp3_no_mdf_decompress: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, default_value = "cdc:32KiB:256KiB:8MiB", value_parser = crate::scripts::kirikiri::archive::xp3::parse_segmenter_config)]
    /// Configuration for Kirikiri XP3 segmenter when creating XP3 archive.
    /// none segmenter - none
    /// fastcdc segmenter - cdc:<min>:<avg>:<max>
    /// fixed segmenter - fixed:<size>
    /// custom segmenter - custom:<json_path>
    pub xp3_segmenter: crate::scripts::kirikiri::archive::xp3::SegmenterConfig,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable compressing files in Kirikiri XP3 archive when creating XP3 archive.
    pub xp3_no_compress_files: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable compressing index in Kirikiri XP3 archive when creating XP3 archive.
    pub xp3_no_compress_index: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, default_value_t = num_cpus::get(), visible_alias = "xp3-compress-jobs")]
    /// Workers count for compress files in Kirikiri XP3 archive when creating in parallel.
    pub xp3_compress_workers: usize,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, group = "xp3-compression")]
    /// Use zstd compression for files in Kirikiri XP3 archive when creating. (Warning: Kirikiri engine don't support this. Hook is required.)
    pub xp3_zstd: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, group = "xp3-compression")]
    /// Use zopfli compression for files in Kirikiri XP3 archive when creating. This is very slow.
    pub xp3_zopfli: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(
        long,
        global = true,
        default_value_t = 1,
        visible_alias = "xp3-pack-jobs"
    )]
    /// Workers count for packing file in Kirikiri XP3 archive in parallel.
    /// This not works when segment is disabled.
    pub xp3_pack_workers: usize,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored.
    pub kirikiri_language_insert: bool,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true, value_parser = get_musica_game_title_value_parser())]
    /// Musica game title for paz archive.
    pub musica_game_title: Option<String>,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true)]
    /// Musica xor key for paz archive.
    pub musica_xor_key: Option<u8>,
    #[cfg(feature = "musica-arc")]
    #[arg(long, global = true)]
    /// Compress files in Musica paz archive when packing paz archive.
    pub musica_compress: bool,
    #[arg(short = 'x', long, default_value_t = 0, global = true)]
    /// Exit code when some jobs failed
    pub exit_code: i32,
    #[arg(short = 'X', long, global = true)]
    /// Exit code when all jobs failed. By default, this is same as exit_code. This can override exit_code when all jobs failed.
    pub exit_code_all_failed: Option<i32>,
    #[arg(long, global = true)]
    /// Do not add quote to translated text when exporting to m3t files.
    pub m3t_no_quote: bool,
    #[arg(long, global = true)]
    /// Use original text as translated text if translated text and llm text are all empty.
    pub m3t_use_original_text: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Disable adler32 checksum for Kirikiri XP3 archive when creating.
    /// This will keep compatibility with https://github.com/arcusmaximus/KirikiriTools tool.
    pub xp3_no_adler: bool,
    #[cfg(feature = "bgi")]
    #[arg(long, global = true)]
    /// Add an additional space at the end of message in BGI scripts when importing.
    /// This may help BGI engine to display the message correctly in save/load screen for some games.
    pub bgi_add_space: bool,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(15).unwrap(), visible_alias = "zp-ic")]
    /// Maximum amount of times to rerun forward and backward pass to optimize LZ77 compression cost.
    /// Good values: 10, 15 for small files, 5 for files over several MB in size or it will be too slow.
    /// Default is 15.
    pub zopfli_iteration_count: std::num::NonZeroU64,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = std::num::NonZeroU64::new(u64::MAX).unwrap(), visible_alias = "zp-iwi")]
    /// Stop after rerunning forward and backward pass this many times without finding a smaller representation of the block.
    /// Default value: practically infinite (maximum u64 value)
    pub zopfli_iterations_without_improvement: std::num::NonZeroU64,
    #[cfg(feature = "zopfli")]
    #[arg(long, global = true, default_value_t = 15, visible_alias = "zp-mbs")]
    /// Maximum amount of blocks to split into (0 for unlimited, but this can give extreme results that hurt compression on some files).
    /// Default value: 15.
    pub zopfli_maximum_block_splits: u16,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, action = ArgAction::SetTrue, alias = "entis-gls-csx-diasm")]
    /// Disassemble Entis GLS csx script when exporting in custom mode.
    pub entis_gls_csx_disasm: bool,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, default_value = "/")]
    /// The line feed character used in Entis GLS csx script.
    pub entis_gls_csx_lf: String,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS csx script version.
    /// If not specified. Will try use lower version first.
    pub entis_gls_csx_ver: Option<crate::scripts::entis_gls::csx::CSXScriptVersion>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true)]
    /// Entis GLS csx script version2 full version.
    /// If not specified. Will try use higher version first.
    pub entis_gls_csx_v2_ver: Option<crate::scripts::entis_gls::csx::CSXScriptV2FullVer>,
    #[cfg(feature = "entis-gls")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Disable part labels in Entis GLS csx script when exporting.
    pub entis_gls_csx_no_part_label: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Disable process ABMP10 images in ABMP10 images.
    pub qlie_abmp10_no_process_abmp10: bool,
    #[cfg(feature = "qlie-arc")]
    #[arg(long, global = true)]
    /// Path to qlie pack archive key file (pack_keyfile_kfueheish15538fa9or.key)
    pub qlie_pack_keyfile: Option<String>,
    #[cfg(feature = "qlie-arc")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to compress files in Qlie pack archive.
    pub qlie_pack_compress_files: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to use PNG file directly for Qlie DPNG images when importing.
    /// Enable this will disable reencoding PNG files. Useful when the PNG files are already optimized by other tools.
    pub qlie_dpng_use_raw_png: bool,
    #[cfg(feature = "qlie-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Export Qlie DPNG images as PSD files.
    pub qlie_dpng_psd: bool,
    #[cfg(feature = "utils-psd")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Whether to disable compression for image data in PSD files.
    pub psd_no_compress: bool,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, action = ArgAction::SetTrue)]
    /// Export Emote PIMG images as PSD files.
    pub emote_pimg_psd: bool,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Whether to only extract message between Talk and Hitret command in Kirikiri KS script. Auto detect if not specified.
    pub kirikiri_ks_hitret: Option<bool>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// The line feed character used in Kirikiri KS script.
    pub kirikiri_ks_lf: Option<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true, value_delimiter = ',', default_value = "macCmd")]
    /// Kirikiri message tags, used to extract more message from ks script.
    pub kirikiri_message_tags: Vec<String>,
    #[cfg(feature = "kirikiri")]
    #[arg(long, global = true)]
    /// Specifiy BOM type when creating new Kirikiri ks script. If not specified, detect from original script.
    pub kirikiri_ks_bom: Option<BomType>,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::emote::psb::BC7Config::default())]
    /// BC7 compress configuration
    pub bc7: crate::scripts::emote::psb::BC7Config,
    #[cfg(feature = "artemis")]
    #[arg(
        long,
        global = true,
        value_delimiter = ',',
        default_value = "click,hcls,rpx"
    )]
    /// A list of Artemis ASB script end tags, used to determine a dialogue block in script.
    pub artemis_asb_end_tags: Vec<String>,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, value_parser = get_xp3_game_title_value_parser())]
    /// Game title for Kirikiri XP3 archive. This is used to decrypt file in archives.
    pub xp3_game_title: Option<String>,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Print/write debug information for Kirikiri XP3 archive when extracting archive to specifiy location.
    /// This is used to find correct configuration for unknown XP3 archives.
    pub xp3_debug_archive: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Force extract encrypted files in Kirikiri XP3 archive without decryption.
    pub xp3_force_extract: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, visible_alias = "xp3-fd")]
    /// Force decrypt files in Kirikiri xp3 archive even when flags are not set.
    /// Some encrypted files in Kirikiri XP3 archive may not set encryption flag, but still encrypted. Enable this to force decrypt these files.
    pub xp3_force_decrypt: bool,
    #[cfg(feature = "emote-img")]
    #[arg(long, global = true)]
    /// Disable diff handle when exporting Emote PIMG images to PSD files.
    /// If enabled, no group layers will be crated if both layer don't have diff_id and group_layer_id attribute.
    pub emote_pimg_psd_no_diff: bool,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// The path to the file list for Kirikiri XP3 archive. This is used to recover file names from hashed values.
    /// Only works with some encyrption methods.
    pub xp3_file_list_path: Option<String>,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::kirikiri::archive::xp3::FileHashOption::Both)]
    /// Control the behavior to how to extract files from Cxdec3/4(Hxv4) protected archives.
    pub xp3_cxdec_file_hash: crate::scripts::kirikiri::archive::xp3::FileHashOption,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true, value_enum,  default_value_t = crate::scripts::kirikiri::archive::xp3::PathHashOption::Both)]
    /// Control the behavior to how to append path name to files from Cxdec3/4(Hxv4) protected archives.
    pub xp3_cxdec_path_hash: crate::scripts::kirikiri::archive::xp3::PathHashOption,
    #[cfg(feature = "kirikiri-arc")]
    #[arg(long, global = true)]
    /// Dump xp3 file name hash into a json file. Only some protected archive may supported.
    pub xp3_dump_file_hash_list: Option<String>,
    #[cfg(feature = "yuris")]
    #[arg(long, global = true)]
    /// Path to the ysc.ybn file
    pub yuris_ysc_path: Option<String>,
    #[cfg(feature = "yuris")]
    #[arg(long, global = true)]
    /// Path to the ysl.ybn file
    pub yuris_ysl_path: Option<String>,
    #[cfg(feature = "yuris")]
    #[arg(long, global = true)]
    /// Disasm Yu-RIS YSTB (.ybn) file
    pub yuris_ystb_disasm: bool,
    #[cfg(feature = "yuris")]
    #[arg(long, global = true)]
    /// Path to YuRis Tips json map. Used to replace tip name in YuRis scenario file
    pub yuris_tips_map: Option<String>,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true, value_enum, default_value_t = Default::default())]
    /// Name hash type in the Yu-RIS archive (.ypf)
    pub yuris_name_hash_type: crate::scripts::yuris::arc::ypf::NameHashType,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true, value_enum, default_value_t = Default::default())]
    /// Data hash type in the Yu-RIS archive (.ypf)
    pub yuris_data_hash_type: crate::scripts::yuris::arc::ypf::DataHashType,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Check hash when unpack Yu-RIS archive (.ypf)
    pub yuris_check_hash: bool,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Print debug information for Yu-RIS archive (.ypf) when extracting archive to stdout.
    /// This is used to find correct configuration for Yu-RIS archives.
    pub yuris_debug_archive: bool,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Yu-RIS YPF engine version. Used when pack files into Yu-RIS archive.
    pub yuris_ypf_version: Option<u32>,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Do not compress file when packing files into Yu-RIS archive.
    pub yuris_ypf_no_compress_file: bool,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Use zopfli to compress files in Yu-RIS archive.
    pub yuris_ypf_zopfli: bool,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true, default_value_t = num_cpus::get())]
    /// Workers count for compress files in Yu-RIS archive when creating in parallel. Default is CPU cores count.
    pub yuris_ypf_workers: usize,
    #[cfg(feature = "yuris-arc")]
    #[arg(long, global = true)]
    /// Use new file type mapping for Yu-RIS archive (.ypf).
    /// When enabled, ogg and wav are mapped to larger type values (OGG_ and WAV_).
    pub yuris_use_new_file_type: bool,
    #[command(subcommand)]
    /// Command
    pub command: Command,
}

#[derive(Parser, Debug, Clone)]
#[clap(group = ArgGroup::new("patched_encodingg").multiple(false), group = ArgGroup::new("patched_archive_encodingg").multiple(false))]
pub struct ImportArgs {
    /// Input script file or directory
    pub input: String,
    /// Text file or directory
    pub output: String,
    /// Patched script file or directory
    pub patched: String,
    #[arg(short = 'p', long, group = "patched_encodingg")]
    /// Patched script encoding
    pub patched_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(short = 'P', long, group = "patched_encodingg")]
    /// Patched script code page
    pub patched_code_page: Option<u32>,
    #[arg(
        long,
        value_enum,
        group = "patched_archive_encodingg",
        visible_alias = "pa"
    )]
    /// Patched archive filename encoding
    pub patched_archive_encoding: Option<TextEncoding>,
    #[cfg(windows)]
    #[arg(
        long,
        value_enum,
        group = "patched_archive_encodingg",
        visible_alias = "PA"
    )]
    /// Patched archive code page
    pub patched_archive_code_page: Option<u32>,
    #[arg(long)]
    /// Patched script format type
    pub patched_format: Option<FormatType>,
    #[arg(long)]
    /// Fixed length of one line in patched script (for fixed format)
    pub patched_fixed_length: Option<usize>,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Keep original line breaks in patched script (for fixed format)
    pub patched_keep_original: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Break words in patched script (for fixed format)
    pub patched_break_words: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Insert fullwidth space at the start of line in patched script (for fixed format)
    pub patched_insert_fullwidth_space_at_line_start: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// If a line break occurs in the middle of some symbols, bring the sentence to next line (for fixed format)
    pub patched_break_with_sentence: bool,
    #[cfg(feature = "jieba")]
    #[arg(long, action = ArgAction::SetTrue)]
    /// Whether to disable break Chinese words at the end of the line.
    pub patched_no_break_chinese_words: bool,
    #[arg(long, action = ArgAction::SetTrue)]
    /// Do not remove space at the start of the line
    pub patched_no_remove_space_at_line_start: bool,
    #[arg(long)]
    /// Name table file
    pub name_csv: Option<String>,
    #[arg(long)]
    /// Replacement table file
    pub replacement_json: Option<String>,
    #[arg(long, action = ArgAction::SetTrue)]
    pub warn_when_output_file_not_found: bool,
    #[arg(long)]
    /// Output dependency file path. This file will contain a list of all files used during import.
    pub dep_file: Option<String>,
    #[arg(short = 'j', long, default_value_t = 1)]
    /// Workers count for import scripts in parallel.
    pub jobs: usize,
}

#[derive(Subcommand, Debug, Clone)]
/// Commands
pub enum Command {
    /// Extract from script
    Export {
        /// Input script file or directory
        input: String,
        /// Output file or directory
        output: Option<String>,
    },
    /// Import to script
    Import(ImportArgs),
    /// Pack files to archive
    Pack {
        /// Input directory
        input: String,
        /// Output archive file
        output: Option<String>,
        #[arg(long)]
        /// Use \ as path separator instead of / in archive
        backslash: bool,
    },
    /// Unpack archive to directory
    Unpack {
        /// Input archive file
        input: String,
        /// Output directory
        output: Option<String>,
        #[arg(short = 's', long)]
        /// Skip unpack if file already exists
        skip_existed: bool,
    },
    /// Create a new script file
    Create {
        /// Input script
        input: String,
        /// Output script file
        output: Option<String>,
    },
    /// Pack files to archive (version 2)
    PackV2 {
        #[arg(short = 'o', long)]
        /// Output archive file
        output: Option<String>,
        /// Path to input files/directories
        input: Vec<String>,
        #[arg(long)]
        /// Use \ as path separator instead of / in archive
        backslash: bool,
        #[arg(long)]
        /// Do not create directory entries in archive. This means all files are stored in a flat structure.
        no_dir: bool,
        #[arg(long)]
        /// Output dependency file path. This file will contain a list of all files packed in the archive.
        dep_file: Option<String>,
    },
    /// Convert output script to another format
    Convert {
        /// Input script format type
        input_type: OutputScriptType,
        /// Output script format type
        output_type: OutputScriptType,
        /// Input script file
        input: String,
        /// Output script file
        output: Option<String>,
    },
}

pub fn parse_args() -> Arg {
    Arg::parse()
}

#[cfg(feature = "ex-hibit")]
pub fn load_ex_hibit_rld_xor_key(arg: &Arg) -> anyhow::Result<Option<u32>> {
    if let Some(key) = &arg.ex_hibit_rld_xor_key {
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(key, 16)?));
        }
    }
    if let Some(file) = &arg.ex_hibit_rld_xor_key_file {
        let key = std::fs::read_to_string(file)?.trim().to_string();
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(&key, 16)?));
        }
    }
    Ok(None)
}

#[cfg(feature = "ex-hibit")]
pub fn load_ex_hibit_rld_def_xor_key(arg: &crate::args::Arg) -> anyhow::Result<Option<u32>> {
    if let Some(key) = &arg.ex_hibit_rld_def_xor_key {
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(key, 16)?));
        }
    }
    if let Some(file) = &arg.ex_hibit_rld_def_xor_key_file {
        let key = std::fs::read_to_string(file)?.trim().to_string();
        if key.starts_with("0x") {
            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
        } else {
            return Ok(Some(u32::from_str_radix(&key, 16)?));
        }
    }
    Ok(None)
}

#[cfg(feature = "cat-system-arc")]
pub fn get_cat_system_int_encrypt_password(arg: &Arg) -> anyhow::Result<Option<String>> {
    if let Some(exe) = &arg.cat_system_int_exe {
        return Ok(Some(
            crate::scripts::cat_system::archive::int::get_password_from_exe(exe)?,
        ));
    }
    if let Some(password) = &arg.cat_system_int_encrypt_password {
        return Ok(Some(password.clone()));
    }
    Ok(None)
}

#[cfg(feature = "artemis-panmimisoft")]
pub fn get_artemis_panmimisoft_txt_blacklist_names(
    arg: &Arg,
) -> anyhow::Result<std::collections::HashSet<String>> {
    match &arg.artemis_panmimisoft_txt_tag_ini {
        Some(path) => {
            let mut set = crate::scripts::artemis::panmimisoft::txt::read_tags_from_ini(path)?;
            for name in &arg.artemis_panmimisoft_txt_blacklist_names {
                set.insert(name.clone());
            }
            Ok(set)
        }
        None => Ok(arg
            .artemis_panmimisoft_txt_blacklist_names
            .iter()
            .cloned()
            .collect()),
    }
}

#[cfg(feature = "kirikiri")]
pub fn load_kirikiri_chat_json(
    arg: &Arg,
) -> anyhow::Result<
    Option<
        std::sync::Arc<
            std::collections::HashMap<String, std::collections::HashMap<String, (String, usize)>>,
        >,
    >,
> {
    if let Some(path) = &arg.kirikiri_chat_json {
        return Ok(Some(std::sync::Arc::new(
            crate::scripts::kirikiri::read_kirikiri_comu_json(path)?
                .into_iter()
                .map(|(k, v)| {
                    let v: std::collections::HashMap<_, _> =
                        v.into_iter().map(|(k, v)| (k, (v, 1))).collect();
                    (k, v)
                })
                .collect(),
        )));
    }
    if let Some(dir) = &arg.kirikiri_chat_dir {
        let mut outt = arg.output_type.unwrap_or(OutputScriptType::M3t);
        if !matches!(
            outt,
            OutputScriptType::M3t
                | OutputScriptType::M3ta
                | OutputScriptType::M3tTxt
                | OutputScriptType::Po
                | OutputScriptType::Pot
        ) {
            outt = OutputScriptType::M3t;
        }
        let files = crate::utils::files::find_ext_files(dir, arg.recursive, &[outt.as_ref()])?;
        if !files.is_empty() {
            let mut map = std::collections::HashMap::new();
            let mut global: std::collections::HashMap<
                String,
                (String, std::collections::HashSet<String>),
            > = std::collections::HashMap::new();
            for file in files {
                let f = crate::utils::files::read_file(&file)?;
                let data = crate::utils::encoding::decode_to_string(
                    crate::get_output_encoding(arg),
                    &f,
                    true,
                )?;
                let m3t = if outt.is_m3t() {
                    crate::output_scripts::m3t::M3tParser::new(
                        &data,
                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
                        arg.m3t_use_original_text,
                    )
                    .parse_as_vec()?
                } else {
                    crate::output_scripts::po::PoParser::new(
                        &data,
                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
                    )
                    .parse_as_vec()?
                };
                let current_key = std::path::Path::new(&file)
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .unwrap_or("unknown")
                    .to_string();
                let mut entry: std::collections::HashMap<
                    String,
                    (String, std::collections::HashSet<String>),
                > = std::collections::HashMap::new();
                for (k, v) in m3t {
                    if v.is_empty() {
                        continue;
                    }
                    let k = k.replace("\\[", "[");
                    let v = v.replace("\\[", "[");
                    if let Some((_, count)) = entry.get_mut(&k) {
                        count.insert(v.clone());
                    } else {
                        entry.insert(
                            k.clone(),
                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
                        );
                    }
                    if let Some((_, count)) = global.get_mut(&k) {
                        count.insert(v.clone());
                    } else {
                        global.insert(
                            k,
                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
                        );
                    }
                }
                map.insert(current_key, entry);
            }
            map.insert("global".to_string(), global);
            return Ok(Some(std::sync::Arc::new(
                map.into_iter()
                    .map(|(k, v)| {
                        let v: std::collections::HashMap<_, _> =
                            v.into_iter().map(|(k, (v, s))| (k, (v, s.len()))).collect();
                        (k, v)
                    })
                    .collect(),
            )));
        }
    }
    Ok(None)
}

#[cfg(feature = "yuris")]
pub fn load_yuris_tips_map(
    arg: &Arg,
) -> anyhow::Result<Option<std::sync::Arc<std::collections::HashMap<String, String>>>> {
    if let Some(path) = &arg.yuris_tips_map {
        let data = crate::utils::files::read_file(path)?;
        let result = serde_json::from_slice(&data)?;
        return Ok(Some(std::sync::Arc::new(result)));
    }
    Ok(None)
}