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 options
312  #[clap(flatten)]
313  pub batch: BatchOptions,
314
315  /// Ocaml run parameters
316  #[clap(env = "OCAMLRUNPARAM", long, default_value = "s=16M,o=500,O=1000000")]
317  pub ocamlrunparam: String,
318}
319
320#[derive(Parser, Debug, PartialEq, Clone, Default)]
321pub struct BatchOptions {
322  /// Batch mode
323  #[clap(
324        env,
325        long,
326        num_args=0..=1,
327        action = clap::ArgAction::SetTrue,
328        default_value_t = false,
329        conflicts_with = "check_last_installed",
330        value_parser = BoolishValueParser::new(),
331   )]
332  pub batch_mode: bool,
333
334  /// Batch size
335  #[clap(env, long, default_value_t = 5, required = false)]
336  pub batch_size: usize,
337
338  /// Batch skip
339  #[clap(
340    env,
341    long,
342    use_value_delimiter = true,
343    value_delimiter = ',',
344    default_value = "setup-stratagems.tp2",
345    ignore_case = true,
346    required = false
347  )]
348  pub batch_skip: Vec<String>,
349}
350
351pub fn path_must_exist(arg: &str) -> Result<PathBuf, std::io::Error> {
352  let path = PathBuf::from(arg);
353  path.try_exists()?;
354  Ok(path)
355}
356
357fn path_exists_full(arg: &str) -> Result<PathBuf, std::io::Error> {
358  fs::canonicalize(path_must_exist(arg)?)
359}
360
361fn parse_absolute_path(arg: &str) -> Result<PathBuf, String> {
362  let path = path_must_exist(arg).map_err(|err| err.to_string())?;
363  fs::canonicalize(path).map_err(|err| err.to_string())
364}
365
366fn find_weidu_bin() -> PathBuf {
367  if let Some(paths) = var_os("PATH") {
368    for path in split_paths(&paths) {
369      let full_path = path.join(WEIDU_FILE_NAME);
370      if full_path.is_file() && !full_path.is_dir() {
371        return full_path;
372      }
373    }
374  }
375  PathBuf::new()
376}
377
378fn find_weidu_bin_on_path() -> OsStr {
379  OsStr::from(find_weidu_bin().into_os_string())
380}
381
382fn current_work_dir() -> Vec<PathBuf> {
383  vec![std::env::current_dir().unwrap_or_default()]
384}
385
386fn working_dir() -> OsStr {
387  OsStr::from(current_work_dir().first().unwrap().clone().into_os_string())
388}
389
390#[cfg(test)]
391mod tests {
392
393  use super::*;
394  use pretty_assertions::assert_eq;
395  use std::{error::Error, path::PathBuf};
396
397  #[test]
398  fn test_bool_flags() -> Result<(), Box<dyn Error>> {
399    let workspace_root: PathBuf = std::env::current_dir()?;
400    let fake_game_dir: PathBuf = workspace_root.join("fixtures");
401    let fake_weidu_bin = fake_game_dir.clone().join("weidu");
402    let fake_log_file = fake_game_dir.clone().join("weidu.log");
403    let fake_mod_dirs = fake_game_dir.clone().join("mods");
404    let tests = vec![
405      ("true", true),
406      ("t", true),
407      ("yes", true),
408      ("y", true),
409      ("1", true),
410      ("false", false),
411      ("f", false),
412      ("no", false),
413      ("n", false),
414      ("0", false),
415    ];
416    for (flag_value, expected_flag_value) in tests {
417      let expected = Args {
418        command: CommandType::Normal(Normal {
419          log_file: fake_log_file.clone(),
420          game_directory: fake_game_dir.clone(),
421          generate_directory: None,
422          options: Options {
423            weidu_binary: fake_weidu_bin.clone(),
424            mod_directories: vec![fake_mod_dirs.clone()],
425            language: "en_US".to_string(),
426            depth: 5,
427            skip_installed: expected_flag_value,
428            abort_on_warnings: expected_flag_value,
429            timeout: 3600,
430            weidu_log_mode: vec![
431              LogOptions::AutoLog,
432              LogOptions::LogAppend,
433              LogOptions::LogExternal,
434            ],
435            strict_matching: true,
436            download: true,
437            overwrite: false,
438            check_last_installed: true,
439            tick: 500,
440            lookback: 10,
441            casefold: false,
442            never_abort: false,
443            generic_weidu_args: vec![],
444            batch: BatchOptions {
445              batch_mode: false,
446              batch_size: 5,
447              batch_skip: vec!["setup-stratagems.tp2".into()],
448            },
449            ocamlrunparam: "s=16M,o=500,O=1000000".into(),
450          },
451        }),
452      };
453      let test_arg_string = format!(
454        "mod_installer -n -x -a {} -s {} -w {} -m {} -f {} -g {}",
455        flag_value,
456        flag_value,
457        fake_weidu_bin.to_str().unwrap_or_default(),
458        fake_mod_dirs.to_str().unwrap_or_default(),
459        fake_log_file.to_str().unwrap_or_default(),
460        fake_game_dir.to_str().unwrap_or_default(),
461      );
462      let result = Args::parse_from(test_arg_string.split(' '));
463      assert_eq!(
464        result, expected,
465        "Result {result:?} didn't match Expected {expected:?}",
466      );
467    }
468    Ok(())
469  }
470
471  #[test]
472  fn test_eet_flags() -> Result<(), Box<dyn Error>> {
473    let workspace_root: PathBuf = std::env::current_dir()?;
474    let fake_game_dir: PathBuf = workspace_root.join("fixtures");
475    let fake_weidu_bin = fake_game_dir.clone().join("weidu");
476    let fake_log_file = fake_game_dir.clone().join("weidu.log");
477    let new_dir = PathBuf::new().join("test");
478    let expected_flag_value = true;
479
480    let expected = Args {
481      command: CommandType::Eet(Eet {
482        bg1_game_directory: fake_game_dir.clone(),
483        bg1_log_file: fake_log_file.clone(),
484        bg2_game_directory: fake_game_dir.clone(),
485        bg2_log_file: fake_log_file.clone(),
486        options: Options {
487          weidu_binary: fake_weidu_bin.clone(),
488          mod_directories: vec![std::env::current_dir().unwrap()],
489          language: "en_US".to_string(),
490          depth: 5,
491          skip_installed: expected_flag_value,
492          abort_on_warnings: !expected_flag_value,
493          timeout: 3600,
494          weidu_log_mode: vec![
495            LogOptions::AutoLog,
496            LogOptions::LogAppend,
497            LogOptions::LogExternal,
498          ],
499          strict_matching: !expected_flag_value,
500          download: expected_flag_value,
501          overwrite: !expected_flag_value,
502          check_last_installed: expected_flag_value,
503          tick: 500,
504          lookback: 10,
505          casefold: false,
506          never_abort: false,
507          generic_weidu_args: vec![],
508          batch: BatchOptions {
509            batch_mode: false,
510            batch_size: 5,
511            batch_skip: vec!["setup-stratagems.tp2".into()],
512          },
513          ocamlrunparam: "s=16M,o=500,O=1000000".into(),
514        },
515        new_pre_eet_dir: None,
516        new_eet_dir: Some("test".into()),
517      }),
518    };
519    let test_arg_string = format!(
520      "mod_installer eet -w {} -1 {} -y {} -2 {} -z {} -n {}",
521      fake_weidu_bin.to_str().unwrap_or_default(),
522      fake_game_dir.to_str().unwrap_or_default(),
523      fake_log_file.to_str().unwrap_or_default(),
524      fake_game_dir.to_str().unwrap_or_default(),
525      fake_log_file.to_str().unwrap_or_default(),
526      new_dir.to_str().unwrap_or_default(),
527    );
528    let result = Args::parse_from(test_arg_string.split(' '));
529    assert_eq!(
530      result, expected,
531      "Result {result:?} didn't match Expected {expected:?}",
532    );
533
534    Ok(())
535  }
536  #[test]
537  fn test_log_flags() -> Result<(), Box<dyn Error>> {
538    let workspace_root: PathBuf = std::env::current_dir()?;
539    let fake_game_dir: PathBuf = workspace_root.join("fixtures");
540    let fake_weidu_bin = fake_game_dir.clone().join("weidu");
541    let fake_log_file = fake_game_dir.clone().join("weidu.log");
542    let fake_mod_dirs = fake_game_dir.clone().join("mods");
543    let log_file = tempfile::NamedTempFile::new()?;
544    let log_file_path = log_file.path().as_os_str().to_str().unwrap_or_default();
545    let expected = Args {
546      command: CommandType::Normal(Normal {
547        log_file: fake_log_file.clone(),
548        game_directory: fake_game_dir.clone(),
549        generate_directory: None,
550        options: Options {
551          weidu_binary: fake_weidu_bin.clone(),
552          mod_directories: vec![fake_mod_dirs.clone()],
553          language: "en_US".to_string(),
554          depth: 5,
555          skip_installed: true,
556          abort_on_warnings: false,
557          timeout: 3600,
558          weidu_log_mode: vec![
559            LogOptions::AutoLog,
560            LogOptions::LogAppend,
561            LogOptions::LogExternal,
562            #[cfg(not(target_os = "windows"))]
563            LogOptions::Log(log_file.path().canonicalize()?),
564            #[cfg(windows)]
565            LogOptions::Log(log_file.path()),
566          ],
567          strict_matching: false,
568          download: true,
569          overwrite: false,
570          check_last_installed: true,
571          tick: 500,
572          lookback: 10,
573          casefold: false,
574          never_abort: false,
575          generic_weidu_args: vec![],
576          batch: BatchOptions {
577            batch_mode: false,
578            batch_size: 5,
579            batch_skip: vec!["setup-stratagems.tp2".into()],
580          },
581          ocamlrunparam: "s=16M,o=500,O=1000000".into(),
582        },
583      }),
584    };
585    let log_arg = format!("autolog,logapp,log-extern,log {log_file_path}");
586    let test_arg_string = vec![
587      "mod_installer",
588      "-n",
589      "-w",
590      fake_weidu_bin.to_str().unwrap_or_default(),
591      "-m",
592      fake_mod_dirs.to_str().unwrap_or_default(),
593      "--log-file",
594      fake_log_file.to_str().unwrap_or_default(),
595      "--game-directory",
596      fake_game_dir.to_str().unwrap_or_default(),
597      "--weidu-log-mode",
598      &log_arg,
599    ];
600    let result = Args::parse_from(test_arg_string);
601    assert_eq!(
602      result, expected,
603      "Result {result:?} didn't match Expected {expected:?}",
604    );
605    Ok(())
606  }
607}