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}