rpfm_extensions/diagnostics/mod.rs
1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Pack validation and diagnostic checking system.
12//!
13//! This module provides comprehensive validation for Total War mod packs, detecting
14//! common errors, potential issues, and best practice violations. Diagnostics help
15//! modders identify problems before they cause crashes or unexpected behavior in-game.
16//!
17//! # Diagnostic Types
18//!
19//! The system checks multiple aspects of a pack:
20//!
21//! - **Table Diagnostics** ([`table`]): DB and Loc table validation
22//! - Invalid foreign key references
23//! - Empty required fields (keys, values)
24//! - Duplicate rows
25//! - Orphaned localisation entries
26//!
27//! - **Pack Diagnostics** ([`pack`]): Pack-level checks
28//! - Files conflicting with vanilla
29//! - Missing declared dependencies
30//!
31//! - **Dependency Diagnostics** ([`dependency`]): Cross-pack validation
32//! - References to non-existent files
33//! - Circular dependencies
34//!
35//! - **Portrait Settings Diagnostics** ([`portrait_settings`]): Unit portrait validation
36//! - Invalid art set references
37//! - Missing variant definitions
38//!
39//! - **Group Formations Diagnostics** ([`group_formations`]): Formation block validation
40//! - Missing or looping block references
41//! - Formations without an anchoring block
42//!
43//! - **Animation Fragment Diagnostics** ([`anim_fragment_battle`]): Animation validation
44//! - Invalid animation references
45//! - Malformed fragment data
46//!
47//! - **Text Diagnostics** ([`text`]): Script validation
48//!
49//! - **Config Diagnostics** ([`config`]): Configuration file validation
50//!
51//! # Diagnostic Levels
52//!
53//! Each diagnostic has an associated severity level:
54//!
55//! - **Error**: Critical issues that will likely cause crashes or major problems
56//! - **Warning**: Issues that may cause problems or indicate mistakes
57//! - **Info**: Suggestions and informational notes
58//!
59//! # Cell Position Encoding
60//!
61//! For table diagnostics, the affected cells are encoded as (row, column) pairs:
62//!
63//! - `(-1, -1)`: Affects the entire table
64//! - `(row, -1)`: Affects all columns in a single row
65//! - `(-1, column)`: Affects all rows in a single column
66//! - `(row, column)`: Affects a specific cell
67//!
68//! # Filtering
69//!
70//! Diagnostics can be filtered by:
71//!
72//! - Ignored folders (skip entire directory trees)
73//! - Ignored files (skip specific files)
74//! - Ignored fields (skip specific table columns)
75//! - Ignored diagnostic types
76//!
77//! # Usage Example
78//!
79//! ```ignore
80//! use rpfm_extensions::diagnostics::Diagnostics;
81//!
82//! let mut diagnostics = Diagnostics::default();
83//! diagnostics.check(
84//! &mut pack,
85//! &mut dependencies,
86//! &schema,
87//! &game_info,
88//! game_path,
89//! &[], // Check all paths
90//! false, // Don't check AK-only references
91//! None, // No Lua API, so Lua scripts only get their syntax checked
92//! &|_, _| true, // Ignore the progress, and never stop early
93//! );
94//!
95//! for result in diagnostics.results() {
96//! println!("{}: {}", result.path(), result.message());
97//! }
98//! ```
99
100use getset::{Getters, MutGetters};
101use rayon::prelude::*;
102use serde_derive::{Serialize, Deserialize};
103
104use std::borrow::Cow;
105use std::collections::{BTreeMap, HashMap, HashSet};
106use std::cmp::Ordering;
107use std::{fmt, fmt::Display};
108use std::path::Path;
109use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
110
111use rpfm_lib::error::Result;
112use rpfm_lib::files::{ContainerPath, Container, DecodeableExtraData, FileType, pack::{DiagnosticIgnoreEntry, Pack}, RFile, RFileDecoded};
113use rpfm_lib::games::{GameInfo, VanillaDBTableNameLogic};
114use rpfm_lib::schema::{FieldType, Schema};
115use rpfm_lib::utils::path_to_absolute_string;
116
117use crate::dependencies::Dependencies;
118use crate::lua::LuaApi;
119use crate::lua::check::LuaDefinitions;
120
121use self::anim_fragment_battle::*;
122use self::config::*;
123use self::dependency::*;
124use self::group_formations::*;
125use self::pack::*;
126use self::portrait_settings::*;
127use self::table::*;
128use self::text::TextDiagnostic;
129
130pub mod anim_fragment_battle;
131pub mod config;
132pub mod dependency;
133pub mod group_formations;
134pub mod pack;
135pub mod portrait_settings;
136pub mod table;
137pub mod text;
138
139#[cfg(test)] mod tests;
140
141//-------------------------------------------------------------------------------//
142// Trait definitions
143//-------------------------------------------------------------------------------//
144
145/// Trait for types that can report diagnostic information.
146///
147/// All diagnostic result types implement this trait to provide a consistent
148/// interface for accessing the diagnostic message and severity level.
149pub trait DiagnosticReport {
150
151 /// Returns the human-readable message describing this diagnostic.
152 ///
153 /// The message should clearly explain what the issue is and, where possible,
154 /// suggest how to fix it.
155 fn message(&self) -> String;
156
157 /// Returns the severity level of this diagnostic.
158 ///
159 /// Used for filtering and prioritizing diagnostic results.
160 fn level(&self) -> DiagnosticLevel;
161}
162
163//-------------------------------------------------------------------------------//
164// Enums & Structs
165//-------------------------------------------------------------------------------//
166
167/// Container for diagnostic check results and configuration.
168///
169/// This struct holds both the configuration for which diagnostics to run
170/// (via ignore lists) and the results of the diagnostic check.
171///
172/// # Filtering
173///
174/// Use the ignore fields to exclude certain items from diagnostic checks:
175///
176/// - `folders_ignored`: Skip entire folder trees (e.g., "db/deprecated_tables")
177/// - `files_ignored`: Skip specific files by path
178/// - `fields_ignored`: Skip specific table columns (format: "table_name/field_name")
179/// - `diagnostics_ignored`: Skip specific diagnostic types by identifier
180#[derive(Debug, Clone, Default, Getters, MutGetters, Serialize, Deserialize)]
181#[getset(get = "pub", get_mut = "pub")]
182pub struct Diagnostics {
183
184 /// Folder paths to exclude from diagnostic checks.
185 ///
186 /// Files within these folders (and subfolders) will not be checked.
187 folders_ignored: Vec<String>,
188
189 /// File paths to exclude from diagnostic checks.
190 files_ignored: Vec<String>,
191
192 /// Table fields to exclude from diagnostic checks.
193 ///
194 /// Format: "table_name/field_name" (e.g., "units_tables/key")
195 fields_ignored: Vec<String>,
196
197 /// Diagnostic type identifiers to skip.
198 ///
199 /// Use this to disable specific checks that produce false positives
200 /// or are not relevant to your mod.
201 diagnostics_ignored: Vec<String>,
202
203 /// The diagnostic results from the most recent check.
204 results: Vec<DiagnosticType>
205}
206
207/// Wrapper enum for all diagnostic result types.
208///
209/// Each variant corresponds to a different file type or check category,
210/// containing the specific diagnostic struct for that type.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub enum DiagnosticType {
213 /// Diagnostics for animation fragment battle files.
214 AnimFragmentBattle(AnimFragmentBattleDiagnostic),
215 /// Diagnostics for configuration files.
216 Config(ConfigDiagnostic),
217 /// Diagnostics for dependency-related issues.
218 Dependency(DependencyDiagnostic),
219 /// Diagnostics for DB tables.
220 DB(TableDiagnostic),
221 /// Diagnostics for group formations files.
222 GroupFormations(GroupFormationsDiagnostic),
223 /// Diagnostics for Loc (localisation) tables.
224 Loc(TableDiagnostic),
225 /// Diagnostics for pack-level issues.
226 Pack(PackDiagnostic),
227 /// Diagnostics for portrait settings files.
228 PortraitSettings(PortraitSettingsDiagnostic),
229 /// Diagnostics for text/script files.
230 Text(TextDiagnostic),
231}
232
233/// Severity level of a diagnostic result.
234///
235/// Used to categorize diagnostics by importance and filter results
236/// in the user interface.
237#[derive(Debug, Clone, Default, Serialize, Deserialize)]
238pub enum DiagnosticLevel {
239 /// Informational message or suggestion.
240 ///
241 /// These don't indicate errors but may highlight potential improvements
242 /// or provide useful information about the mod.
243 #[default]
244 Info,
245 /// Potential issue that may cause problems.
246 ///
247 /// Warnings indicate things that might be mistakes or could cause
248 /// issues in certain circumstances, but aren't definite errors.
249 Warning,
250 /// Critical issue that will likely cause problems.
251 ///
252 /// Errors indicate definite problems that should be fixed, such as
253 /// invalid references or malformed data that could crash the game.
254 Error,
255}
256
257/// Per-file ignore state derived from the pack's `diagnostics_files_to_ignore` setting.
258///
259/// Tuple shape: `(ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields)`.
260/// `None` means the whole file is skipped.
261pub type FileIgnoreState = (Vec<String>, HashSet<String>, HashMap<String, Vec<String>>);
262
263//-------------------------------------------------------------------------------//
264// Implementations
265//-------------------------------------------------------------------------------//
266
267impl Default for DiagnosticType {
268 fn default() -> Self {
269 Self::Pack(PackDiagnostic::default())
270 }
271}
272
273impl DiagnosticType {
274 pub fn path(&self) -> &str {
275 match self {
276 Self::AnimFragmentBattle(ref diag) => diag.path(),
277 Self::DB(ref diag) |
278 Self::Loc(ref diag) => diag.path(),
279 Self::Pack(_) => "",
280 Self::GroupFormations(diag) => diag.path(),
281 Self::PortraitSettings(diag) => diag.path(),
282 Self::Text(diag) => diag.path(),
283 Self::Dependency(diag) => diag.path(),
284 Self::Config(_) => "",
285 }
286 }
287
288 pub fn pack(&self) -> &str {
289 match self {
290 Self::AnimFragmentBattle(ref diag) => diag.pack(),
291 Self::DB(ref diag) |
292 Self::Loc(ref diag) => diag.pack(),
293 Self::Pack(diag) => diag.pack(),
294 Self::GroupFormations(diag) => diag.pack(),
295 Self::PortraitSettings(diag) => diag.pack(),
296 Self::Text(diag) => diag.pack(),
297 Self::Dependency(diag) => diag.pack(),
298 Self::Config(_) => "",
299 }
300 }
301}
302
303impl Diagnostics {
304
305 /// This function performs a search over the parts of the provided Packs, storing his results.
306 ///
307 /// `on_progress` is called with the steps done and their total as the check advances. If it returns `false`,
308 /// the check stops early, leaving the results incomplete.
309 ///
310 /// # Returns
311 ///
312 /// `false` if the check was stopped before finishing.
313 #[allow(clippy::too_many_arguments)]
314 pub fn check(&mut self, packs: &mut BTreeMap<String, Pack>, dependencies: &mut Dependencies, schema: &Schema, game_info: &GameInfo, game_path: &Path, paths_to_check: &[ContainerPath], check_ak_only_refs: bool, lua_api: Option<&LuaApi>, on_progress: &(dyn Fn(usize, usize) -> bool + Sync)) -> bool {
315
316 // Clear the diagnostics first if we're doing a full check, or only the config ones and the ones for the path to update if we're doing a partial check.
317 if paths_to_check.is_empty() {
318 self.results.clear();
319 } else {
320 self.results.retain(|diagnostic| !paths_to_check.contains(&ContainerPath::File(diagnostic.path().to_string())));
321 self.results.iter_mut().for_each(|x| {
322 if let DiagnosticType::Config(config) = x {
323 config.results_mut().retain(|x|
324 match x.report_type() {
325 ConfigDiagnosticReportType::DependenciesCacheNotGenerated |
326 ConfigDiagnosticReportType::DependenciesCacheOutdated |
327 ConfigDiagnosticReportType::DependenciesCacheCouldNotBeLoaded(_) |
328 ConfigDiagnosticReportType::IncorrectGamePath => false,
329 }
330 );
331 }
332 });
333 }
334
335 // First, check for config issues, as some of them may stop the checking prematurely.
336 if let Some(diagnostics) = ConfigDiagnostic::check(dependencies, game_info, game_path) {
337 let is_diagnostic_blocking = if let DiagnosticType::Config(ref diagnostic) = diagnostics {
338 diagnostic.results().iter().any(|diagnostic| matches!(diagnostic.report_type(),
339 ConfigDiagnosticReportType::IncorrectGamePath |
340 ConfigDiagnosticReportType::DependenciesCacheNotGenerated |
341 ConfigDiagnosticReportType::DependenciesCacheOutdated |
342 ConfigDiagnosticReportType::DependenciesCacheCouldNotBeLoaded(_)))
343 } else { false };
344
345 // If we have one of the blocking diagnostics, report it and return.
346 self.results.push(diagnostics);
347 if is_diagnostic_blocking {
348 return true;
349 }
350 }
351
352 // TODO: Check if we should split this so each pack is only affected by their own ignored files.
353 let files_to_ignore = packs.values().find_map(|pack| pack.settings().diagnostics_files_to_ignore());
354
355 // Set of pack keys whose on-disk path matches a CA (vanilla) pack. Built once
356 // so per-file and per-pack checks can skip diagnostics that don't apply to vanilla content.
357 let ca_pack_paths: HashSet<String> = game_info.ca_packs_paths(game_path).unwrap_or_default()
358 .into_iter()
359 .map(|p| path_to_absolute_string(&p))
360 .collect();
361 let ca_packs: HashSet<String> = packs.iter()
362 .filter(|(_, pack)| ca_pack_paths.contains(&path_to_absolute_string(Path::new(pack.disk_file_path()))))
363 .map(|(key, _)| key.clone())
364 .collect();
365
366 // To make sure we can read any non-db and non-loc file, we need to pre-decode them here.
367 {
368 // Extra data to decode animfragmentbattle files.
369 let mut extra_data = DecodeableExtraData::default();
370 extra_data.set_game_info(Some(game_info));
371 let extra_data = Some(extra_data);
372
373 for pack in packs.values_mut() {
374 pack.files_by_type_mut(&[FileType::AnimFragmentBattle, FileType::GroupFormations, FileType::Text, FileType::PortraitSettings])
375 .par_iter_mut()
376 .for_each(|file| { let _ = file.decode(&extra_data, true, false); });
377 }
378 }
379
380 // The total isn't known yet, so this only asks if the check should go on.
381 if !on_progress(0, 1) {
382 return false;
383 }
384
385 // Logic here: we want to process the tables on batches containing all the tables of the same type, so we can check duplicates in different tables.
386 // To do that, we have to sort/split the file list, the process that.
387 let files: Vec<(&str, &RFile)> = if paths_to_check.is_empty() {
388 packs.iter().flat_map(|(key, pack)| pack.files_by_type(&[FileType::AnimFragmentBattle, FileType::DB, FileType::GroupFormations, FileType::Loc, FileType::Text, FileType::PortraitSettings]).into_iter().map(move |file| (key.as_str(), file))).collect()
389 } else {
390 packs.iter().flat_map(|(key, pack)| pack.files_by_type_and_paths(&[FileType::AnimFragmentBattle, FileType::DB, FileType::GroupFormations, FileType::Loc, FileType::Text, FileType::PortraitSettings], paths_to_check, false).into_iter().map(move |file| (key.as_str(), file))).collect()
391 };
392
393 let mut files_split: HashMap<&str, Vec<(&str, &RFile)>> = HashMap::new();
394 let mut we_need_loc_data = false;
395 for (pack_key, file) in &files {
396 match file.file_type() {
397 FileType::AnimFragmentBattle => {
398 if let Some(table_set) = files_split.get_mut("anim_fragment_battle") {
399 table_set.push((pack_key, file));
400 } else {
401 files_split.insert("anim_fragment_battle", vec![(pack_key, file)]);
402 }
403 },
404 FileType::DB => {
405 we_need_loc_data = true;
406
407 let path_split = file.path_in_container_split();
408 if path_split.len() > 2 {
409 if let Some(table_set) = files_split.get_mut(path_split[1]) {
410 table_set.push((pack_key, file));
411 } else {
412 files_split.insert(path_split[1], vec![(pack_key, file)]);
413 }
414 }
415 },
416 FileType::Loc => {
417 if let Some(table_set) = files_split.get_mut("locs") {
418 table_set.push((pack_key, file));
419 } else {
420 files_split.insert("locs", vec![(pack_key, file)]);
421 }
422 },
423 FileType::Text => {
424 if let Some(name) = file.file_name() {
425 if name.ends_with(".lua") {
426 if let Some(table_set) = files_split.get_mut("lua") {
427 table_set.push((pack_key, file));
428 } else {
429 files_split.insert("lua", vec![(pack_key, file)]);
430 }
431 }
432 }
433 },
434 FileType::GroupFormations => {
435 if let Some(table_set) = files_split.get_mut("group_formations") {
436 table_set.push((pack_key, file));
437 } else {
438 files_split.insert("group_formations", vec![(pack_key, file)]);
439 }
440 },
441 FileType::PortraitSettings => {
442 if let Some(table_set) = files_split.get_mut("portrait_settings") {
443 table_set.push((pack_key, file));
444 } else {
445 files_split.insert("portrait_settings", vec![(pack_key, file)]);
446 }
447 },
448 _ => {},
449 }
450 }
451
452 // Progress steps: decoding the files, generating the references, each checked file, and the global checks.
453 let files_to_check = files_split.values().map(Vec::len).sum::<usize>();
454 let total_steps = files_to_check + 3;
455 let files_checked = AtomicUsize::new(0);
456
457 // Getting this here speeds up a lot path-checking later.
458 let mut local_file_path_list = HashMap::new();
459 for pack in packs.values() {
460 local_file_path_list.extend(pack.paths_cache().iter().map(|(k, v)| (k.clone(), v.clone())));
461 }
462 let local_file_path_list = &local_file_path_list;
463
464 let loc_files: Vec<&RFile> = packs.values().flat_map(|pack| pack.files_by_type(&[FileType::Loc])).collect();
465 let loc_decoded = loc_files.iter()
466 .filter_map(|file| if let Ok(RFileDecoded::Loc(loc)) = file.decoded() { Some(loc) } else { None })
467 .map(|file| file.data())
468 .collect::<Vec<_>>();
469
470 // Loc data takes a few ms to get, and it's only needed if we're going to check on tables, for the lookup data. So only get it if we really need it.
471 let loc_data = if we_need_loc_data {
472 Some(loc_decoded.par_iter()
473 .flat_map(|data| data.par_iter()
474 .map(|entry| (entry[0].data_to_string(), entry[1].data_to_string()))
475 .collect::<Vec<(_,_)>>()
476 ).collect::<HashMap<_,_>>())
477 } else {
478 None
479 };
480
481 // That way we can get it fast on the first try, and skip.
482 let table_names = files_split.iter().filter(|(key, _)| **key != "anim_fragment_battle" && **key != "group_formations" && **key != "locs" && **key != "lua" && **key != "portrait_settings").map(|(key, _)| key.to_string()).collect::<Vec<_>>();
483
484 // If table names is empty this triggers a full regeneration, which is slow as fuck. So make sure to avoid that if we're only doing a partial check.
485 if !table_names.is_empty() || (table_names.is_empty() && paths_to_check.is_empty()) {
486 dependencies.generate_local_db_references(schema, packs, &table_names);
487 }
488
489 // Caches for Portrait Settings diagnostics. There are some alt lookups for tables with differently named columns between games.
490 let art_set_ids = dependencies.db_values_from_table_name_and_column_name(Some(packs), "campaign_character_arts_tables", "art_set_id", true, true);
491 let mut variant_filenames = dependencies.db_values_from_table_name_and_column_name(Some(packs), "variants_tables", "variant_filename", true, true);
492 if variant_filenames.is_empty() {
493 variant_filenames = dependencies.db_values_from_table_name_and_column_name(Some(packs), "variants_tables", "variant_name", true, true);
494 }
495
496 // Scripts can use what any other open script defines, so definitions come from all of them, not only the checked ones.
497 let lua_definitions = if files_split.contains_key("lua") {
498 packs.values()
499 .flat_map(|pack| pack.files_by_type(&[FileType::Text]))
500 .filter(|file| file.path_in_container_raw().ends_with(".lua"))
501 .collect::<Vec<_>>()
502 .par_iter()
503 .filter_map(|file| match file.decoded() {
504 Ok(RFileDecoded::Text(text)) => Some(text.contents()),
505 _ => None,
506 })
507 .fold(LuaDefinitions::default, |mut definitions, source| {
508 definitions.add_script(source);
509 definitions
510 })
511 .reduce(LuaDefinitions::default, |mut definitions, other| {
512 definitions.extend(other);
513 definitions
514 })
515 } else {
516 LuaDefinitions::default()
517 };
518
519 if !on_progress(2, total_steps) {
520 return false;
521 }
522
523 // Process the files in batches.
524 self.results.append(&mut files_split.par_iter().filter_map(|(_, files)| {
525 if !on_progress(2 + files_checked.load(AtomicOrdering::Relaxed), total_steps) {
526 return None;
527 }
528
529 let mut diagnostics = Vec::with_capacity(files.len());
530
531 // Ignore empty groups, which should never happen, but just in case.
532 if let Some(file_type) = files.first().map(|(_, x)| x.file_type()) {
533
534 // DB groups are processed as a group, not per file, so we are able to detect duplicated lines between files.
535 // Same for locs.
536 match file_type {
537 FileType::DB => {
538 diagnostics.extend_from_slice(&TableDiagnostic::check_db(
539 files,
540 dependencies,
541 &self.diagnostics_ignored,
542 game_info,
543 local_file_path_list,
544 check_ak_only_refs,
545 &files_to_ignore,
546 packs,
547 schema,
548 &loc_data,
549 &ca_packs,
550 ));
551 },
552 FileType::Loc => {
553 diagnostics.extend_from_slice(&TableDiagnostic::check_loc(
554 files,
555 &self.diagnostics_ignored,
556 &files_to_ignore,
557 ));
558 }
559 _ => {
560 for (pack_key, file) in files {
561 let (ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields) = Self::ignore_data_for_file(file, &files_to_ignore)?;
562
563 let diagnostic = match file.file_type() {
564 FileType::AnimFragmentBattle => AnimFragmentBattleDiagnostic::check(
565 pack_key,
566 file,
567 dependencies,
568 &self.diagnostics_ignored,
569 &ignored_fields,
570 &ignored_diagnostics,
571 &ignored_diagnostics_for_fields,
572 local_file_path_list,
573 ),
574
575 FileType::GroupFormations => GroupFormationsDiagnostic::check(pack_key, file, &self.diagnostics_ignored),
576 FileType::Text => TextDiagnostic::check(pack_key, file, packs, dependencies, &self.diagnostics_ignored, &ignored_fields, &ignored_diagnostics, &ignored_diagnostics_for_fields, lua_api, &lua_definitions),
577 FileType::PortraitSettings => PortraitSettingsDiagnostic::check(pack_key, file, &art_set_ids, &variant_filenames, dependencies, &self.diagnostics_ignored, &ignored_fields, &ignored_diagnostics, &ignored_diagnostics_for_fields, local_file_path_list),
578 _ => None,
579 };
580
581 if let Some(diagnostic) = diagnostic {
582 diagnostics.push(diagnostic);
583 }
584 }
585 }
586 }
587 }
588
589 files_checked.fetch_add(files.len(), AtomicOrdering::Relaxed);
590 Some(diagnostics)
591 }).flatten().collect());
592
593 if !on_progress(2 + files_checked.load(AtomicOrdering::Relaxed), total_steps) {
594 return false;
595 }
596
597 // These two are global, so do not execute on file-specific runs.
598 if paths_to_check.is_empty() {
599 self.results_mut().extend(DependencyDiagnostic::check(packs));
600 self.results_mut().extend(PackDiagnostic::check(packs, dependencies, game_info, &ca_packs));
601 }
602
603 self.results_mut().sort_by(|a, b| {
604 if !a.path().is_empty() && !b.path().is_empty() {
605 a.path().cmp(b.path())
606 } else if a.path().is_empty() && !b.path().is_empty() {
607 Ordering::Greater
608 } else if !a.path().is_empty() && b.path().is_empty() {
609 Ordering::Less
610 } else {
611 Ordering::Equal
612 }
613 });
614
615 true
616 }
617
618 /// Function to know if an specific field/diagnostic must be ignored.
619 ///
620 /// A diagnostic is ignored if it's ignored globally or for the whole file, if its field is ignored,
621 /// or if it's ignored for its field.
622 fn ignore_diagnostic(global_ignored_diagnostics: &[String], field_name: Option<&str>, diagnostic: Option<&str>, ignored_fields: &[String], ignored_diagnostics: &HashSet<String>, ignored_diagnostics_for_fields: &HashMap<String, Vec<String>>) -> bool {
623 if let Some(diagnostic) = diagnostic {
624 if global_ignored_diagnostics.iter().any(|x| x == diagnostic) || ignored_diagnostics.contains(diagnostic) {
625 return true;
626 }
627 }
628
629 match field_name {
630 Some(field_name) => ignored_fields.iter().any(|x| x == field_name) || diagnostic.is_some_and(|diagnostic| {
631 ignored_diagnostics_for_fields.get(field_name).is_some_and(|diags| diags.iter().any(|x| x == diagnostic))
632 }),
633 None => false,
634 }
635 }
636
637 /// Ignore entire tables if their path starts with the one we have (so we can do mass ignores) and we didn't specified a field to ignore.
638 fn ignore_data_for_file(file: &RFile, files_to_ignore: &Option<Vec<DiagnosticIgnoreEntry>>) -> Option<FileIgnoreState> {
639 let mut ignored_fields = vec![];
640 let mut ignored_diagnostics = HashSet::new();
641 let mut ignored_diagnostics_for_fields: HashMap<String, Vec<String>> = HashMap::new();
642 if let Some(ref files_to_ignore) = files_to_ignore {
643 for (path_to_ignore, fields, diags_to_ignore) in files_to_ignore {
644
645 // If the rule doesn't affect this PackedFile, ignore it.
646 if !path_to_ignore.is_empty() && file.path_in_container_raw().starts_with(path_to_ignore) {
647
648 // If we don't have either fields or diags specified, we ignore the entire file.
649 if fields.is_empty() && diags_to_ignore.is_empty() {
650 return None;
651 }
652
653 // If we have both, fields and diags, disable only those diags for those fields.
654 if !fields.is_empty() && !diags_to_ignore.is_empty() {
655 for field in fields {
656 match ignored_diagnostics_for_fields.get_mut(field) {
657 Some(diagnostics) => diagnostics.append(&mut diags_to_ignore.to_vec()),
658 None => { ignored_diagnostics_for_fields.insert(field.to_owned(), diags_to_ignore.to_vec()); },
659 }
660 }
661 }
662
663 // Otherwise, check if we only have fields or diags, and put them separately.
664 else if !fields.is_empty() {
665 ignored_fields.append(&mut fields.to_vec());
666 }
667
668 else if !diags_to_ignore.is_empty() {
669 ignored_diagnostics.extend(diags_to_ignore.to_vec());
670 }
671 }
672 }
673 }
674 Some((ignored_fields, ignored_diagnostics, ignored_diagnostics_for_fields))
675 }
676
677 /// This function converts an entire diagnostics struct into a JSon string.
678 pub fn json(&self) -> Result<String> {
679 serde_json::to_string_pretty(self).map_err(From::from)
680 }
681}
682
683impl Display for DiagnosticType {
684 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
685 Display::fmt(match self {
686 Self::AnimFragmentBattle(_) => "AnimFragmentBattle",
687 Self::Config(_) => "Config",
688 Self::DB(_) => "DB",
689 Self::GroupFormations(_) => "GroupFormations",
690 Self::Loc(_) => "Loc",
691 Self::Pack(_) => "Packfile",
692 Self::PortraitSettings(_) => "PortraitSettings",
693 Self::Text(_) => "Text",
694 Self::Dependency(_) => "DependencyManager",
695 }, f)
696 }
697}