rpfm_lib/files/group_formations/
validation.rs1use std::collections::{HashMap, HashSet, hash_map::Entry};
14
15use super::{Block, GroupFormation, GroupFormationBlock, GroupFormations};
16
17#[derive(Clone, Debug, PartialEq)]
23pub enum ValidationIssue {
24
25 DuplicateFormationName,
27
28 NoAbsoluteBlock,
30
31 DuplicateBlockId(u32),
33
34 MissingReference { block_id: u32, referenced_id: u32 },
36
37 ForwardReference { block_id: u32, referenced_id: u32 },
39
40 ReferenceCycle(Vec<u32>),
42
43 EmptySpan(u32),
45
46 InvalidThresholds(u32),
48
49 NoEntityPreferences(u32),
51}
52
53#[derive(Clone, Copy, PartialEq)]
55enum VisitState {
56 Unvisited,
57 OnStack,
58 Done,
59}
60
61struct 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
70impl ValidationIssue {
75
76 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 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 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 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 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 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}