Skip to main content

rpfm_lib/files/group_formations/
validation.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//! Consistency checks for Group Formations files.
12
13use std::collections::{HashMap, HashSet, hash_map::Entry};
14
15use super::{Block, GroupFormation, GroupFormationBlock, GroupFormations};
16
17//---------------------------------------------------------------------------//
18//                              Enum & Structs
19//---------------------------------------------------------------------------//
20
21/// A problem found in a Group Formations file.
22#[derive(Clone, Debug, PartialEq)]
23pub enum ValidationIssue {
24
25    /// Another formation earlier in the file has the same name.
26    DuplicateFormationName,
27
28    /// The formation has no absolute container, so nothing anchors its blocks.
29    NoAbsoluteBlock,
30
31    /// More than one block uses this id.
32    DuplicateBlockId(u32),
33
34    /// A block references an id that doesn't exist in the formation.
35    MissingReference { block_id: u32, referenced_id: u32 },
36
37    /// A block references a block defined after it. Vanilla files never do this.
38    ForwardReference { block_id: u32, referenced_id: u32 },
39
40    /// These blocks depend on each other's position in a loop.
41    ReferenceCycle(Vec<u32>),
42
43    /// A spanning block with no members.
44    EmptySpan(u32),
45
46    /// A container whose minimum entity threshold is above its maximum.
47    InvalidThresholds(u32),
48
49    /// A container with no entity preferences, so no unit can be placed in it.
50    NoEntityPreferences(u32),
51}
52
53/// Visit state of a block while searching for reference cycles.
54#[derive(Clone, Copy, PartialEq)]
55enum VisitState {
56    Unvisited,
57    OnStack,
58    Done,
59}
60
61/// Depth-first search over the block dependencies, collecting every cycle it finds.
62struct CycleFinder<'a> {
63    blocks: &'a [GroupFormationBlock],
64    positions: &'a HashMap<u32, usize>,
65    states: Vec<VisitState>,
66    stack: Vec<usize>,
67    cycles: Vec<Vec<u32>>,
68}
69
70//---------------------------------------------------------------------------//
71//                             Implementations
72//---------------------------------------------------------------------------//
73
74impl ValidationIssue {
75
76    /// Returns if this issue can break the formation in-game, as opposed to being just suspicious.
77    pub fn is_error(&self) -> bool {
78        match self {
79            Self::NoAbsoluteBlock |
80            Self::DuplicateBlockId(_) |
81            Self::MissingReference { .. } |
82            Self::ReferenceCycle(_) => true,
83            Self::DuplicateFormationName |
84            Self::ForwardReference { .. } |
85            Self::EmptySpan(_) |
86            Self::InvalidThresholds(_) |
87            Self::NoEntityPreferences(_) => false,
88        }
89    }
90}
91
92impl GroupFormations {
93
94    /// Checks all the formations of the file for consistency problems.
95    ///
96    /// # Returns
97    ///
98    /// A list of issues, each one paired with the index of the formation it was found in.
99    pub fn validate(&self) -> Vec<(usize, ValidationIssue)> {
100        let mut issues = vec![];
101        let mut names = HashSet::with_capacity(self.formations.len());
102
103        for (index, formation) in self.formations.iter().enumerate() {
104            if !names.insert(formation.name.as_str()) {
105                issues.push((index, ValidationIssue::DuplicateFormationName));
106            }
107
108            issues.extend(formation.validate().into_iter().map(|issue| (index, issue)));
109        }
110
111        issues
112    }
113}
114
115impl GroupFormation {
116
117    /// Checks the blocks of this formation for consistency problems.
118    ///
119    /// # Returns
120    ///
121    /// A list of the issues found, in block order.
122    pub fn validate(&self) -> Vec<ValidationIssue> {
123        let mut issues = vec![];
124        let blocks = &self.group_formation_blocks;
125
126        if !blocks.iter().any(|block| matches!(block.block, Block::ContainerAbsolute(_))) {
127            issues.push(ValidationIssue::NoAbsoluteBlock);
128        }
129
130        // Duplicated ids keep the position of their first appearance, as that's the one references will find.
131        let mut positions = HashMap::with_capacity(blocks.len());
132        for (position, block) in blocks.iter().enumerate() {
133            match positions.entry(block.block_id) {
134                Entry::Occupied(_) => issues.push(ValidationIssue::DuplicateBlockId(block.block_id)),
135                Entry::Vacant(entry) => { entry.insert(position); },
136            }
137        }
138
139        for (position, block) in blocks.iter().enumerate() {
140            let block_id = block.block_id;
141            for referenced_id in block.block.dependencies() {
142                match positions.get(referenced_id) {
143                    None => issues.push(ValidationIssue::MissingReference { block_id, referenced_id: *referenced_id }),
144                    Some(referenced_position) if *referenced_position > position => issues.push(ValidationIssue::ForwardReference { block_id, referenced_id: *referenced_id }),
145                    Some(_) => {},
146                }
147            }
148
149            let (minimum, maximum, has_preferences) = match &block.block {
150                Block::ContainerAbsolute(container) => (*container.minimum_entity_threshold(), *container.maximum_entity_threshold(), !container.entity_preferences().is_empty()),
151                Block::ContainerRelative(container) => (*container.minimum_entity_threshold(), *container.maximum_entity_threshold(), !container.entity_preferences().is_empty()),
152                Block::Spanning(span) => {
153                    if span.spanned_block_ids().is_empty() {
154                        issues.push(ValidationIssue::EmptySpan(block_id));
155                    }
156                    continue;
157                },
158            };
159
160            // A negative maximum means there's no limit.
161            if maximum >= 0 && minimum > maximum {
162                issues.push(ValidationIssue::InvalidThresholds(block_id));
163            }
164
165            if !has_preferences {
166                issues.push(ValidationIssue::NoEntityPreferences(block_id));
167            }
168        }
169
170        let mut finder = CycleFinder {
171            blocks,
172            positions: &positions,
173            states: vec![VisitState::Unvisited; blocks.len()],
174            stack: vec![],
175            cycles: vec![],
176        };
177
178        for position in 0..blocks.len() {
179            if finder.states[position] == VisitState::Unvisited {
180                finder.visit(position);
181            }
182        }
183
184        issues.extend(finder.cycles.into_iter().map(ValidationIssue::ReferenceCycle));
185        issues
186    }
187}
188
189impl CycleFinder<'_> {
190
191    /// Visits a block and all its dependencies, recording a cycle whenever a dependency is still on the stack.
192    fn visit(&mut self, position: usize) {
193        self.states[position] = VisitState::OnStack;
194        self.stack.push(position);
195
196        for referenced_id in self.blocks[position].block.dependencies() {
197            let Some(&referenced_position) = self.positions.get(referenced_id) else { continue };
198
199            match self.states[referenced_position] {
200                VisitState::Unvisited => self.visit(referenced_position),
201                VisitState::OnStack => {
202                    let start = self.stack.iter().position(|stacked| *stacked == referenced_position).expect("a block marked as on stack must be in the stack");
203                    self.cycles.push(self.stack[start..].iter().map(|stacked| self.blocks[*stacked].block_id).collect());
204                },
205                VisitState::Done => {},
206            }
207        }
208
209        self.stack.pop();
210        self.states[position] = VisitState::Done;
211    }
212}