mod_installer/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::{builder::BoolishValueParser, builder::OsStr, Parser};
8
9use super::colors::styles;
10
11pub(crate) const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
12
13pub(crate) const LONG: &str = r"
14
15  /\/\   ___   __| | (_)_ __  ___| |_ __ _| | | ___ _ __
16 /    \ / _ \ / _` | | | '_ \/ __| __/ _` | | |/ _ \ '__|
17/ /\/\ \ (_) | (_| | | | | | \__ \ || (_| | | |  __/ |
18\/    \/\___/ \__,_| |_|_| |_|___/\__\__,_|_|_|\___|_|
19";
20
21pub(crate) const WEIDU_LOG_MODE_ERROR: &str = r"
22Please provide a valid weidu logging setting, options are:
23--weidu-log-mode log X       log output and details to X
24--weidu-log-mode autolog     log output and details to WSETUP.DEBUG
25--weidu-log-mode logapp      append to log instead of overwriting
26--weidu-log-mode log-extern  also log output from commands invoked by WeiDU
27";
28
29#[cfg(not(target_os = "windows"))]
30pub(crate) const WEIDU_FILE_NAME: &str = "weidu";
31#[cfg(target_os = "windows")]
32pub(crate) const WEIDU_FILE_NAME: &str = "weidu.exe";
33
34// https://docs.rs/clap/latest/clap/_derive/index.html#arg-attributes
35#[derive(Parser, Debug, PartialEq)]
36#[command(
37    version,
38    propagate_version = true,
39    styles = styles(),
40    about = format!("{}\n{}", LONG, std::env::var("CARGO_PKG_DESCRIPTION").unwrap_or_default())
41)]
42pub(crate) struct Args {
43    /// Install Type
44    #[command(subcommand)]
45    pub(crate) command: InstallType,
46}
47
48/// Type of Install, Normal or EET
49#[derive(Subcommand, Debug, PartialEq, Clone)]
50pub(crate) enum InstallType {
51    #[command()]
52    Normal(Normal),
53    #[command()]
54    Eet(Eet),
55}
56
57/// Normal install for (BG1EE,BG2EE,IWDEE) (STABLE)
58#[derive(Parser, Debug, PartialEq, Clone)]
59#[clap(short_flag = 'n')]
60pub(crate) struct Normal {
61    /// Path to target log
62    #[clap(env, long, short = 'f', value_parser = path_must_exist, required = true)]
63    pub(crate) log_file: PathBuf,
64
65    /// Absolute Path to game directory
66    #[clap(env, short, long, value_parser = parse_absolute_path, required = true)]
67    pub(crate) game_directory: PathBuf,
68
69    /// 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.
70    #[clap(env, long, short = 'n', required = false)]
71    pub(crate) new_game_directory: Option<PathBuf>,
72
73    /// CommonOptions
74    #[clap(flatten)]
75    pub(crate) options: Options,
76}
77
78/// EET install for (eet) (ALPHA)
79#[derive(Parser, Debug, PartialEq, Clone)]
80#[clap(short_flag = 'e')]
81pub(crate) struct Eet {
82    /// Absolute Path to bg1ee game directory
83    #[clap(env, short='1', long, value_parser = parse_absolute_path, required = true)]
84    pub(crate) bg1_game_directory: PathBuf,
85
86    /// Path to bg1ee weidu.log file
87    #[clap(env, short='y', long, value_parser = path_must_exist, required = true)]
88    pub(crate) bg1_log_file: PathBuf,
89
90    /// Absolute Path to bg2ee game directory
91    #[clap(env, short='2', long, value_parser = parse_absolute_path, required = true)]
92    pub(crate) bg2_game_directory: PathBuf,
93
94    /// Path to bg2ee weidu.log file
95    #[clap(env, short='z', long, value_parser = path_must_exist, required = true)]
96    pub(crate) bg2_log_file: PathBuf,
97
98    /// 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.
99    #[clap(
100        env,
101        short,
102        long,
103        action = clap::ArgAction::SetTrue,
104        required = false,
105    )]
106    pub(crate) generate_directories: bool,
107
108    /// If generate_directories is true. This is the path for the new pre-eet directory.
109    #[clap(env, short = 'p', long, value_parser = path_must_exist)]
110    pub(crate) new_pre_eet_dir: Option<PathBuf>,
111
112    /// If generate_directories is true. This is the path for the new eet directory.
113    #[clap(env, short = 'n', long, value_parser = path_must_exist)]
114    pub(crate) new_eet_dir: Option<PathBuf>,
115
116    /// CommonOptions
117    #[clap(flatten)]
118    pub(crate) options: Options,
119}
120
121#[derive(Parser, Debug, PartialEq, Clone, Default)]
122pub(crate) struct Options {
123    /// Absolute Path to weidu binary
124    #[clap(
125        env,
126        short,
127        long,
128        value_parser = parse_absolute_path,
129        default_value_os_t = find_weidu_bin(),
130        default_missing_value = find_weidu_bin_on_path(),
131        required = false
132    )]
133    pub(crate) weidu_binary: PathBuf,
134
135    /// Path to mod directories
136    #[clap(
137        env,
138        short,
139        long,
140        value_parser = path_exists_full,
141        use_value_delimiter = true,
142        value_delimiter = ',',
143        default_values_os_t = current_work_dir(),
144        default_missing_value = working_dir(),
145        required = false
146    )]
147    pub(crate) mod_directories: Vec<PathBuf>,
148
149    /// Game Language
150    #[clap(short, long, default_value = "en_US")]
151    pub(crate) language: String,
152
153    /// Depth to walk folder structure
154    #[clap(env, long, short, default_value = "5")]
155    pub(crate) depth: usize,
156
157    /// Compare against installed weidu log, note this is best effort
158    #[clap(
159        env,
160        short,
161        long,
162        num_args=0..=1,
163        action = clap::ArgAction::SetFalse,
164        default_value_t = true,
165        value_parser = BoolishValueParser::new(),
166    )]
167    pub(crate) skip_installed: bool,
168
169    /// If a warning occurs in the weidu child process exit
170    #[clap(
171        env,
172        short,
173        long,
174        num_args=0..=1,
175        action = clap::ArgAction::SetTrue,
176        default_value_t = false,
177        value_parser = BoolishValueParser::new(),
178    )]
179    pub(crate) abort_on_warnings: bool,
180
181    /// Timeout time per mod in seconds, default is 1 hour
182    #[clap(env, long, short, default_value = "3600")]
183    pub(crate) timeout: usize,
184
185    /// Weidu log setting "--autolog" is default
186    #[clap(
187        env,
188        long,
189        short='u',
190        default_value = "autolog",
191        value_parser = parse_weidu_log_mode,
192        required = false
193    )]
194    pub(crate) weidu_log_mode: String,
195
196    /// Strict Version and Component/SubComponent matching
197    #[clap(
198        env,
199        short = 'x',
200        long,
201        num_args=0..=1,
202        action = clap::ArgAction::SetTrue,
203        default_value_t = false,
204        value_parser = BoolishValueParser::new(),
205    )]
206    pub(crate) strict_matching: bool,
207
208    /// When a missing log is discovered ask the user for the download uri, download the mod and install it
209    #[clap(
210        env,
211        long,
212        num_args=0..=1,
213        action = clap::ArgAction::SetFalse,
214        default_value_t = true,
215        value_parser = BoolishValueParser::new(),
216    )]
217    pub(crate) download: bool,
218
219    /// Force copy mod folder, even if the mod folder was found in the game directory
220    #[clap(
221        env,
222        short,
223        long,
224        num_args=0..=1,
225        action = clap::ArgAction::SetTrue,
226        default_value_t = false,
227        value_parser = BoolishValueParser::new(),
228    )]
229    pub(crate) overwrite: bool,
230
231    /// Strict weidu log checking
232    #[clap(
233        env,
234        short,
235        long,
236        num_args=0..=1,
237        action = clap::ArgAction::SetTrue,
238        default_value_t = false,
239        value_parser = BoolishValueParser::new(),
240    )]
241    pub(crate) check_last_installed: bool,
242
243    /// Tick
244    #[clap(env, short = 'i', long, default_value_t = 500)]
245    pub(crate) tick: u64,
246}
247
248fn parse_weidu_log_mode(arg: &str) -> Result<String, String> {
249    let mut args = arg.split(' ');
250    let mut output = vec![];
251    while let Some(arg) = args.next() {
252        match arg {
253            "log" if path_must_exist(arg).is_ok() => {
254                let path = args.next().unwrap();
255                output.push(format!("--{arg} {path}"));
256            }
257            "autolog" => output.push(format!("--{arg}")),
258            "logapp" => output.push(format!("--{arg}")),
259            "log-extern" => output.push(format!("--{arg}")),
260            _ => return Err(format!("{WEIDU_LOG_MODE_ERROR}, Provided {arg}")),
261        };
262    }
263    Ok(output.join(" "))
264}
265
266fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
267    let path = PathBuf::from(arg);
268    path.try_exists()?;
269    Ok(path)
270}
271
272fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
273    fs::canonicalize(path_must_exist(arg)?)
274}
275
276fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
277    let path = path_must_exist(arg).map_err(|err| err.to_string())?;
278    fs::canonicalize(path).map_err(|err| err.to_string())
279}
280
281fn find_weidu_bin() -> PathBuf {
282    if let Some(paths) = var_os("PATH") {
283        for path in split_paths(&paths) {
284            let full_path = path.join(WEIDU_FILE_NAME);
285            if full_path.is_file() && !full_path.is_dir() {
286                return full_path;
287            }
288        }
289    }
290    PathBuf::new()
291}
292
293fn find_weidu_bin_on_path() -> OsStr {
294    OsStr::from(find_weidu_bin().into_os_string())
295}
296
297fn current_work_dir() -> Vec<PathBuf> {
298    vec![std::env::current_dir().unwrap()]
299}
300
301fn working_dir() -> OsStr {
302    OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
303}
304
305#[cfg(test)]
306mod tests {
307
308    use super::*;
309    use pretty_assertions::assert_eq;
310    use std::error::Error;
311
312    #[test]
313    fn test_parse_weidu_log_mode() -> Result<(), Box<dyn Error>> {
314        let tests = vec![
315            ("autolog", Ok("--autolog".to_string())),
316            ("log /home", Ok("--log /home".to_string())),
317            ("autolog logapp", Ok("--autolog --logapp".to_string())),
318            (
319                "autolog logapp log-extern",
320                Ok("--autolog --logapp --log-extern".to_string()),
321            ),
322            (
323                "log /home logapp log-extern",
324                Ok("--log /home --logapp --log-extern".to_string()),
325            ),
326            (
327                "fish",
328                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
329            ),
330            (
331                "log /home fish",
332                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
333            ),
334        ];
335        for (test, expected) in tests {
336            println!("{test:#?}");
337            let result = parse_weidu_log_mode(test);
338            assert_eq!(
339                result, expected,
340                "Result {result:?} didn't match Expected {expected:?}",
341            );
342        }
343        Ok(())
344    }
345
346    #[test]
347    fn test_bool_flags() -> Result<(), Box<dyn Error>> {
348        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
349        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
350        let fake_log_file = fake_game_dir.clone().join("weidu.log");
351        let fake_mod_dirs = fake_game_dir.clone().join("mods");
352        let tests = vec![
353            ("true", true),
354            ("t", true),
355            ("yes", true),
356            ("y", true),
357            ("1", true),
358            ("false", false),
359            ("f", false),
360            ("no", false),
361            ("n", false),
362            ("0", false),
363        ];
364        for (flag_value, expected_flag_value) in tests {
365            let expected = Args {
366                command: InstallType::Normal(Normal {
367                    log_file: fake_log_file.clone(),
368                    game_directory: fake_game_dir.clone(),
369                    new_game_directory: None,
370                    options: Options {
371                        weidu_binary: fake_weidu_bin.clone(),
372                        mod_directories: vec![fake_mod_dirs.clone()],
373                        language: "en_US".to_string(),
374                        depth: 5,
375                        skip_installed: expected_flag_value,
376                        abort_on_warnings: expected_flag_value,
377                        timeout: 3600,
378                        weidu_log_mode: "--autolog".to_string(),
379                        strict_matching: true,
380                        download: true,
381                        overwrite: false,
382                        check_last_installed: false,
383                        tick: 500,
384                    },
385                }),
386            };
387            let test_arg_string = format!(
388                "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
389                flag_value,
390                flag_value,
391                fake_weidu_bin.to_str().unwrap_or_default(),
392                fake_mod_dirs.to_str().unwrap_or_default(),
393                fake_log_file.to_str().unwrap_or_default(),
394                fake_game_dir.to_str().unwrap_or_default(),
395            );
396            let result = Args::parse_from(test_arg_string.split(' '));
397            assert_eq!(
398                result, expected,
399                "Result {result:?} didn't match Expected {expected:?}",
400            );
401        }
402        Ok(())
403    }
404
405    #[test]
406    fn test_eet_flags() -> Result<(), Box<dyn Error>> {
407        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
408        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
409        let fake_log_file = fake_game_dir.clone().join("weidu.log");
410        let new_dir = PathBuf::new().join("test");
411        let expected_flag_value = true;
412
413        let expected = Args {
414            command: InstallType::Eet(Eet {
415                bg1_game_directory: fake_game_dir.clone(),
416                bg1_log_file: fake_log_file.clone(),
417                bg2_game_directory: fake_game_dir.clone(),
418                bg2_log_file: fake_log_file.clone(),
419                options: Options {
420                    weidu_binary: fake_weidu_bin.clone(),
421                    mod_directories: vec![std::env::current_dir().unwrap()],
422                    language: "en_US".to_string(),
423                    depth: 5,
424                    skip_installed: expected_flag_value,
425                    abort_on_warnings: !expected_flag_value,
426                    timeout: 3600,
427                    weidu_log_mode: "--autolog".to_string(),
428                    strict_matching: !expected_flag_value,
429                    download: expected_flag_value,
430                    overwrite: !expected_flag_value,
431                    check_last_installed: !expected_flag_value,
432                    tick: 500,
433                },
434                generate_directories: false,
435                new_pre_eet_dir: None,
436                new_eet_dir: Some("test".into()),
437            }),
438        };
439        let test_arg_string = format!(
440            "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
441            fake_weidu_bin.to_str().unwrap_or_default(),
442            fake_game_dir.to_str().unwrap_or_default(),
443            fake_log_file.to_str().unwrap_or_default(),
444            fake_game_dir.to_str().unwrap_or_default(),
445            fake_log_file.to_str().unwrap_or_default(),
446            new_dir.to_str().unwrap_or_default(),
447        );
448        let result = Args::parse_from(test_arg_string.split(' '));
449        assert_eq!(
450            result, expected,
451            "Result {result:?} didn't match Expected {expected:?}",
452        );
453
454        Ok(())
455    }
456}