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