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
303pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
304    let path = PathBuf::from(arg);
305    path.try_exists()?;
306    Ok(path)
307}
308
309fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
310    fs::canonicalize(path_must_exist(arg)?)
311}
312
313fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
314    let path = path_must_exist(arg).map_err(|err| err.to_string())?;
315    fs::canonicalize(path).map_err(|err| err.to_string())
316}
317
318fn find_weidu_bin() -> PathBuf {
319    if let Some(paths) = var_os("PATH") {
320        for path in split_paths(&paths) {
321            let full_path = path.join(WEIDU_FILE_NAME);
322            if full_path.is_file() && !full_path.is_dir() {
323                return full_path;
324            }
325        }
326    }
327    PathBuf::new()
328}
329
330fn find_weidu_bin_on_path() -> OsStr {
331    OsStr::from(find_weidu_bin().into_os_string())
332}
333
334fn current_work_dir() -> Vec<PathBuf> {
335    vec![std::env::current_dir().unwrap()]
336}
337
338fn working_dir() -> OsStr {
339    OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
340}
341
342#[cfg(test)]
343mod tests {
344
345    use super::*;
346    use pretty_assertions::assert_eq;
347    use std::{error::Error, path::PathBuf};
348
349    #[test]
350    fn test_bool_flags() -> Result<(), Box<dyn Error>> {
351        let workspace_root: PathBuf = std::env::current_dir()?;
352        let fake_game_dir: PathBuf = workspace_root
353            .parent()
354            .ok_or("Could not get workspace root")?
355            .join("fixtures");
356        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
357        let fake_log_file = fake_game_dir.clone().join("weidu.log");
358        let fake_mod_dirs = fake_game_dir.clone().join("mods");
359        let tests = vec![
360            ("true", true),
361            ("t", true),
362            ("yes", true),
363            ("y", true),
364            ("1", true),
365            ("false", false),
366            ("f", false),
367            ("no", false),
368            ("n", false),
369            ("0", false),
370        ];
371        for (flag_value, expected_flag_value) in tests {
372            let expected = Args {
373                command: CommandType::Normal(Normal {
374                    log_file: fake_log_file.clone(),
375                    game_directory: fake_game_dir.clone(),
376                    generate_directory: None,
377                    options: Options {
378                        weidu_binary: fake_weidu_bin.clone(),
379                        mod_directories: vec![fake_mod_dirs.clone()],
380                        language: "en_US".to_string(),
381                        depth: 5,
382                        skip_installed: expected_flag_value,
383                        abort_on_warnings: expected_flag_value,
384                        timeout: 3600,
385                        weidu_log_mode: vec![
386                            LogOptions::AutoLog,
387                            LogOptions::LogAppend,
388                            LogOptions::LogExternal,
389                        ],
390                        strict_matching: true,
391                        download: true,
392                        overwrite: false,
393                        check_last_installed: true,
394                        tick: 500,
395                        lookback: 10,
396                        casefold: false,
397                        never_abort: false,
398                    },
399                }),
400            };
401            let test_arg_string = format!(
402                "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
403                flag_value,
404                flag_value,
405                fake_weidu_bin.to_str().unwrap_or_default(),
406                fake_mod_dirs.to_str().unwrap_or_default(),
407                fake_log_file.to_str().unwrap_or_default(),
408                fake_game_dir.to_str().unwrap_or_default(),
409            );
410            let result = Args::parse_from(test_arg_string.split(' '));
411            assert_eq!(
412                result, expected,
413                "Result {result:?} didn't match Expected {expected:?}",
414            );
415        }
416        Ok(())
417    }
418
419    #[test]
420    fn test_eet_flags() -> Result<(), Box<dyn Error>> {
421        let workspace_root: PathBuf = std::env::current_dir()?;
422        let fake_game_dir: PathBuf = workspace_root
423            .parent()
424            .ok_or("Could not get workspace root")?
425            .join("fixtures");
426        let fake_weidu_bin = fake_game_dir.clone().join("weidu");
427        let fake_log_file = fake_game_dir.clone().join("weidu.log");
428        let new_dir = PathBuf::new().join("test");
429        let expected_flag_value = true;
430
431        let expected = Args {
432            command: CommandType::Eet(Eet {
433                bg1_game_directory: fake_game_dir.clone(),
434                bg1_log_file: fake_log_file.clone(),
435                bg2_game_directory: fake_game_dir.clone(),
436                bg2_log_file: fake_log_file.clone(),
437                options: Options {
438                    weidu_binary: fake_weidu_bin.clone(),
439                    mod_directories: vec![std::env::current_dir().unwrap()],
440                    language: "en_US".to_string(),
441                    depth: 5,
442                    skip_installed: expected_flag_value,
443                    abort_on_warnings: !expected_flag_value,
444                    timeout: 3600,
445                    weidu_log_mode: vec![
446                        LogOptions::AutoLog,
447                        LogOptions::LogAppend,
448                        LogOptions::LogExternal,
449                    ],
450                    strict_matching: !expected_flag_value,
451                    download: expected_flag_value,
452                    overwrite: !expected_flag_value,
453                    check_last_installed: expected_flag_value,
454                    tick: 500,
455                    lookback: 10,
456                    casefold: false,
457                    never_abort: false,
458                },
459                new_pre_eet_dir: None,
460                new_eet_dir: Some("test".into()),
461            }),
462        };
463        let test_arg_string = format!(
464            "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
465            fake_weidu_bin.to_str().unwrap_or_default(),
466            fake_game_dir.to_str().unwrap_or_default(),
467            fake_log_file.to_str().unwrap_or_default(),
468            fake_game_dir.to_str().unwrap_or_default(),
469            fake_log_file.to_str().unwrap_or_default(),
470            new_dir.to_str().unwrap_or_default(),
471        );
472        let result = Args::parse_from(test_arg_string.split(' '));
473        assert_eq!(
474            result, expected,
475            "Result {result:?} didn't match Expected {expected:?}",
476        );
477
478        Ok(())
479    }
480}