Skip to main content

rpfm_extensions/diagnostics/
group_formations.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//! Module with the structs and functions specific for `GroupFormations` diagnostics.
12//!
13//! The checks themselves live in `rpfm_lib`, as the GroupFormations editor uses them too. This module
14//! turns their results into diagnostics.
15
16use getset::{Getters, MutGetters};
17use serde_derive::{Serialize, Deserialize};
18
19use std::{fmt, fmt::Display};
20
21use rpfm_lib::files::{RFile, RFileDecoded};
22use rpfm_lib::files::group_formations::validation::ValidationIssue;
23
24use crate::diagnostics::*;
25
26//-------------------------------------------------------------------------------//
27//                              Enums & Structs
28//-------------------------------------------------------------------------------//
29
30/// This struct contains the results of a GroupFormations diagnostic.
31#[derive(Debug, Clone, Default, Getters, MutGetters, Serialize, Deserialize)]
32#[getset(get = "pub", get_mut = "pub")]
33pub struct GroupFormationsDiagnostic {
34    path: String,
35    pack: String,
36    results: Vec<GroupFormationsDiagnosticReport>
37}
38
39/// This struct defines an individual GroupFormations diagnostic result.
40#[derive(Debug, Clone, Getters, MutGetters, Serialize, Deserialize)]
41#[getset(get = "pub", get_mut = "pub")]
42pub struct GroupFormationsDiagnosticReport {
43
44    /// Index of the formation with the issue, in the file.
45    formation_index: usize,
46
47    /// Name of the formation with the issue.
48    formation_name: String,
49
50    report_type: GroupFormationsDiagnosticReportType,
51}
52
53/// Kinds of GroupFormations issues. They match the issues found by the file's validation.
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
55pub enum GroupFormationsDiagnosticReportType {
56    DuplicateFormationName,
57    NoAbsoluteBlock,
58    DuplicateBlockId(u32),
59    MissingReference(u32, u32),
60    ForwardReference(u32, u32),
61    ReferenceCycle(Vec<u32>),
62    EmptySpan(u32),
63    InvalidThresholds(u32),
64    NoEntityPreferences(u32),
65}
66
67//-------------------------------------------------------------------------------//
68//                             Implementations
69//-------------------------------------------------------------------------------//
70
71impl GroupFormationsDiagnosticReport {
72    pub fn new(formation_index: usize, formation_name: &str, report_type: GroupFormationsDiagnosticReportType) -> Self {
73        Self {
74            formation_index,
75            formation_name: formation_name.to_owned(),
76            report_type,
77        }
78    }
79}
80
81impl GroupFormationsDiagnosticReportType {
82
83    /// Returns the block the issue points to, if it points to a specific one.
84    pub fn block_id(&self) -> Option<u32> {
85        match self {
86            Self::DuplicateFormationName |
87            Self::NoAbsoluteBlock => None,
88            Self::DuplicateBlockId(block_id) |
89            Self::MissingReference(block_id, _) |
90            Self::ForwardReference(block_id, _) |
91            Self::EmptySpan(block_id) |
92            Self::InvalidThresholds(block_id) |
93            Self::NoEntityPreferences(block_id) => Some(*block_id),
94            Self::ReferenceCycle(block_ids) => block_ids.first().copied(),
95        }
96    }
97}
98
99impl From<ValidationIssue> for GroupFormationsDiagnosticReportType {
100    fn from(issue: ValidationIssue) -> Self {
101        match issue {
102            ValidationIssue::DuplicateFormationName => Self::DuplicateFormationName,
103            ValidationIssue::NoAbsoluteBlock => Self::NoAbsoluteBlock,
104            ValidationIssue::DuplicateBlockId(block_id) => Self::DuplicateBlockId(block_id),
105            ValidationIssue::MissingReference { block_id, referenced_id } => Self::MissingReference(block_id, referenced_id),
106            ValidationIssue::ForwardReference { block_id, referenced_id } => Self::ForwardReference(block_id, referenced_id),
107            ValidationIssue::ReferenceCycle(block_ids) => Self::ReferenceCycle(block_ids),
108            ValidationIssue::EmptySpan(block_id) => Self::EmptySpan(block_id),
109            ValidationIssue::InvalidThresholds(block_id) => Self::InvalidThresholds(block_id),
110            ValidationIssue::NoEntityPreferences(block_id) => Self::NoEntityPreferences(block_id),
111        }
112    }
113}
114
115impl DiagnosticReport for GroupFormationsDiagnosticReport {
116    fn message(&self) -> String {
117        let name = &self.formation_name;
118        match &self.report_type {
119            GroupFormationsDiagnosticReportType::DuplicateFormationName => format!("Formation '{name}': another formation has the same name."),
120            GroupFormationsDiagnosticReportType::NoAbsoluteBlock => format!("Formation '{name}': it has no absolute block, so nothing anchors its blocks."),
121            GroupFormationsDiagnosticReportType::DuplicateBlockId(block_id) => format!("Formation '{name}': more than one block uses the id {block_id}."),
122            GroupFormationsDiagnosticReportType::MissingReference(block_id, referenced_id) => format!("Formation '{name}': block {block_id} references block {referenced_id}, which doesn't exist."),
123            GroupFormationsDiagnosticReportType::ForwardReference(block_id, referenced_id) => format!("Formation '{name}': block {block_id} references block {referenced_id}, which is defined after it."),
124            GroupFormationsDiagnosticReportType::ReferenceCycle(block_ids) => {
125                let block_ids = block_ids.iter().map(|block_id| block_id.to_string()).collect::<Vec<_>>().join(", ");
126                format!("Formation '{name}': blocks {block_ids} depend on each other in a loop.")
127            },
128            GroupFormationsDiagnosticReportType::EmptySpan(block_id) => format!("Formation '{name}': span {block_id} has no members."),
129            GroupFormationsDiagnosticReportType::InvalidThresholds(block_id) => format!("Formation '{name}': block {block_id} has a minimum amount of units above its maximum."),
130            GroupFormationsDiagnosticReportType::NoEntityPreferences(block_id) => format!("Formation '{name}': block {block_id} has no entity preferences, so no unit can be placed in it."),
131        }
132    }
133
134    fn level(&self) -> DiagnosticLevel {
135        match self.report_type {
136            GroupFormationsDiagnosticReportType::NoAbsoluteBlock |
137            GroupFormationsDiagnosticReportType::DuplicateBlockId(_) |
138            GroupFormationsDiagnosticReportType::MissingReference(_, _) |
139            GroupFormationsDiagnosticReportType::ReferenceCycle(_) => DiagnosticLevel::Error,
140            GroupFormationsDiagnosticReportType::DuplicateFormationName |
141            GroupFormationsDiagnosticReportType::ForwardReference(_, _) |
142            GroupFormationsDiagnosticReportType::EmptySpan(_) |
143            GroupFormationsDiagnosticReportType::InvalidThresholds(_) |
144            GroupFormationsDiagnosticReportType::NoEntityPreferences(_) => DiagnosticLevel::Warning,
145        }
146    }
147}
148
149impl Display for GroupFormationsDiagnosticReportType {
150    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
151        Display::fmt(match self {
152            Self::DuplicateFormationName => "GroupFormationsDuplicateFormationName",
153            Self::NoAbsoluteBlock => "GroupFormationsNoAbsoluteBlock",
154            Self::DuplicateBlockId(_) => "GroupFormationsDuplicateBlockId",
155            Self::MissingReference(_, _) => "GroupFormationsMissingReference",
156            Self::ForwardReference(_, _) => "GroupFormationsForwardReference",
157            Self::ReferenceCycle(_) => "GroupFormationsReferenceCycle",
158            Self::EmptySpan(_) => "GroupFormationsEmptySpan",
159            Self::InvalidThresholds(_) => "GroupFormationsInvalidThresholds",
160            Self::NoEntityPreferences(_) => "GroupFormationsNoEntityPreferences",
161        }, f)
162    }
163}
164
165impl GroupFormationsDiagnostic {
166    pub fn new(path: &str, pack: &str) -> Self {
167        Self {
168            path: path.to_owned(),
169            pack: pack.to_owned(),
170            results: vec![],
171        }
172    }
173
174    /// This function takes care of checking a GroupFormations file for errors.
175    ///
176    /// # Arguments
177    ///
178    /// * `pack_key` - Key of the pack the file belongs to.
179    /// * `file` - The file to check. It must be already decoded.
180    /// * `global_ignored_diagnostics` - Diagnostics disabled in the diagnostics panel.
181    ///
182    /// # Returns
183    ///
184    /// The diagnostic with the issues of the file, or `None` if it has none or isn't decoded.
185    pub fn check(
186        pack_key: &str,
187        file: &RFile,
188        global_ignored_diagnostics: &[String],
189    ) -> Option<DiagnosticType> {
190        let Ok(RFileDecoded::GroupFormations(data)) = file.decoded() else { return None };
191
192        let mut diagnostic = GroupFormationsDiagnostic::new(file.path_in_container_raw(), pack_key);
193        for (formation_index, issue) in data.validate() {
194            let report_type = GroupFormationsDiagnosticReportType::from(issue);
195            if global_ignored_diagnostics.contains(&report_type.to_string()) {
196                continue;
197            }
198
199            let formation_name = data.formations().get(formation_index).map(|formation| formation.name().as_str()).unwrap_or_default();
200            diagnostic.results_mut().push(GroupFormationsDiagnosticReport::new(formation_index, formation_name, report_type));
201        }
202
203        if diagnostic.results().is_empty() {
204            None
205        } else {
206            Some(DiagnosticType::GroupFormations(diagnostic))
207        }
208    }
209}