tiger_bin_shared/
auto.rs

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