tiger_bin_shared/
auto.rs

1use std::fs::{read_dir, DirEntry};
2use std::mem::forget;
3use std::path::{Path, PathBuf};
4
5use anyhow::{bail, Context, Result};
6use console::Term;
7#[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
8use tiger_lib::ModFile;
9#[cfg(feature = "vic3")]
10use tiger_lib::ModMetadata;
11use tiger_lib::{emit_reports, set_output_file, Everything};
12
13use crate::gamedir::{find_game_directory_steam, find_paradox_directory};
14use crate::GameConsts;
15
16/// Run the automatic version of the tiger application.
17///
18/// It can search the paradox mod folder, detect mods and list them for user selection. However,
19/// it has **no** command line arguments and hence less customizable compared to the main application.
20pub fn run(game_consts: &GameConsts) -> Result<()> {
21    let &GameConsts { name, name_short, version, app_id, signature_file, paradox_dir } =
22        game_consts;
23
24    // Colors are off by default, but enable ANSI support in case the config file turns colors on again.
25    #[cfg(windows)]
26    let _ = ansiterm::enable_ansi_support().map_err(|_| {
27        eprintln!("Failed to enable ANSI support for Windows10 users. Continuing anyway.")
28    });
29
30    eprintln!("This validator was made for {name} version {version}.");
31    eprintln!("If you are using a newer version of {name}, it may be inaccurate.");
32    eprintln!("!! Currently it's inaccurate anyway because it's in beta state.");
33
34    let game = find_game_directory_steam(app_id).context("Cannot find the game directory.")?;
35    eprintln!("Using {name_short} directory: {}", game.display());
36    let sig = game.clone().join(signature_file);
37    if !sig.is_file() {
38        eprintln!("That does not look like a {name_short} directory.");
39        bail!("Cannot find the game directory.");
40    }
41
42    let pdx = get_paradox_directory(&PathBuf::from(paradox_dir))?;
43    let pdxmod = pdx.join("mod");
44    let pdxlogs = pdx.join("logs");
45
46    let mut entries: Vec<_> =
47        read_dir(pdxmod)?.filter_map(Result::ok).filter(is_local_mod_entry).collect();
48    entries.sort_by_key(DirEntry::file_name);
49
50    if entries.len() == 1 {
51        validate_mod(name_short, &game, &entries[0].path(), &pdxlogs)?;
52    } else if entries.is_empty() {
53        bail!("Did not find any mods to validate.");
54    } else {
55        eprintln!("Found several possible mods to validate:");
56        for (i, entry) in entries.iter().enumerate().take(35) {
57            #[allow(clippy::cast_possible_truncation)] // known to be <= 35
58            let ordinal = (i + 1) as u32;
59            if ordinal <= 9 {
60                eprintln!("{}. {}", ordinal, entry.file_name().to_str().unwrap_or(""));
61            } else {
62                let modkey = char::from_u32(ordinal - 10 + 'A' as u32).unwrap_or('?');
63                eprintln!("{modkey}. {}", entry.file_name().to_str().unwrap_or(""));
64            }
65        }
66        let term = Term::stdout();
67        // This takes me back to the 80s...
68        loop {
69            eprint!("\nChoose one by typing its key: ");
70            let ch = term.read_char();
71            if let Ok(ch) = ch {
72                let modnr = if ('1'..='9').contains(&ch) {
73                    ch as usize - '1' as usize
74                } else if ch.is_ascii_lowercase() {
75                    9 + ch as usize - 'a' as usize
76                } else if ch.is_ascii_uppercase() {
77                    9 + ch as usize - 'A' as usize
78                } else {
79                    continue;
80                };
81                if modnr < entries.len() {
82                    eprintln!();
83                    validate_mod(name_short, &game, &entries[modnr].path(), &pdxlogs)?;
84                    return Ok(());
85                }
86            } else {
87                bail!("Cannot read user input. Giving up.");
88            }
89        }
90    }
91
92    Ok(())
93}
94
95#[allow(unused_mut)]
96fn validate_mod(
97    name_short: &'static str,
98    game: &Path,
99    modpath: &Path,
100    logdir: &Path,
101) -> Result<()> {
102    let mut everything;
103    let mut modpath = modpath;
104
105    #[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
106    let modfile = ModFile::read(modpath)?;
107    #[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
108    let modpath_owned = modfile.modpath();
109    #[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
110    {
111        modpath = &modpath_owned;
112        if !modpath.is_dir() {
113            eprintln!("Looking for mod in {}", modpath.display());
114            bail!("Cannot find mod directory. Please make sure the .mod file is correct.");
115        }
116    }
117
118    eprintln!("Using mod directory: {}", modpath.display());
119    let output_filename =
120        format!("{name_short}-tiger-{}.log", modpath.file_name().unwrap().to_string_lossy());
121    let output_file = &logdir.join(output_filename);
122    set_output_file(output_file)?;
123    eprintln!("Writing error reports to {} ...", output_file.display());
124    eprintln!("This will take a few seconds.");
125
126    #[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
127    {
128        everything = Everything::new(None, Some(game), modpath, modfile.replace_paths())?;
129    }
130    #[cfg(feature = "vic3")]
131    {
132        let metadata = ModMetadata::read(modpath)?;
133        everything = Everything::new(None, Some(game), modpath, metadata.replace_paths())?;
134    }
135
136    // Unfortunately have to disable the colors by default because
137    // on Windows there's no easy way to view a file that contains those escape sequences.
138    // There are workarounds but those defeat the purpose of -auto.
139    // The colors can be enabled again in the config file.
140    everything.load_output_settings(false);
141    everything.load_config_filtering_rules();
142    emit_reports(false);
143
144    everything.load_all();
145    everything.validate_all();
146    everything.check_rivers();
147    emit_reports(false);
148
149    // Properly dropping `everything` takes a noticeable amount of time, and we're exiting anyway.
150    forget(everything);
151
152    Ok(())
153}
154
155fn is_local_mod_entry(entry: &DirEntry) -> bool {
156    #[cfg(any(feature = "ck3", feature = "imperator", feature = "hoi4"))]
157    {
158        let filename = entry.file_name();
159        let name = filename.to_string_lossy();
160        name.ends_with(".mod") && !name.starts_with("pdx_") && !name.starts_with("ugc")
161    }
162    #[cfg(feature = "vic3")]
163    {
164        entry.path().join(".metadata/metadata.json").is_file()
165    }
166}
167
168fn get_paradox_directory(paradox_dir: &Path) -> Result<PathBuf> {
169    if let Some(pdx) = find_paradox_directory(paradox_dir) {
170        Ok(pdx)
171    } else {
172        bail!("Cannot find the Paradox directory.");
173    }
174}