Skip to main content

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