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