1use anyhow::{anyhow, Result};
14
15use rpfm_extensions::diagnostics::{DiagnosticLevel as CheckLevel, DiagnosticReport, Diagnostics, DiagnosticType};
16use rpfm_extensions::lua::check::{check_script, LuaDefinitions};
17use rpfm_extensions::lua::harness::{run_tests, LuaScripts, LuaTestOptions, LuaTestReport};
18
19use rpfm_ipc::api::{ApiError, Done};
20use rpfm_ipc::api::diagnostics::{DEFAULT_DIAGNOSTICS_LIMIT, DiagnosticLevel, DiagnosticList, DiagnosticResult, DiagnosticsSummary, IgnoreDiagnostics, ListDiagnostics, RunDiagnostics};
21
22use rpfm_lib::files::{Container, ContainerPath};
23
24use rpfm_telemetry::info;
25
26use rpfm_ipc::settings::Settings;
27
28use super::{SessionState, cached_lua_api};
29
30pub(super) struct DiagnosticsResults {
32
33 diagnostics: Diagnostics,
35
36 results: Vec<DiagnosticResult>,
38}
39
40pub type LuaHover = (u64, u64, u64, u64, String);
42
43impl SessionState {
44
45 pub fn check_diagnostics(&mut self, mut diagnostics: Diagnostics, paths_to_check: &[ContainerPath], check_ak_only_refs: bool, settings: &Settings, on_progress: &(dyn Fn(usize, usize) -> bool + Sync)) -> Option<Diagnostics> {
61 if let Some(ref schema) = self.schema {
62 let game_path = settings.path_buf(self.game.key());
63 let lua_api = cached_lua_api(&mut self.lua_api_cache, &self.game, settings, &self.dependencies);
64 if !diagnostics.check(&mut self.packs, &mut self.dependencies, schema, &self.game, &game_path, paths_to_check, check_ak_only_refs, lua_api, on_progress) {
65 info!("Checking diagnostics: stopped.");
66 return None;
67 }
68 }
69
70 info!("Checking diagnostics: done.");
71 Some(diagnostics)
72 }
73
74 pub fn run_diagnostics(&mut self, request: &RunDiagnostics, settings: &Settings, on_progress: &(dyn Fn(usize, usize) -> bool + Sync)) -> Option<DiagnosticsSummary> {
86 let paths = request.paths.iter()
87 .map(|path| if self.packs.values().any(|pack| pack.has_file(path)) {
88 ContainerPath::File(path.to_owned())
89 } else {
90 ContainerPath::Folder(path.trim_end_matches('/').to_owned())
91 })
92 .collect::<Vec<_>>();
93
94 let mut diagnostics = match self.diagnostics {
96 Some(ref previous) if !paths.is_empty() => previous.diagnostics.clone(),
97 _ => Diagnostics::default(),
98 };
99
100 diagnostics.diagnostics_ignored_mut().clone_from(&request.ignored_types);
101 let diagnostics = self.check_diagnostics(diagnostics, &paths, request.check_assembly_kit_only_references, settings, on_progress)?;
102 let results = flatten_diagnostics(&diagnostics);
103 let summary = summarize(&results);
104
105 self.diagnostics = Some(DiagnosticsResults { diagnostics, results });
106 Some(summary)
107 }
108
109 pub fn list_diagnostics(&self, request: &ListDiagnostics) -> Result<DiagnosticList> {
115 let results = &self.diagnostics.as_ref().ok_or(ApiError::DiagnosticsNotRun)?.results;
116 let matching = results.iter()
117 .filter(|result| request.levels.as_ref().is_none_or(|levels| levels.contains(&result.level)))
118 .filter(|result| request.report_types.as_ref().is_none_or(|types| types.contains(&result.report_type)))
119 .filter(|result| request.pack.as_ref().is_none_or(|pack| *pack == result.pack))
120 .filter(|result| result.path.starts_with(&request.path_prefix));
121
122 let mut page = vec![];
123 let mut total = 0;
124 let limit = request.limit.unwrap_or(DEFAULT_DIAGNOSTICS_LIMIT);
125 for result in matching {
126 if total >= request.offset && page.len() < limit {
127 page.push(result.clone());
128 }
129
130 total += 1;
131 }
132
133 Ok(DiagnosticList { results: page, total })
134 }
135
136 pub fn diagnostics_report(&self) -> Result<Diagnostics> {
142 Ok(self.diagnostics.as_ref().ok_or(ApiError::DiagnosticsNotRun)?.diagnostics.clone())
143 }
144
145 pub fn ignore_diagnostics(&mut self, request: &IgnoreDiagnostics) -> Result<Done> {
151 let values = std::iter::once(&request.path).chain(&request.columns).chain(&request.report_types);
152 if let Some(value) = values.clone().find(|value| value.contains([';', '\n'])) {
153 return Err(ApiError::InvalidParams(format!("Values can't contain ';' or line breaks: {value}")).into());
154 }
155
156 if let Some(value) = request.columns.iter().chain(&request.report_types).find(|value| value.contains(',')) {
157 return Err(ApiError::InvalidParams(format!("Columns and report types can't contain ',': {value}")).into());
158 }
159
160 let line = format!("\n{};{};{}", request.path, request.columns.join(","), request.report_types.join(","));
161 self.add_pack_ignored_diagnostics_line(&request.pack, line)?;
162 Ok(Done {})
163 }
164
165 pub fn lua_hovers(&mut self, source: &str, settings: &Settings) -> Vec<LuaHover> {
167 let Some(lua_api) = cached_lua_api(&mut self.lua_api_cache, &self.game, settings, &self.dependencies) else { return vec![] };
168
169 check_script(source, Some(lua_api), &LuaDefinitions::default()).hovers().iter()
170 .filter_map(|hover| {
171 let ((start_line, start_column), (end_line, end_column)) = *hover.range();
172 lua_api.hover_html(hover.target()).map(|html| (start_line, start_column, end_line, end_column, html))
173 })
174 .collect()
175 }
176
177 pub fn lua_run_tests(&mut self, test_source: &str, campaign: Option<String>, settings: &Settings) -> Result<LuaTestReport> {
193 let lua_api = cached_lua_api(&mut self.lua_api_cache, &self.game, settings, &self.dependencies)
194 .ok_or_else(|| anyhow!("The Lua API of the game is not available. Lua tests need the game's Assembly Kit to be installed."))?;
195
196 let scripts = LuaScripts::from_game_and_packs(&self.dependencies, self.packs.values());
197 let key_values = |table_name| self.dependencies.db_key_values(Some(&self.packs), table_name)
198 .map(|(_, keys)| keys.into_iter().collect::<Vec<_>>())
199 .unwrap_or_default();
200
201 let mut options = LuaTestOptions::default();
202 options.set_campaign(campaign);
203 options.set_faction_keys(key_values("factions_tables"));
204 options.set_region_keys(key_values("regions_tables"));
205
206 Ok(run_tests(lua_api, &scripts, test_source, &options)?)
207 }
208}
209
210fn flatten_diagnostics(diagnostics: &Diagnostics) -> Vec<DiagnosticResult> {
212 diagnostics.results().iter()
213 .flat_map(|diagnostic| match diagnostic {
214 DiagnosticType::DB(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "db", diagnostic.results(), |report| report.report_type().to_string(), |report| (cells(report.cells_affected()), report.column_names().clone())),
215 DiagnosticType::Loc(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "loc", diagnostic.results(), |report| report.report_type().to_string(), |report| (cells(report.cells_affected()), report.column_names().clone())),
216 DiagnosticType::AnimFragmentBattle(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "anim_fragment_battle", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
217 DiagnosticType::Config(diagnostic) => results_of("", "", "config", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
218 DiagnosticType::Dependency(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "dependency", diagnostic.results(), |report| report.report_type().to_string(), |report| (cells(report.cells_affected()), vec![])),
219 DiagnosticType::GroupFormations(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "group_formations", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
220 DiagnosticType::Pack(diagnostic) => results_of(diagnostic.pack(), "", "pack", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
221 DiagnosticType::PortraitSettings(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "portrait_settings", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
222 DiagnosticType::Text(diagnostic) => results_of(diagnostic.pack(), diagnostic.path(), "text", diagnostic.results(), |report| report.report_type().to_string(), no_cells),
223 })
224 .collect()
225}
226
227fn results_of<R: DiagnosticReport>(pack: &str, path: &str, checked: &str, reports: &[R], report_type: impl Fn(&R) -> String, cells: impl Fn(&R) -> (Vec<[i32; 2]>, Vec<String>)) -> Vec<DiagnosticResult> {
238 reports.iter()
239 .map(|report| {
240 let (cells, columns) = cells(report);
241 DiagnosticResult {
242 pack: pack.to_owned(),
243 path: path.to_owned(),
244 checked: checked.to_owned(),
245 report_type: report_type(report),
246 level: match report.level() {
247 CheckLevel::Info => DiagnosticLevel::Info,
248 CheckLevel::Warning => DiagnosticLevel::Warning,
249 CheckLevel::Error => DiagnosticLevel::Error,
250 },
251 message: report.message(),
252 cells,
253 columns,
254 }
255 })
256 .collect()
257}
258
259fn no_cells<R>(_: &R) -> (Vec<[i32; 2]>, Vec<String>) {
261 (vec![], vec![])
262}
263
264fn cells(cells: &[(i32, i32)]) -> Vec<[i32; 2]> {
266 cells.iter().map(|(row, column)| [*row, *column]).collect()
267}
268
269fn summarize(results: &[DiagnosticResult]) -> DiagnosticsSummary {
271 let mut summary = DiagnosticsSummary { total: results.len(), ..DiagnosticsSummary::default() };
272 for result in results {
273 *summary.by_level.entry(result.level).or_default() += 1;
274 *summary.by_type.entry(result.report_type.clone()).or_default() += 1;
275 }
276
277 summary
278}
279
280#[cfg(test)]
285#[path = "diagnostics_test.rs"]
286mod diagnostics_test;