msg_tool/
args.rs

1use crate::types::*;
2#[allow(unused)]
3use crate::utils::num_range::*;
4use clap::{ArgAction, ArgGroup, Parser, Subcommand};
5
6#[cfg(feature = "flate2")]
7fn parse_compression_level(level: &str) -> Result<u32, String> {
8    let lower = level.to_ascii_lowercase();
9    if lower == "none" {
10        return Ok(0);
11    } else if lower == "best" {
12        return Ok(9);
13    } else if lower == "default" {
14        return Ok(6);
15    } else if lower == "fast" {
16        return Ok(1);
17    }
18    number_range(level, 0, 9)
19}
20
21#[cfg(feature = "mozjpeg")]
22fn parse_jpeg_quality(quality: &str) -> Result<u8, String> {
23    let lower = quality.to_ascii_lowercase();
24    if lower == "best" {
25        return Ok(100);
26    }
27    number_range(quality, 0, 100)
28}
29
30#[cfg(feature = "zstd")]
31fn parse_zstd_compression_level(level: &str) -> Result<i32, String> {
32    let lower = level.to_ascii_lowercase();
33    if lower == "default" {
34        return Ok(3);
35    } else if lower == "best" {
36        return Ok(22);
37    }
38    number_range(level, 0, 22)
39}
40
41#[cfg(feature = "webp")]
42fn parse_webp_quality(quality: &str) -> Result<u8, String> {
43    let lower = quality.to_ascii_lowercase();
44    if lower == "best" {
45        return Ok(100);
46    }
47    number_range(quality, 0, 100)
48}
49
50#[cfg(feature = "audio-flac")]
51fn parse_flac_compression_level(level: &str) -> Result<u32, String> {
52    let lower = level.to_ascii_lowercase();
53    if lower == "fast" {
54        return Ok(0);
55    } else if lower == "best" {
56        return Ok(8);
57    } else if lower == "default" {
58        return Ok(5);
59    }
60    number_range(level, 0, 8)
61}
62
63#[cfg(feature = "image-jxl")]
64fn parse_jxl_distance(s: &str) -> Result<f32, String> {
65    let lower = s.to_ascii_lowercase();
66    if lower == "lossless" {
67        return Ok(0.0);
68    } else if lower == "visually-lossless" {
69        return Ok(1.0);
70    }
71    number_range(s, 0.0, 25.0)
72}
73
74#[cfg(feature = "musica-arc")]
75pub fn get_musica_game_title_value_parser() -> Vec<clap::builder::PossibleValue> {
76    crate::scripts::musica::archive::paz::get_supported_games_with_title()
77        .iter()
78        .map(|(name, title)| {
79            let mut pv = clap::builder::PossibleValue::new(*name);
80            if let Some(t) = title {
81                pv = pv.help(t);
82                let mut alias_count = 0usize;
83                for i in t.split("|") {
84                    pv = pv.alias(i.trim());
85                    alias_count += 1;
86                }
87                // alias for full title
88                if alias_count > 1 {
89                    pv = pv.alias(t);
90                }
91            }
92            pv
93        })
94        .collect()
95}
96
97/// Tools for export and import scripts
98#[derive(Parser, Debug, Clone)]
99#[clap(
100    group = ArgGroup::new("encodingg").multiple(false),
101    group = ArgGroup::new("output_encodingg").multiple(false),
102    group = ArgGroup::new("archive_encodingg").multiple(false),
103    group = ArgGroup::new("artemis_indentg").multiple(false),
104    group = ArgGroup::new("ex_hibit_rld_xor_keyg").multiple(false),
105    group = ArgGroup::new("ex_hibit_rld_def_xor_keyg").multiple(false),
106    group = ArgGroup::new("webp_qualityg").multiple(false),
107    group = ArgGroup::new("cat_system_int_encrypt_passwordg").multiple(false),
108    group = ArgGroup::new("kirikiri_chat_jsong").multiple(false),
109)]
110#[command(
111    version,
112    about,
113    long_about = "Tools for export and import scripts\nhttps://github.com/lifegpc/msg-tool"
114)]
115pub struct Arg {
116    #[arg(short = 't', long, value_enum, global = true)]
117    /// Script type
118    pub script_type: Option<ScriptType>,
119    #[arg(short = 'T', long, value_enum, global = true)]
120    /// Output script type
121    pub output_type: Option<OutputScriptType>,
122    #[arg(short = 'n', long, global = true)]
123    /// Disable extra extension when locating/export output script
124    pub output_no_extra_ext: bool,
125    #[cfg(feature = "image")]
126    #[arg(short = 'i', long, value_enum, global = true)]
127    /// Output image type
128    pub image_type: Option<ImageOutputType>,
129    #[arg(short = 'e', long, value_enum, global = true, group = "encodingg")]
130    /// Script encoding
131    pub encoding: Option<TextEncoding>,
132    #[cfg(windows)]
133    #[arg(short = 'c', long, value_enum, global = true, group = "encodingg")]
134    /// Script code page
135    pub code_page: Option<u32>,
136    #[arg(
137        short = 'E',
138        long,
139        value_enum,
140        global = true,
141        group = "output_encodingg"
142    )]
143    /// Output text encoding
144    pub output_encoding: Option<TextEncoding>,
145    #[cfg(windows)]
146    #[arg(
147        short = 'C',
148        long,
149        value_enum,
150        global = true,
151        group = "output_encodingg"
152    )]
153    /// Output code page
154    pub output_code_page: Option<u32>,
155    #[arg(
156        short = 'a',
157        long,
158        value_enum,
159        global = true,
160        group = "archive_encodingg"
161    )]
162    /// Archive filename encoding
163    pub archive_encoding: Option<TextEncoding>,
164    #[cfg(windows)]
165    #[arg(
166        short = 'A',
167        long,
168        value_enum,
169        global = true,
170        group = "archive_encodingg"
171    )]
172    /// Archive code page
173    pub archive_code_page: Option<u32>,
174    #[cfg(feature = "circus")]
175    #[arg(long, value_enum, global = true)]
176    /// Circus Game
177    pub circus_mes_type: Option<CircusMesType>,
178    #[arg(short, long, action = ArgAction::SetTrue, global = true)]
179    /// Search for script files in the directory recursively
180    pub recursive: bool,
181    #[arg(global = true, action = ArgAction::SetTrue, short, long)]
182    /// Print backtrace on error
183    pub backtrace: bool,
184    #[cfg(feature = "escude-arc")]
185    #[arg(long, action = ArgAction::SetTrue, global = true)]
186    /// Whether to use fake compression for Escude archive
187    pub escude_fake_compress: bool,
188    #[cfg(feature = "escude")]
189    #[arg(long, global = true)]
190    /// The path to the Escude enum script file (enum_scr.bin)
191    pub escude_enum_scr: Option<String>,
192    #[cfg(feature = "bgi")]
193    #[arg(long, action = ArgAction::SetTrue, global = true)]
194    /// Duplicate same strings when importing into BGI scripts.
195    /// Enable this will cause BGI scripts to become very large.
196    pub bgi_import_duplicate: bool,
197    #[cfg(feature = "bgi")]
198    #[arg(long, action = ArgAction::SetTrue, global = true, visible_alias = "bgi-no-append")]
199    /// Disable appending new strings to the end of BGI scripts.
200    /// Disable may cause BGI scripts broken.
201    pub bgi_disable_append: bool,
202    #[cfg(all(feature = "bgi-arc", feature = "bgi-img"))]
203    #[arg(long, global = true)]
204    /// Detect all files in BGI archive as SysGrp Images. By default, only files which name is `sysgrp.arc` will enabled this.
205    pub bgi_is_sysgrp_arc: Option<bool>,
206    #[cfg(feature = "bgi-img")]
207    #[arg(long, global = true)]
208    /// Whether to create scrambled SysGrp images. When in import mode, the default value depends on the original image.
209    /// When in creation mode, it is not enabled by default.
210    pub bgi_img_scramble: Option<bool>,
211    #[cfg(feature = "bgi-img")]
212    #[arg(long, global = true, default_value_t = crate::types::get_default_threads())]
213    /// Workers count for decode BGI compressed images v2 in parallel. Default is half of CPU cores.
214    /// Set this to 1 to disable parallel decoding. 0 means same as 1.
215    pub bgi_img_workers: usize,
216    #[cfg(feature = "cat-system-arc")]
217    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
218    /// CatSystem2 engine int archive password
219    pub cat_system_int_encrypt_password: Option<String>,
220    #[cfg(feature = "cat-system-arc")]
221    #[arg(long, global = true, group = "cat_system_int_encrypt_passwordg")]
222    /// The path to the CatSystem2 engine executable file. Used to get the int archive password.
223    pub cat_system_int_exe: Option<String>,
224    #[cfg(feature = "cat-system-img")]
225    #[arg(long, global = true, action = ArgAction::SetTrue)]
226    /// Draw CatSystem2 image on canvas (if canvas width and height are specified in file)
227    pub cat_system_image_canvas: bool,
228    #[cfg(feature = "kirikiri")]
229    #[arg(long, global = true)]
230    /// Kirikiri language index in script. If not specified, the first language will be used.
231    pub kirikiri_language_index: Option<usize>,
232    #[cfg(feature = "kirikiri")]
233    #[arg(long, global = true)]
234    /// Export chat message to extra json file. (for Kirikiri SCN script.)
235    /// For example, CIRCUS's comu message. Yuzusoft's phone chat message.
236    pub kirikiri_export_chat: bool,
237    #[cfg(feature = "kirikiri")]
238    #[arg(long, global = true, value_delimiter = ',')]
239    /// Kirikiri chat message key. For example, CIRCUS's key is "comumode". Yuzusoft's key is "phonechat".
240    /// If not specified, "comumode" will be used. Multiple keys can be specified separated by comma.
241    pub kirikiri_chat_key: Option<Vec<String>>,
242    #[cfg(feature = "kirikiri")]
243    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
244    /// Kirikiri chat message translation file. (Map<String, String>, key is original text, value is translated text.)
245    pub kirikiri_chat_json: Option<String>,
246    #[cfg(feature = "kirikiri")]
247    #[arg(long, global = true, group = "kirikiri_chat_jsong")]
248    /// Kirikiri chat message translation directory. All json files in this directory will be merged. (Only m3t files are supported.)
249    pub kirikiri_chat_dir: Option<String>,
250    #[cfg(feature = "kirikiri")]
251    #[arg(long, global = true, value_delimiter = ',')]
252    /// Kirikiri language list. First language code is code for language index 1.
253    pub kirikiri_languages: Option<Vec<String>>,
254    #[cfg(feature = "kirikiri")]
255    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-title")]
256    /// Whether to handle title in Kirikiri SCN script.
257    pub kirikiri_title: bool,
258    #[cfg(feature = "kirikiri")]
259    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-chat-no-multilang")]
260    /// Disable multi-language support for Kirikiri chat messages. (for Kirikiri SCN script.)
261    pub kirikiri_chat_no_multilang: bool,
262    #[cfg(feature = "kirikiri")]
263    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "kr-no-empty-lines", visible_alias = "kirikiri-no-empty-lines")]
264    /// Remove empty lines in Kirikiri KS script.
265    pub kirikiri_remove_empty_lines: bool,
266    #[cfg(feature = "kirikiri")]
267    #[arg(
268        long,
269        global = true,
270        value_delimiter = ',',
271        default_value = "nm,set_title,speaker,Talk,talk,cn,name,名前"
272    )]
273    /// Kirikiri name commands, used to extract names from ks script.
274    pub kirikiri_name_commands: Vec<String>,
275    #[cfg(feature = "kirikiri")]
276    #[arg(
277        long,
278        global = true,
279        value_delimiter = ',',
280        default_value = "sel01,sel02,sel03,sel04,AddSelect,ruby,exlink,e_xlink"
281    )]
282    /// Kirikiri message commands, used to extract more message from ks script.
283    pub kirikiri_message_commands: Vec<String>,
284    #[cfg(feature = "image")]
285    #[arg(short = 'f', long, global = true)]
286    /// Output multiple image as `<basename>_<name>.<ext>` instead of `<basename>/<name>.<ext>`
287    pub image_output_flat: bool,
288    #[cfg(feature = "bgi-arc")]
289    #[arg(long, global = true, action = ArgAction::SetTrue)]
290    /// Whether to compress files in BGI archive when packing BGI archive.
291    pub bgi_compress_file: bool,
292    #[cfg(feature = "bgi-arc")]
293    #[arg(long, global = true, default_value_t = 3, value_parser = crate::scripts::bgi::archive::dsc::parse_min_length)]
294    /// Minimum length of match size for DSC compression. Possible values are 2-256.
295    pub bgi_compress_min_len: usize,
296    #[cfg(feature = "emote-img")]
297    #[arg(long, global = true)]
298    /// Whether to overlay PIMG images. (By default, true if all layers are not group layers.)
299    pub emote_pimg_overlay: Option<bool>,
300    #[cfg(feature = "artemis-arc")]
301    #[arg(long, global = true)]
302    /// Disable Artemis archive (.pfs) XOR encryption when packing.
303    pub artemis_arc_disable_xor: bool,
304    #[cfg(feature = "artemis")]
305    #[arg(long, global = true, group = "artemis_indentg")]
306    /// Artemis script indent size, used to format Artemis script.
307    /// Default is 4 spaces.
308    pub artemis_indent: Option<usize>,
309    #[cfg(feature = "artemis")]
310    #[arg(long, global = true, action = ArgAction::SetTrue, group = "artemis_indentg")]
311    /// Disable Artemis script indent, used to format Artemis script.
312    pub artemis_no_indent: bool,
313    #[cfg(feature = "artemis")]
314    #[arg(long, global = true, default_value_t = 100)]
315    /// Max line width in Artemis script, used to format Artemis script.
316    pub artemis_max_line_width: usize,
317    #[cfg(feature = "artemis")]
318    #[arg(long, global = true)]
319    /// Specify the language of Artemis AST script.
320    /// If not specified, the first language will be used.
321    pub artemis_ast_lang: Option<String>,
322    #[cfg(feature = "artemis")]
323    #[arg(long, global = true, action = ArgAction::SetTrue)]
324    /// Do not format lua code in Artemis ASB script(.asb/.iet) when exporting.
325    pub artemis_asb_no_format_lua: bool,
326    // Default value is from tagFilters in macro.iet
327    #[cfg(feature = "artemis-panmimisoft")]
328    #[arg(
329        long,
330        global = true,
331        value_delimiter = ',',
332        default_value = "背景,イベントCG,遅延背景,遅延背景予約,背景予約,遅延イベントCG,遅延イベントCG予約,イベントCG予約,遅延ポップアップ,遅延bgm_in,遅延bgm_out,遅延se_in,遅延se_out,遅延bgs_in,遅延bgs_out,立ち絵face非連動,セーブサムネイル置換終了,シネスコ,ポップアップ"
333    )]
334    /// Artemis Engine blacklist tag names for TXT script.
335    /// This is used to ignore these tags when finding names in Artemis TXT script (ぱんみみそふと).
336    pub artemis_panmimisoft_txt_blacklist_names: Vec<String>,
337    #[cfg(feature = "artemis-panmimisoft")]
338    #[arg(long, global = true)]
339    /// Specify the language of Artemis TXT (ぱんみみそふと) script.
340    /// If not specified, the first language will be used.
341    pub artemis_panmimisoft_txt_lang: Option<String>,
342    #[cfg(feature = "artemis-panmimisoft")]
343    #[arg(long, global = true)]
344    /// The path to the tag.ini file, which contains the tags to be ignored when finding names in Artemis TXT script (ぱんみみそふと).
345    pub artemis_panmimisoft_txt_tag_ini: Option<String>,
346    #[cfg(feature = "cat-system")]
347    #[arg(long, global = true)]
348    /// CatSystem2 CSTL script language, used to extract messages from CSTL script.
349    /// If not specified, the first language will be used.
350    pub cat_system_cstl_lang: Option<String>,
351    #[cfg(feature = "flate2")]
352    #[arg(short = 'z', long, global = true, value_name = "LEVEL", value_parser = parse_compression_level, default_value_t = 6)]
353    /// Zlib compression level. 0 means no compression, 9 means best compression.
354    pub zlib_compression_level: u32,
355    #[cfg(feature = "image")]
356    #[arg(short = 'g', long, global = true, value_enum, default_value_t = PngCompressionLevel::Fast)]
357    /// PNG compression level.
358    pub png_compression_level: PngCompressionLevel,
359    #[cfg(feature = "circus-img")]
360    #[arg(long, global = true, action = ArgAction::SetTrue)]
361    /// Keep original BPP when importing Circus CRX images.
362    pub circus_crx_keep_original_bpp: bool,
363    #[cfg(feature = "circus-img")]
364    #[arg(long, global = true, action = ArgAction::SetTrue)]
365    /// Use zstd compression for Circus CRX images. (CIRCUS Engine don't support this. Hook is required.)
366    pub circus_crx_zstd: bool,
367    #[cfg(feature = "zstd")]
368    #[arg(short = 'Z', long, global = true, value_name = "LEVEL", value_parser = parse_zstd_compression_level, default_value_t = 3)]
369    /// Zstd compression level. 0 means default compression level (3), 22 means best compression.
370    pub zstd_compression_level: i32,
371    #[cfg(feature = "circus-img")]
372    #[arg(long, global = true, value_enum, default_value_t = crate::scripts::circus::image::crx::CircusCrxMode::Auto)]
373    /// Circus CRX image row type mode
374    pub circus_crx_mode: crate::scripts::circus::image::crx::CircusCrxMode,
375    #[cfg(feature = "circus-img")]
376    #[arg(long, global = true, action = ArgAction::SetTrue)]
377    /// Draw Circus CRX images on canvas (if canvas width and height are specified in file)
378    pub circus_crx_canvas: bool,
379    #[arg(short = 'F', long, global = true, action = ArgAction::SetTrue)]
380    /// Force all files in archive to be treated as script files.
381    pub force_script: bool,
382    #[cfg(feature = "ex-hibit")]
383    #[arg(
384        long,
385        global = true,
386        value_name = "HEX",
387        group = "ex_hibit_rld_xor_keyg"
388    )]
389    /// ExHibit xor key for rld script, in hexadecimal format. (e.g. `12345678`)
390    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to find the key.
391    pub ex_hibit_rld_xor_key: Option<String>,
392    #[cfg(feature = "ex-hibit")]
393    #[arg(
394        long,
395        global = true,
396        value_name = "PATH",
397        group = "ex_hibit_rld_xor_keyg"
398    )]
399    /// ExHibit rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
400    pub ex_hibit_rld_xor_key_file: Option<String>,
401    #[cfg(feature = "ex-hibit")]
402    #[arg(
403        long,
404        global = true,
405        value_name = "HEX",
406        group = "ex_hibit_rld_def_xor_keyg"
407    )]
408    /// ExHibit rld def.rld xor key, in hexadecimal format. (e.g. `12345678`)
409    pub ex_hibit_rld_def_xor_key: Option<String>,
410    #[cfg(feature = "ex-hibit")]
411    #[arg(
412        long,
413        global = true,
414        value_name = "PATH",
415        group = "ex_hibit_rld_def_xor_keyg"
416    )]
417    /// ExHibit rld def.rld xor key file, which contains the xor key in hexadecimal format. (e.g. `0x12345678`)
418    pub ex_hibit_rld_def_xor_key_file: Option<String>,
419    #[cfg(feature = "ex-hibit")]
420    #[arg(long, global = true, value_name = "PATH")]
421    /// Path to the ExHibit rld keys file, which contains the keys in BINARY format.
422    /// Use https://github.com/ZQF-ReVN/RxExHIBIT to get this file.
423    pub ex_hibit_rld_keys: Option<String>,
424    #[cfg(feature = "ex-hibit")]
425    #[arg(long, global = true, value_name = "PATH")]
426    /// Path to the ExHibit rld def keys file, which contains the keys in BINARY format.
427    pub ex_hibit_rld_def_keys: Option<String>,
428    #[cfg(feature = "mozjpeg")]
429    #[arg(long, global = true, default_value_t = 80, value_parser = parse_jpeg_quality)]
430    /// JPEG quality for output images, 0-100. 100 means best quality.
431    pub jpeg_quality: u8,
432    #[cfg(feature = "webp")]
433    #[arg(short = 'w', long, global = true, group = "webp_qualityg")]
434    /// Use WebP lossless compression for output images.
435    pub webp_lossless: bool,
436    #[cfg(feature = "webp")]
437    #[arg(short = 'W', long, global = true, value_name = "QUALITY", group = "webp_qualityg", value_parser = parse_webp_quality, default_value_t = 80)]
438    /// WebP quality for output images, 0-100. 100 means best quality.
439    pub webp_quality: u8,
440    #[arg(long, global = true)]
441    /// Try use YAML format instead of JSON when custom exporting.
442    /// By default, this is based on output type. But can be overridden by this option.
443    pub custom_yaml: Option<bool>,
444    #[cfg(feature = "entis-gls")]
445    #[arg(long, global = true)]
446    /// Entis GLS srcxml script language, used to extract messages from srcxml script.
447    /// If not specified, the first language will be used.
448    pub entis_gls_srcxml_lang: Option<String>,
449    #[cfg(feature = "will-plus")]
450    #[arg(long, global = true)]
451    /// Disable disassembly for WillPlus ws2 script.
452    /// Use another parser to parse the script.
453    /// Should only be used when the default parser not works well.
454    pub will_plus_ws2_no_disasm: bool,
455    #[cfg(feature = "lossless-audio")]
456    #[arg(short = 'l', long, global = true, value_enum, default_value_t = LosslessAudioFormat::Wav)]
457    /// Audio format for output lossless audio files.
458    pub lossless_audio_fmt: LosslessAudioFormat,
459    #[cfg(feature = "audio-flac")]
460    #[arg(short = 'L', long, global = true, default_value_t = 5, value_parser = parse_flac_compression_level)]
461    /// FLAC compression level for output FLAC audio files. 0 means fastest compression, 8 means best compression.
462    pub flac_compression_level: u32,
463    #[arg(long, global = true)]
464    /// Add a mark to the end of each message for LLM translation.
465    /// Only works on m3t format.
466    pub llm_trans_mark: Option<String>,
467    #[cfg(feature = "favorite")]
468    #[arg(long, global = true, action = ArgAction::SetTrue)]
469    /// Do not filter ascii strings in Favorite HCB script.
470    pub favorite_hcb_no_filter_ascii: bool,
471    #[cfg(feature = "image-jxl")]
472    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "jxl-no-lossless")]
473    /// Disable JXL lossless compression for output images
474    pub jxl_lossy: bool,
475    #[cfg(feature = "image-jxl")]
476    #[arg(long, global = true, default_value_t = 1.0, value_parser = parse_jxl_distance)]
477    /// JXL distance for output images. 0 means mathematically lossless compression. 1.0 means visually lossless compression.
478    /// Allowed range is 0.0-25.0. Recommended range is 0.5-3.0. Default value is 1
479    pub jxl_distance: f32,
480    #[cfg(feature = "image-jxl")]
481    #[arg(long, global = true, default_value_t = 1, visible_alias = "jxl-jobs")]
482    /// Workers count for encode JXL images in parallel. Default is 1.
483    /// Set this to 1 to disable parallel encoding. 0 means same as 1
484    pub jxl_workers: usize,
485    #[cfg(feature = "image")]
486    #[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")]
487    /// Workers count for encode images in parallel. Default is half of CPU cores.
488    /// Set this to 1 to disable parallel encoding. 0 means same as 1.
489    pub image_workers: usize,
490    #[cfg(feature = "jieba")]
491    #[arg(long, global = true)]
492    /// Path to custom jieba dictionary
493    pub jieba_dict: Option<String>,
494    #[cfg(feature = "emote-img")]
495    #[arg(long, global = true, action = ArgAction::SetTrue, visible_alias = "psb-no-tlg")]
496    /// Do not process TLG images in PSB files.
497    pub psb_no_process_tlg: bool,
498    #[cfg(feature = "softpal-img")]
499    #[arg(long, global = true, visible_alias = "pgd-co")]
500    /// Whether to use compression for Softpal Pgd images.
501    /// WARN: Compress may cause image broken.
502    pub pgd_compress: bool,
503    #[arg(long, global = true)]
504    /// Disable multiple messages section support.
505    pub no_multi_message: bool,
506    #[cfg(feature = "softpal")]
507    #[arg(long, global = true, visible_alias = "softpal-idx")]
508    /// Whether to add message index to Softpal src script when exporting.
509    pub softpal_add_message_index: bool,
510    #[cfg(feature = "kirikiri-arc")]
511    #[arg(long, global = true)]
512    /// Disable decrypt SimpleCrypt files in Kirikiri XP3 archive when extracting.
513    pub xp3_no_simple_crypt: bool,
514    #[cfg(feature = "kirikiri-arc")]
515    #[arg(long, global = true)]
516    /// Disable decompressing mdf files in Kirikiri XP3 archive when extracting.
517    pub xp3_no_mdf_decompress: bool,
518    #[cfg(feature = "kirikiri-arc")]
519    #[arg(long, global = true, default_value = "cdc:32KiB:256KiB:8MiB", value_parser = crate::scripts::kirikiri::archive::xp3::parse_segmenter_config)]
520    /// Configuration for Kirikiri XP3 segmenter when creating XP3 archive.
521    /// none segmenter - none
522    /// fastcdc segmenter - cdc:<min>:<avg>:<max>
523    /// fixed segmenter - fixed:<size>
524    pub xp3_segmenter: crate::scripts::kirikiri::archive::xp3::SegmenterConfig,
525    #[cfg(feature = "kirikiri-arc")]
526    #[arg(long, global = true)]
527    /// Disable compressing files in Kirikiri XP3 archive when creating XP3 archive.
528    pub xp3_no_compress_files: bool,
529    #[cfg(feature = "kirikiri-arc")]
530    #[arg(long, global = true)]
531    /// Disable compressing index in Kirikiri XP3 archive when creating XP3 archive.
532    pub xp3_no_compress_index: bool,
533    #[cfg(feature = "kirikiri-arc")]
534    #[arg(long, global = true, default_value_t = num_cpus::get(), visible_alias = "xp3-compress-jobs")]
535    /// Workers count for compress files in Kirikiri XP3 archive when creating in parallel.
536    pub xp3_compress_workers: usize,
537    #[cfg(feature = "kirikiri-arc")]
538    #[arg(long, global = true)]
539    /// Use zstd compression for files in Kirikiri XP3 archive when creating. (Warning: Kirikiri engine don't support this. Hook is required.)
540    pub xp3_zstd: bool,
541    #[cfg(feature = "kirikiri-arc")]
542    #[arg(
543        long,
544        global = true,
545        default_value_t = 1,
546        visible_alias = "xp3-pack-jobs"
547    )]
548    /// Workers count for packing file in Kirikiri XP3 archive in parallel.
549    /// This not works when segment is disabled.
550    pub xp3_pack_workers: usize,
551    #[cfg(feature = "kirikiri")]
552    #[arg(long, global = true)]
553    /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored.
554    pub kirikiri_language_insert: bool,
555    #[cfg(feature = "musica-arc")]
556    #[arg(long, global = true, value_parser = get_musica_game_title_value_parser())]
557    /// Musica game title for paz archive.
558    pub musica_game_title: Option<String>,
559    #[cfg(feature = "musica-arc")]
560    #[arg(long, global = true)]
561    /// Musica xor key for paz archive.
562    pub musica_xor_key: Option<u8>,
563    #[cfg(feature = "musica-arc")]
564    #[arg(long, global = true)]
565    /// Compress files in Musica paz archive when packing paz archive.
566    pub musica_compress: bool,
567    #[arg(short = 'x', long, default_value_t = 0, global = true)]
568    /// Exit code when some jobs failed
569    pub exit_code: i32,
570    #[arg(short = 'X', long, global = true)]
571    /// Exit code when all jobs failed. By default, this is same as exit_code. This can override exit_code when all jobs failed.
572    pub exit_code_all_failed: Option<i32>,
573    #[arg(long, global = true)]
574    /// Do not add quote to translated text when exporting to m3t files.
575    pub m3t_no_quote: bool,
576    #[cfg(feature = "kirikiri-arc")]
577    #[arg(long, global = true)]
578    /// Disable adler32 checksum for Kirikiri XP3 archive when creating.
579    /// This will keep compatibility with https://github.com/arcusmaximus/KirikiriTools tool.
580    pub xp3_no_adler: bool,
581    #[cfg(feature = "bgi")]
582    #[arg(long, global = true)]
583    /// Add an additional space at the end of message in BGI scripts when importing.
584    /// This may help BGI engine to display the message correctly in save/load screen for some games.
585    pub bgi_add_space: bool,
586    #[command(subcommand)]
587    /// Command
588    pub command: Command,
589}
590
591#[derive(Parser, Debug, Clone)]
592#[clap(group = ArgGroup::new("patched_encodingg").multiple(false), group = ArgGroup::new("patched_archive_encodingg").multiple(false))]
593pub struct ImportArgs {
594    /// Input script file or directory
595    pub input: String,
596    /// Text file or directory
597    pub output: String,
598    /// Patched script file or directory
599    pub patched: String,
600    #[arg(short = 'p', long, group = "patched_encodingg")]
601    /// Patched script encoding
602    pub patched_encoding: Option<TextEncoding>,
603    #[cfg(windows)]
604    #[arg(short = 'P', long, group = "patched_encodingg")]
605    /// Patched script code page
606    pub patched_code_page: Option<u32>,
607    #[arg(
608        long,
609        value_enum,
610        group = "patched_archive_encodingg",
611        visible_alias = "pa"
612    )]
613    /// Patched archive filename encoding
614    pub patched_archive_encoding: Option<TextEncoding>,
615    #[cfg(windows)]
616    #[arg(
617        long,
618        value_enum,
619        group = "patched_archive_encodingg",
620        visible_alias = "PA"
621    )]
622    /// Patched archive code page
623    pub patched_archive_code_page: Option<u32>,
624    #[arg(long)]
625    /// Patched script format type
626    pub patched_format: Option<FormatType>,
627    #[arg(long)]
628    /// Fixed length of one line in patched script (for fixed format)
629    pub patched_fixed_length: Option<usize>,
630    #[arg(long, action = ArgAction::SetTrue)]
631    /// Keep original line breaks in patched script (for fixed format)
632    pub patched_keep_original: bool,
633    #[arg(long, action = ArgAction::SetTrue)]
634    /// Break words in patched script (for fixed format)
635    pub patched_break_words: bool,
636    #[arg(long, action = ArgAction::SetTrue)]
637    /// Insert fullwidth space at the start of line in patched script (for fixed format)
638    pub patched_insert_fullwidth_space_at_line_start: bool,
639    #[arg(long, action = ArgAction::SetTrue)]
640    /// If a line break occurs in the middle of some symbols, bring the sentence to next line (for fixed format)
641    pub patched_break_with_sentence: bool,
642    #[cfg(feature = "jieba")]
643    #[arg(long, action = ArgAction::SetTrue)]
644    /// Whether to disable break Chinese words at the end of the line.
645    pub patched_no_break_chinese_words: bool,
646    #[arg(long)]
647    /// Name table file
648    pub name_csv: Option<String>,
649    #[arg(long)]
650    /// Replacement table file
651    pub replacement_json: Option<String>,
652    #[arg(long, action = ArgAction::SetTrue)]
653    pub warn_when_output_file_not_found: bool,
654    #[arg(long)]
655    /// Output dependency file path. This file will contain a list of all files used during import.
656    pub dep_file: Option<String>,
657    #[arg(short = 'j', long, default_value_t = 1)]
658    /// Workers count for import scripts in parallel.
659    pub jobs: usize,
660}
661
662#[derive(Subcommand, Debug, Clone)]
663/// Commands
664pub enum Command {
665    /// Extract from script
666    Export {
667        /// Input script file or directory
668        input: String,
669        /// Output file or directory
670        output: Option<String>,
671    },
672    /// Import to script
673    Import(ImportArgs),
674    /// Pack files to archive
675    Pack {
676        /// Input directory
677        input: String,
678        /// Output archive file
679        output: Option<String>,
680        #[arg(long)]
681        /// Use \ as path separator instead of / in archive
682        backslash: bool,
683    },
684    /// Unpack archive to directory
685    Unpack {
686        /// Input archive file
687        input: String,
688        /// Output directory
689        output: Option<String>,
690    },
691    /// Create a new script file
692    Create {
693        /// Input script
694        input: String,
695        /// Output script file
696        output: Option<String>,
697    },
698    /// Pack files to archive (version 2)
699    PackV2 {
700        #[arg(short = 'o', long)]
701        /// Output archive file
702        output: Option<String>,
703        /// Path to input files/directories
704        input: Vec<String>,
705        #[arg(long)]
706        /// Use \ as path separator instead of / in archive
707        backslash: bool,
708        #[arg(long)]
709        /// Do not create directory entries in archive. This means all files are stored in a flat structure.
710        no_dir: bool,
711        #[arg(long)]
712        /// Output dependency file path. This file will contain a list of all files packed in the archive.
713        dep_file: Option<String>,
714    },
715}
716
717pub fn parse_args() -> Arg {
718    Arg::parse()
719}
720
721#[cfg(feature = "ex-hibit")]
722pub fn load_ex_hibit_rld_xor_key(arg: &Arg) -> anyhow::Result<Option<u32>> {
723    if let Some(key) = &arg.ex_hibit_rld_xor_key {
724        if key.starts_with("0x") {
725            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
726        } else {
727            return Ok(Some(u32::from_str_radix(key, 16)?));
728        }
729    }
730    if let Some(file) = &arg.ex_hibit_rld_xor_key_file {
731        let key = std::fs::read_to_string(file)?.trim().to_string();
732        if key.starts_with("0x") {
733            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
734        } else {
735            return Ok(Some(u32::from_str_radix(&key, 16)?));
736        }
737    }
738    Ok(None)
739}
740
741#[cfg(feature = "ex-hibit")]
742pub fn load_ex_hibit_rld_def_xor_key(arg: &crate::args::Arg) -> anyhow::Result<Option<u32>> {
743    if let Some(key) = &arg.ex_hibit_rld_def_xor_key {
744        if key.starts_with("0x") {
745            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
746        } else {
747            return Ok(Some(u32::from_str_radix(key, 16)?));
748        }
749    }
750    if let Some(file) = &arg.ex_hibit_rld_def_xor_key_file {
751        let key = std::fs::read_to_string(file)?.trim().to_string();
752        if key.starts_with("0x") {
753            return Ok(Some(u32::from_str_radix(&key[2..], 16)?));
754        } else {
755            return Ok(Some(u32::from_str_radix(&key, 16)?));
756        }
757    }
758    Ok(None)
759}
760
761#[cfg(feature = "cat-system-arc")]
762pub fn get_cat_system_int_encrypt_password(arg: &Arg) -> anyhow::Result<Option<String>> {
763    if let Some(exe) = &arg.cat_system_int_exe {
764        return Ok(Some(
765            crate::scripts::cat_system::archive::int::get_password_from_exe(exe)?,
766        ));
767    }
768    if let Some(password) = &arg.cat_system_int_encrypt_password {
769        return Ok(Some(password.clone()));
770    }
771    Ok(None)
772}
773
774#[cfg(feature = "artemis-panmimisoft")]
775pub fn get_artemis_panmimisoft_txt_blacklist_names(
776    arg: &Arg,
777) -> anyhow::Result<std::collections::HashSet<String>> {
778    match &arg.artemis_panmimisoft_txt_tag_ini {
779        Some(path) => {
780            let mut set = crate::scripts::artemis::panmimisoft::txt::read_tags_from_ini(path)?;
781            for name in &arg.artemis_panmimisoft_txt_blacklist_names {
782                set.insert(name.clone());
783            }
784            Ok(set)
785        }
786        None => Ok(arg
787            .artemis_panmimisoft_txt_blacklist_names
788            .iter()
789            .cloned()
790            .collect()),
791    }
792}
793
794#[cfg(feature = "kirikiri")]
795pub fn load_kirikiri_chat_json(
796    arg: &Arg,
797) -> anyhow::Result<
798    Option<
799        std::sync::Arc<
800            std::collections::HashMap<String, std::collections::HashMap<String, (String, usize)>>,
801        >,
802    >,
803> {
804    if let Some(path) = &arg.kirikiri_chat_json {
805        return Ok(Some(std::sync::Arc::new(
806            crate::scripts::kirikiri::read_kirikiri_comu_json(path)?
807                .into_iter()
808                .map(|(k, v)| {
809                    let v: std::collections::HashMap<_, _> =
810                        v.into_iter().map(|(k, v)| (k, (v, 1))).collect();
811                    (k, v)
812                })
813                .collect(),
814        )));
815    }
816    if let Some(dir) = &arg.kirikiri_chat_dir {
817        let mut outt = arg.output_type.unwrap_or(OutputScriptType::M3t);
818        if !matches!(
819            outt,
820            OutputScriptType::M3t
821                | OutputScriptType::M3ta
822                | OutputScriptType::M3tTxt
823                | OutputScriptType::Po
824                | OutputScriptType::Pot
825        ) {
826            outt = OutputScriptType::M3t;
827        }
828        let files = crate::utils::files::find_ext_files(dir, arg.recursive, &[outt.as_ref()])?;
829        if !files.is_empty() {
830            let mut map = std::collections::HashMap::new();
831            let mut global: std::collections::HashMap<
832                String,
833                (String, std::collections::HashSet<String>),
834            > = std::collections::HashMap::new();
835            for file in files {
836                let f = crate::utils::files::read_file(&file)?;
837                let data = crate::utils::encoding::decode_to_string(
838                    crate::get_output_encoding(arg),
839                    &f,
840                    true,
841                )?;
842                let m3t = if outt.is_m3t() {
843                    crate::output_scripts::m3t::M3tParser::new(
844                        &data,
845                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
846                    )
847                    .parse_as_vec()?
848                } else {
849                    crate::output_scripts::po::PoParser::new(
850                        &data,
851                        arg.llm_trans_mark.as_ref().map(|s| s.as_str()),
852                    )
853                    .parse_as_vec()?
854                };
855                let current_key = std::path::Path::new(&file)
856                    .file_stem()
857                    .and_then(|s| s.to_str())
858                    .unwrap_or("unknown")
859                    .to_string();
860                let mut entry: std::collections::HashMap<
861                    String,
862                    (String, std::collections::HashSet<String>),
863                > = std::collections::HashMap::new();
864                for (k, v) in m3t {
865                    if v.is_empty() {
866                        continue;
867                    }
868                    let k = k.replace("\\[", "[");
869                    let v = v.replace("\\[", "[");
870                    if let Some((_, count)) = entry.get_mut(&k) {
871                        count.insert(v.clone());
872                    } else {
873                        entry.insert(
874                            k.clone(),
875                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
876                        );
877                    }
878                    if let Some((_, count)) = global.get_mut(&k) {
879                        count.insert(v.clone());
880                    } else {
881                        global.insert(
882                            k,
883                            (v.clone(), std::collections::HashSet::from_iter([v.clone()])),
884                        );
885                    }
886                }
887                map.insert(current_key, entry);
888            }
889            map.insert("global".to_string(), global);
890            return Ok(Some(std::sync::Arc::new(
891                map.into_iter()
892                    .map(|(k, v)| {
893                        let v: std::collections::HashMap<_, _> =
894                            v.into_iter().map(|(k, (v, s))| (k, (v, s.len()))).collect();
895                        (k, v)
896                    })
897                    .collect(),
898            )));
899        }
900    }
901    Ok(None)
902}