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