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) generate_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    /// Generates a new pre-eet directory.
99    #[clap(env, short = 'p', long, value_parser = path_must_exist)]
100    pub(crate) new_pre_eet_dir: Option<PathBuf>,
101
102    /// Generates a new eet directory.
103    #[clap(env, short = 'n', long, value_parser = path_must_exist)]
104    pub(crate) new_eet_dir: Option<PathBuf>,
105
106    /// CommonOptions
107    #[clap(flatten)]
108    pub(crate) options: Options,
109}
110
111#[derive(Parser, Debug, PartialEq, Clone, Default)]
112pub(crate) struct Options {
113    /// Absolute Path to weidu binary
114    #[clap(
115        env,
116        short,
117        long,
118        value_parser = parse_absolute_path,
119        default_value_os_t = find_weidu_bin(),
120        default_missing_value = find_weidu_bin_on_path(),
121        required = false
122    )]
123    pub(crate) weidu_binary: PathBuf,
124
125    /// Path to mod directories
126    #[clap(
127        env,
128        short,
129        long,
130        value_parser = path_exists_full,
131        use_value_delimiter = true,
132        value_delimiter = ',',
133        default_values_os_t = current_work_dir(),
134        default_missing_value = working_dir(),
135        required = false
136    )]
137    pub(crate) mod_directories: Vec<PathBuf>,
138
139    /// Game Language
140    #[clap(short, long, default_value = "en_US")]
141    pub(crate) language: String,
142
143    /// Depth to walk folder structure
144    #[clap(env, long, short, default_value = "5")]
145    pub(crate) depth: usize,
146
147    /// Compare against installed weidu log, note this is best effort
148    #[clap(
149        env,
150        short,
151        long,
152        num_args=0..=1,
153        action = clap::ArgAction::SetFalse,
154        default_value_t = true,
155        value_parser = BoolishValueParser::new(),
156    )]
157    pub(crate) skip_installed: bool,
158
159    /// If a warning occurs in the weidu child process exit
160    #[clap(
161        env,
162        short,
163        long,
164        num_args=0..=1,
165        action = clap::ArgAction::SetTrue,
166        default_value_t = false,
167        value_parser = BoolishValueParser::new(),
168    )]
169    pub(crate) abort_on_warnings: bool,
170
171    /// Timeout time per mod in seconds, default is 1 hour
172    #[clap(env, long, short, default_value = "3600")]
173    pub(crate) timeout: usize,
174
175    /// Weidu log setting "--autolog" is default
176    #[clap(
177        env,
178        long,
179        short='u',
180        default_value = "autolog",
181        value_parser = parse_weidu_log_mode,
182        required = false
183    )]
184    pub(crate) weidu_log_mode: String,
185
186    /// Strict Version and Component/SubComponent matching
187    #[clap(
188        env,
189        short = 'x',
190        long,
191        num_args=0..=1,
192        action = clap::ArgAction::SetTrue,
193        default_value_t = false,
194        value_parser = BoolishValueParser::new(),
195    )]
196    pub(crate) strict_matching: bool,
197
198    /// When a missing log is discovered ask the user for the download uri, download the mod and install it
199    #[clap(
200        env,
201        long,
202        num_args=0..=1,
203        action = clap::ArgAction::SetFalse,
204        default_value_t = true,
205        value_parser = BoolishValueParser::new(),
206    )]
207    pub(crate) download: bool,
208
209    /// Force copy mod folder, even if the mod folder was found in the game directory
210    #[clap(
211        env,
212        short,
213        long,
214        num_args=0..=1,
215        action = clap::ArgAction::SetTrue,
216        default_value_t = false,
217        value_parser = BoolishValueParser::new(),
218    )]
219    pub(crate) overwrite: bool,
220
221    /// Strict weidu log checking
222    #[clap(
223        env,
224        short,
225        long,
226        num_args=0..=1,
227        action = clap::ArgAction::SetTrue,
228        default_value_t = false,
229        value_parser = BoolishValueParser::new(),
230    )]
231    pub(crate) check_last_installed: bool,
232
233    /// Tick
234    #[clap(env, short = 'i', long, default_value_t = 500)]
235    pub(crate) tick: u64,
236}
237
238fn parse_weidu_log_mode(arg: &str) -> Result<String, String> {
239    let mut args = arg.split(' ');
240    let mut output = vec![];
241    while let Some(arg) = args.next() {
242        match arg {
243            "log" if path_must_exist(arg).is_ok() => {
244                let path = args.next().unwrap();
245                output.push(format!("--{arg} {path}"));
246            }
247            "autolog" => output.push(format!("--{arg}")),
248            "logapp" => output.push(format!("--{arg}")),
249            "log-extern" => output.push(format!("--{arg}")),
250            _ => return Err(format!("{WEIDU_LOG_MODE_ERROR}, Provided {arg}")),
251        };
252    }
253    Ok(output.join(" "))
254}
255
256fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
257    let path = PathBuf::from(arg);
258    path.try_exists()?;
259    Ok(path)
260}
261
262fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
263    fs::canonicalize(path_must_exist(arg)?)
264}
265
266fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
267    let path = path_must_exist(arg).map_err(|err| err.to_string())?;
268    fs::canonicalize(path).map_err(|err| err.to_string())
269}
270
271fn find_weidu_bin() -> PathBuf {
272    if let Some(paths) = var_os("PATH") {
273        for path in split_paths(&paths) {
274            let full_path = path.join(WEIDU_FILE_NAME);
275            if full_path.is_file() && !full_path.is_dir() {
276                return full_path;
277            }
278        }
279    }
280    PathBuf::new()
281}
282
283fn find_weidu_bin_on_path() -> OsStr {
284    OsStr::from(find_weidu_bin().into_os_string())
285}
286
287fn current_work_dir() -> Vec<PathBuf> {
288    vec![std::env::current_dir().unwrap()]
289}
290
291fn working_dir() -> OsStr {
292    OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
293}
294
295#[cfg(test)]
296mod tests {
297
298    use super::*;
299    use pretty_assertions::assert_eq;
300    use std::error::Error;
301
302    #[test]
303    fn test_parse_weidu_log_mode() -> Result<(), Box<dyn Error>> {
304        let tests = vec![
305            ("autolog", Ok("--autolog".to_string())),
306            ("log /home", Ok("--log /home".to_string())),
307            ("autolog logapp", Ok("--autolog --logapp".to_string())),
308            (
309                "autolog logapp log-extern",
310                Ok("--autolog --logapp --log-extern".to_string()),
311            ),
312            (
313                "log /home logapp log-extern",
314                Ok("--log /home --logapp --log-extern".to_string()),
315            ),
316            (
317                "fish",
318                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
319            ),
320            (
321                "log /home fish",
322                Err(format!("{}, Provided {}", WEIDU_LOG_MODE_ERROR, "fish")),
323            ),
324        ];
325        for (test, expected) in tests {
326            println!("{test:#?}");
327            let result = parse_weidu_log_mode(test);
328            assert_eq!(
329                result, expected,
330                "Result {result:?} didn't match Expected {expected:?}",
331            );
332        }
333        Ok(())
334    }
335
336    #[test]
337    fn test_bool_flags() -> Result<(), Box<dyn Error>> {
338        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
339        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
340        let fake_log_file = fake_game_dir.clone().join("weidu.log");
341        let fake_mod_dirs = fake_game_dir.clone().join("mods");
342        let tests = vec![
343            ("true", true),
344            ("t", true),
345            ("yes", true),
346            ("y", true),
347            ("1", true),
348            ("false", false),
349            ("f", false),
350            ("no", false),
351            ("n", false),
352            ("0", false),
353        ];
354        for (flag_value, expected_flag_value) in tests {
355            let expected = Args {
356                command: InstallType::Normal(Normal {
357                    log_file: fake_log_file.clone(),
358                    game_directory: fake_game_dir.clone(),
359                    generate_directory: None,
360                    options: Options {
361                        weidu_binary: fake_weidu_bin.clone(),
362                        mod_directories: vec![fake_mod_dirs.clone()],
363                        language: "en_US".to_string(),
364                        depth: 5,
365                        skip_installed: expected_flag_value,
366                        abort_on_warnings: expected_flag_value,
367                        timeout: 3600,
368                        weidu_log_mode: "--autolog".to_string(),
369                        strict_matching: true,
370                        download: true,
371                        overwrite: false,
372                        check_last_installed: false,
373                        tick: 500,
374                    },
375                }),
376            };
377            let test_arg_string = format!(
378                "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
379                flag_value,
380                flag_value,
381                fake_weidu_bin.to_str().unwrap_or_default(),
382                fake_mod_dirs.to_str().unwrap_or_default(),
383                fake_log_file.to_str().unwrap_or_default(),
384                fake_game_dir.to_str().unwrap_or_default(),
385            );
386            let result = Args::parse_from(test_arg_string.split(' '));
387            assert_eq!(
388                result, expected,
389                "Result {result:?} didn't match Expected {expected:?}",
390            );
391        }
392        Ok(())
393    }
394
395    #[test]
396    fn test_eet_flags() -> Result<(), Box<dyn Error>> {
397        let fake_game_dir = std::env::current_dir().unwrap().join("fixtures");
398        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
399        let fake_log_file = fake_game_dir.clone().join("weidu.log");
400        let new_dir = PathBuf::new().join("test");
401        let expected_flag_value = true;
402
403        let expected = Args {
404            command: InstallType::Eet(Eet {
405                bg1_game_directory: fake_game_dir.clone(),
406                bg1_log_file: fake_log_file.clone(),
407                bg2_game_directory: fake_game_dir.clone(),
408                bg2_log_file: fake_log_file.clone(),
409                options: Options {
410                    weidu_binary: fake_weidu_bin.clone(),
411                    mod_directories: vec![std::env::current_dir().unwrap()],
412                    language: "en_US".to_string(),
413                    depth: 5,
414                    skip_installed: expected_flag_value,
415                    abort_on_warnings: !expected_flag_value,
416                    timeout: 3600,
417                    weidu_log_mode: "--autolog".to_string(),
418                    strict_matching: !expected_flag_value,
419                    download: expected_flag_value,
420                    overwrite: !expected_flag_value,
421                    check_last_installed: !expected_flag_value,
422                    tick: 500,
423                },
424                new_pre_eet_dir: None,
425                new_eet_dir: Some("test".into()),
426            }),
427        };
428        let test_arg_string = format!(
429            "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
430            fake_weidu_bin.to_str().unwrap_or_default(),
431            fake_game_dir.to_str().unwrap_or_default(),
432            fake_log_file.to_str().unwrap_or_default(),
433            fake_game_dir.to_str().unwrap_or_default(),
434            fake_log_file.to_str().unwrap_or_default(),
435            new_dir.to_str().unwrap_or_default(),
436        );
437        let result = Args::parse_from(test_arg_string.split(' '));
438        assert_eq!(
439            result, expected,
440            "Result {result:?} didn't match Expected {expected:?}",
441        );
442
443        Ok(())
444    }
445}