Skip to main content

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}