config/
args.rs

1use std::env::{split_paths, var_os};
2use std::fmt::Debug;
3use std::fs;
4use std::path::PathBuf;
5
6use clap::Subcommand;
7use clap::{Parser, builder::BoolishValueParser, builder::OsStr};
8
9use crate::log_options::LogOptions;
10
11use super::colors::styles;
12
13pub const CARGO_PKG_NAME: &str = "mod_installer";
14
15pub const LONG: &str = r"
16
17  /\/\   ___   __| | (_)_ __  ___| |_ __ _| | | ___ _ __
18 /    \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
19/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | |  __/ |
20\/    \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
21";
22
23#[cfg(not(target_os = "windows"))]
24pub const WEIDU_FILE_NAME: &str = "weidu";
25#[cfg(target_os = "windows")]
26pub const WEIDU_FILE_NAME: &str = "weidu.exe";
27
28// https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes
29#[derive(Parser, Debug, PartialEq)]
30#[command(
31    version,
32    propagate_version = true,
33    styles = styles(),
34    about = format!("{}\n{}", LONG, std::env::var("CARGO_PKG_DESCRIPTION").unwrap_or_default())
35)]
36pub struct Args {
37    /// Install Type
38    #[command(subcommand)]
39    pub command: CommandType,
40}
41
42/// Type of Install, Normal or EET
43#[derive(Subcommand, Debug, PartialEq, Clone)]
44pub enum CommandType {
45    #[command()]
46    Normal(Normal),
47    #[command()]
48    Eet(Eet),
49    #[command(subcommand)]
50    Scan(Scan),
51}
52
53/// Normal install for (BG1EE,BG2EE,IWDEE) (STABLE)
54#[derive(Parser, Debug, PartialEq, Clone)]
55#[clap(short_flag = 'n')]
56pub struct Normal {
57    /// Path to target log
58    #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
59    pub log_file: PathBuf,
60
61    /// Absolute Path to game directory
62    #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
63    pub game_directory: PathBuf,
64
65    /// Instead of operating on an existing directory, create a new one with this flag as its name and then copy the original contents into it.
66    #[clap(env, long, short = 'n', required = false)]
67    pub generate_directory: Option<PathBuf>,
68
69    /// CommonOptions
70    #[clap(flatten)]
71    pub options: Options,
72}
73
74/// EET install for (eet) (BETA)
75#[derive(Parser, Debug, PartialEq, Clone)]
76#[clap(short_flag = 'e')]
77pub struct Eet {
78    /// Absolute Path to bg1ee game directory
79    #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
80    pub bg1_game_directory: PathBuf,
81
82    /// Path to bg1ee weidu.log file
83    #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
84    pub bg1_log_file: PathBuf,
85
86    /// Absolute Path to bg2ee game directory
87    #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
88    pub bg2_game_directory: PathBuf,
89
90    /// Path to bg2ee weidu.log file
91    #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
92    pub bg2_log_file: PathBuf,
93
94    /// Generates a new pre-eet directory.
95    #[clap(env, short = 'p', long, value_parser = path_must_exist)]
96    pub new_pre_eet_dir: Option<PathBuf>,
97
98    /// Generates a new eet directory.
99    #[clap(env, short = 'n', long, value_parser = path_must_exist)]
100    pub new_eet_dir: Option<PathBuf>,
101
102    /// CommonOptions
103    #[clap(flatten)]
104    pub options: Options,
105}
106
107/// Scan for (BG1EE,BG2EE,IWDEE) (ALPHA)
108#[derive(Subcommand, Debug, PartialEq, Clone)]
109#[clap(short_flag = 's')]
110pub enum Scan {
111    #[command()]
112    Langauges(ScanLangauges),
113    #[command()]
114    Components(ScanComponents),
115}
116
117#[derive(Parser, Debug, PartialEq, Clone)]
118#[clap(short_flag = 'l')]
119pub struct ScanLangauges {
120    /// filter by selected language
121    #[clap(short, long, required = false, default_value = "")]
122    pub filter_by_selected_language: String,
123
124    /// CommonOptions
125    #[clap(flatten)]
126    pub options: Options,
127}
128
129#[derive(Parser, Debug, PartialEq, Clone)]
130#[clap(short_flag = 'c')]
131pub struct ScanComponents {
132    /// Absolute Path to game directory
133    #[clap(env, short, long, value_parser = parse_absolute_path)]
134    pub game_directory: PathBuf,
135
136    /// filter by selected language
137    #[clap(short, long, required = false, default_value = "")]
138    pub filter_by_selected_language: String,
139
140    /// CommonOptions
141    #[clap(flatten)]
142    pub options: Options,
143}
144
145#[derive(Parser, Debug, PartialEq, Clone, Default)]
146pub struct Options {
147    /// Absolute Path to weidu binary
148    #[clap(
149        env,
150        short,
151        long,
152        value_parser = parse_absolute_path,
153        default_value_os_t = find_weidu_bin(),
154        default_missing_value = find_weidu_bin_on_path(),
155        required = false
156    )]
157    pub weidu_binary: PathBuf,
158
159    /// Path to mod directories
160    #[clap(
161        env,
162        short,
163        long,
164        value_parser = path_exists_full,
165        use_value_delimiter = true,
166        value_delimiter = ',',
167        default_values_os_t = current_work_dir(),
168        default_missing_value = working_dir(),
169        required = false
170    )]
171    pub mod_directories: Vec<PathBuf>,
172
173    /// Game Language
174    #[clap(short, long, default_value = "en_US")]
175    pub language: String,
176
177    /// Depth to walk folder structure
178    #[clap(env, long, short, default_value = "5")]
179    pub depth: usize,
180
181    /// Compare against installed weidu log, note this is best effort
182    #[clap(
183        env,
184        short,
185        long,
186        num_args=0..=1,
187        action = clap::ArgAction::SetFalse,
188        default_value_t = true,
189        value_parser = BoolishValueParser::new(),
190    )]
191    pub skip_installed: bool,
192
193    /// If a warning occurs in the weidu child process exit
194    #[clap(
195        env,
196        short,
197        long,
198        num_args=0..=1,
199        action = clap::ArgAction::SetTrue,
200        default_value_t = false,
201        value_parser = BoolishValueParser::new(),
202        conflicts_with = "never_abort",
203    )]
204    pub abort_on_warnings: bool,
205
206    /// If an error occurs in the weidu child process continue
207    #[clap(
208        env,
209        short = 'v',
210        long,
211        num_args=0..=1,
212        action = clap::ArgAction::SetTrue,
213        default_value_t = false,
214        value_parser = BoolishValueParser::new(),
215        conflicts_with = "abort_on_warnings",
216    )]
217    pub never_abort: bool,
218
219    /// Timeout time per mod in seconds, default is 1 hour
220    #[clap(env, long, short, default_value = "3600")]
221    pub timeout: usize,
222
223    /// Weidu log setting "autolog,logapp,log-extern" is default
224    #[clap(
225        env,
226        long,
227        short='u',
228        use_value_delimiter = true,
229        value_delimiter = ',',
230        default_value = "autolog,logapp,log-extern",
231        value_parser = LogOptions::value_parser,
232        required = false
233    )]
234    pub weidu_log_mode: Vec<LogOptions>,
235
236    /// Strict Version and Component/SubComponent matching
237    #[clap(
238        env,
239        short = 'x',
240        long,
241        num_args=0..=1,
242        action = clap::ArgAction::SetTrue,
243        default_value_t = false,
244        value_parser = BoolishValueParser::new(),
245    )]
246    pub strict_matching: bool,
247
248    /// When a missing log is discovered ask the user for the download uri, download the mod and install it
249    #[clap(
250        env,
251        long,
252        num_args=0..=1,
253        action = clap::ArgAction::SetFalse,
254        default_value_t = true,
255        value_parser = BoolishValueParser::new(),
256    )]
257    pub download: bool,
258
259    /// Force copy mod folder, even if the mod folder was found in the game directory
260    #[clap(
261        env,
262        short,
263        long,
264        num_args=0..=1,
265        action = clap::ArgAction::SetTrue,
266        default_value_t = false,
267        value_parser = BoolishValueParser::new(),
268    )]
269    pub overwrite: bool,
270
271    /// Strict weidu log checking
272    #[clap(
273        env,
274        short,
275        long,
276        num_args=0..=1,
277        action = clap::ArgAction::SetFalse,
278        default_value_t = true,
279        value_parser = BoolishValueParser::new(),
280    )]
281    pub check_last_installed: bool,
282
283    /// Tick
284    #[clap(env, short = 'i', long, default_value_t = 500)]
285    pub tick: u64,
286
287    /// Lookback
288    #[clap(env, short = '0', long, default_value_t = 10)]
289    pub lookback: usize,
290
291    /// Casefold only available for linux ext4
292    #[clap(
293        env,
294        long,
295        num_args=0..=1,
296        action = clap::ArgAction::SetTrue,
297        default_value_t = false,
298        value_parser = BoolishValueParser::new(),
299    )]
300    pub casefold: bool,
301
302    /// Generic weidu args
303    #[clap(short = 'k', long, use_value_delimiter = true, value_delimiter = ',')]
304    pub generic_weidu_args: Vec<String>,
305}
306
307pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
308    let path = PathBuf::from(arg);
309    path.try_exists()?;
310    Ok(path)
311}
312
313fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
314    fs::canonicalize(path_must_exist(arg)?)
315}
316
317fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
318    let path = path_must_exist(arg).map_err(|err| err.to_string())?;
319    fs::canonicalize(path).map_err(|err| err.to_string())
320}
321
322fn find_weidu_bin() -> PathBuf {
323    if let Some(paths) = var_os("PATH") {
324        for path in split_paths(&paths) {
325            let full_path = path.join(WEIDU_FILE_NAME);
326            if full_path.is_file() && !full_path.is_dir() {
327                return full_path;
328            }
329        }
330    }
331    PathBuf::new()
332}
333
334fn find_weidu_bin_on_path() -> OsStr {
335    OsStr::from(find_weidu_bin().into_os_string())
336}
337
338fn current_work_dir() -> Vec<PathBuf> {
339    vec![std::env::current_dir().unwrap()]
340}
341
342fn working_dir() -> OsStr {
343    OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
344}
345
346#[cfg(test)]
347mod tests {
348
349    use super::*;
350    use pretty_assertions::assert_eq;
351    use std::{error::Error, path::PathBuf};
352
353    #[test]
354    fn test_bool_flags() -> Result<(), Box<dyn Error>> {
355        let workspace_root: PathBuf = std::env::current_dir()?;
356        let fake_game_dir: PathBuf = workspace_root
357            .parent()
358            .ok_or("Could not get workspace root")?
359            .join("fixtures");
360        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
361        let fake_log_file = fake_game_dir.clone().join("weidu.log");
362        let fake_mod_dirs = fake_game_dir.clone().join("mods");
363        let tests = vec![
364            ("true", true),
365            ("t", true),
366            ("yes", true),
367            ("y", true),
368            ("1", true),
369            ("false", false),
370            ("f", false),
371            ("no", false),
372            ("n", false),
373            ("0", false),
374        ];
375        for (flag_value, expected_flag_value) in tests {
376            let expected = Args {
377                command: CommandType::Normal(Normal {
378                    log_file: fake_log_file.clone(),
379                    game_directory: fake_game_dir.clone(),
380                    generate_directory: None,
381                    options: Options {
382                        weidu_binary: fake_weidu_bin.clone(),
383                        mod_directories: vec![fake_mod_dirs.clone()],
384                        language: "en_US".to_string(),
385                        depth: 5,
386                        skip_installed: expected_flag_value,
387                        abort_on_warnings: expected_flag_value,
388                        timeout: 3600,
389                        weidu_log_mode: vec![
390                            LogOptions::AutoLog,
391                            LogOptions::LogAppend,
392                            LogOptions::LogExternal,
393                        ],
394                        strict_matching: true,
395                        download: true,
396                        overwrite: false,
397                        check_last_installed: true,
398                        tick: 500,
399                        lookback: 10,
400                        casefold: false,
401                        never_abort: false,
402                        generic_weidu_args: vec![],
403                    },
404                }),
405            };
406            let test_arg_string = format!(
407                "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
408                flag_value,
409                flag_value,
410                fake_weidu_bin.to_str().unwrap_or_default(),
411                fake_mod_dirs.to_str().unwrap_or_default(),
412                fake_log_file.to_str().unwrap_or_default(),
413                fake_game_dir.to_str().unwrap_or_default(),
414            );
415            let result = Args::parse_from(test_arg_string.split(' '));
416            assert_eq!(
417                result, expected,
418                "Result {result:?} didn't match Expected {expected:?}",
419            );
420        }
421        Ok(())
422    }
423
424    #[test]
425    fn test_eet_flags() -> Result<(), Box<dyn Error>> {
426        let workspace_root: PathBuf = std::env::current_dir()?;
427        let fake_game_dir: PathBuf = workspace_root
428            .parent()
429            .ok_or("Could not get workspace root")?
430            .join("fixtures");
431        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
432        let fake_log_file = fake_game_dir.clone().join("weidu.log");
433        let new_dir = PathBuf::new().join("test");
434        let expected_flag_value = true;
435
436        let expected = Args {
437            command: CommandType::Eet(Eet {
438                bg1_game_directory: fake_game_dir.clone(),
439                bg1_log_file: fake_log_file.clone(),
440                bg2_game_directory: fake_game_dir.clone(),
441                bg2_log_file: fake_log_file.clone(),
442                options: Options {
443                    weidu_binary: fake_weidu_bin.clone(),
444                    mod_directories: vec![std::env::current_dir().unwrap()],
445                    language: "en_US".to_string(),
446                    depth: 5,
447                    skip_installed: expected_flag_value,
448                    abort_on_warnings: !expected_flag_value,
449                    timeout: 3600,
450                    weidu_log_mode: vec![
451                        LogOptions::AutoLog,
452                        LogOptions::LogAppend,
453                        LogOptions::LogExternal,
454                    ],
455                    strict_matching: !expected_flag_value,
456                    download: expected_flag_value,
457                    overwrite: !expected_flag_value,
458                    check_last_installed: expected_flag_value,
459                    tick: 500,
460                    lookback: 10,
461                    casefold: false,
462                    never_abort: false,
463                    generic_weidu_args: vec![],
464                },
465                new_pre_eet_dir: None,
466                new_eet_dir: Some("test".into()),
467            }),
468        };
469        let test_arg_string = format!(
470            "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
471            fake_weidu_bin.to_str().unwrap_or_default(),
472            fake_game_dir.to_str().unwrap_or_default(),
473            fake_log_file.to_str().unwrap_or_default(),
474            fake_game_dir.to_str().unwrap_or_default(),
475            fake_log_file.to_str().unwrap_or_default(),
476            new_dir.to_str().unwrap_or_default(),
477        );
478        let result = Args::parse_from(test_arg_string.split(' '));
479        assert_eq!(
480            result, expected,
481            "Result {result:?} didn't match Expected {expected:?}",
482        );
483
484        Ok(())
485    }
486    #[test]
487    fn test_log_flags() -> Result<(), Box<dyn Error>> {
488        let workspace_root: PathBuf = std::env::current_dir()?;
489        let fake_game_dir: PathBuf = workspace_root
490            .parent()
491            .ok_or("Could not get workspace root")?
492            .join("fixtures");
493        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
494        let fake_log_file = fake_game_dir.clone().join("weidu.log");
495        let fake_mod_dirs = fake_game_dir.clone().join("mods");
496        let log_file = tempfile::NamedTempFile::new()?;
497        let log_file_path = log_file.path().as_os_str().to_str().unwrap_or_default();
498        let expected = Args {
499            command: CommandType::Normal(Normal {
500                log_file: fake_log_file.clone(),
501                game_directory: fake_game_dir.clone(),
502                generate_directory: None,
503                options: Options {
504                    weidu_binary: fake_weidu_bin.clone(),
505                    mod_directories: vec![fake_mod_dirs.clone()],
506                    language: "en_US".to_string(),
507                    depth: 5,
508                    skip_installed: true,
509                    abort_on_warnings: false,
510                    timeout: 3600,
511                    weidu_log_mode: vec![
512                        LogOptions::AutoLog,
513                        LogOptions::LogAppend,
514                        LogOptions::LogExternal,
515                        LogOptions::Log(log_file_path.into()),
516                    ],
517                    strict_matching: false,
518                    download: true,
519                    overwrite: false,
520                    check_last_installed: true,
521                    tick: 500,
522                    lookback: 10,
523                    casefold: false,
524                    never_abort: false,
525                    generic_weidu_args: vec![],
526                },
527            }),
528        };
529        let log_arg = format!("autolog,logapp,log-extern,log {log_file_path}");
530        let test_arg_string = vec![
531            "mod_installer",
532            "-n",
533            "-w",
534            fake_weidu_bin.to_str().unwrap_or_default(),
535            "-m",
536            fake_mod_dirs.to_str().unwrap_or_default(),
537            "--log-file",
538            fake_log_file.to_str().unwrap_or_default(),
539            "--game-directory",
540            fake_game_dir.to_str().unwrap_or_default(),
541            "--weidu-log-mode",
542            &log_arg,
543        ];
544        let result = Args::parse_from(test_arg_string);
545        assert_eq!(
546            result, expected,
547            "Result {result:?} didn't match Expected {expected:?}",
548        );
549        Ok(())
550    }
551}