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
16pub fn run(game_consts: &GameConsts) -> Result<()> {
21 let &GameConsts { name, name_short, version, app_id, signature_file, paradox_dir } =
22 game_consts;
23
24 #[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)] 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 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 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 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}