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