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