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