1use wowlab_types::{combat::ResourceType, sim::FastSet};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub struct HeroTalentTreeDesc {
8 pub name: &'static str,
9 pub spells: &'static [(&'static str, u32)],
10 pub auras: &'static [(&'static str, u32)],
11}
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub struct DeclaredServerProcEntry {
16 pub spell_id: u32,
17 pub attributes: u32,
18}
19
20#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct DeclaredPassiveEffectOverride {
26 pub spell_id: u32,
27 pub effect: u8,
28 pub source: Option<(u32, u8)>,
29 pub value: f64,
30 pub multiplier: f64,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct DeclaredSpecMetadata {
36 pub primary_resource: ResourceType,
37 pub secondary_resource: Option<ResourceType>,
38 pub spell_ids: &'static [u32],
39 pub aura_ids: &'static [u32],
40 pub server_proc_entries: &'static [DeclaredServerProcEntry],
41 pub talents: &'static [(&'static str, u32)],
42 pub rotation_field_schema: &'static [&'static str],
43 pub metrics_plugin_id: Option<u32>,
44 pub hero_talent_trees: &'static [HeroTalentTreeDesc],
45 pub reported_spells: &'static [(&'static str, u32)],
46 pub masked_passive_effects: &'static [(u32, u8)],
49 pub passive_effect_overrides: &'static [DeclaredPassiveEffectOverride],
51}
52
53wowlab_engine_macros::define_error! {
54#[derive(Debug, Eq, PartialEq)]
56pub struct DeclaredSpecMetadataError {
57 kind: DeclaredSpecMetadataErrorKind,
58}
59
60#[derive(Debug, thiserror::Error, Eq, PartialEq)]
61enum DeclaredSpecMetadataErrorKind {
62 #[error("declared spec metadata contains no spell IDs")]
63 EmptySpellIds,
64 #[error("declared {category} contains reserved spell ID 0")]
65 ZeroSpellId { category: &'static str },
66 #[error("declared aura ID {spell_id} is absent from the spell-resolution set")]
67 AuraMissingFromSpells { spell_id: u32 },
68 #[error("declared server proc spell ID {spell_id} is absent from the spell-resolution set")]
69 ServerProcMissingFromSpells { spell_id: u32 },
70 #[error("declared server proc spell ID {spell_id} appears more than once")]
71 DuplicateServerProc { spell_id: u32 },
72 #[error("declared server proc spell ID {spell_id} has no attributes")]
73 EmptyServerProcAttributes { spell_id: u32 },
74 #[error(
75 "declared {category} '{name}' with ID {spell_id} is absent from the spell-resolution set"
76 )]
77 NamedSpellMissingFromSpells {
78 category: &'static str,
79 name: &'static str,
80 spell_id: u32,
81 },
82 #[error(
83 "declared {category} '{name}' with ID {spell_id} is absent from the aura-resolution set"
84 )]
85 NamedAuraMissingFromAuras {
86 category: &'static str,
87 name: &'static str,
88 spell_id: u32,
89 },
90 #[error("declared {category} contains an empty symbolic name")]
91 EmptyName { category: &'static str },
92 #[error("declared {category} contains duplicate symbolic name '{name}'")]
93 DuplicateName {
94 category: &'static str,
95 name: &'static str,
96 },
97 #[error("primary and secondary resources are both {resource}")]
98 DuplicateResource { resource: ResourceType },
99}
100}
101
102impl DeclaredSpecMetadataError {
103 fn new(kind: DeclaredSpecMetadataErrorKind) -> Self {
104 Self { kind }
105 }
106}
107
108impl DeclaredSpecMetadata {
109 pub fn validate(&self) -> Result<(), DeclaredSpecMetadataError> {
113 self.validate_resolution_sets()?;
114
115 self.validate_named_declarations()
116 }
117
118 fn validate_resolution_sets(&self) -> Result<(), DeclaredSpecMetadataError> {
119 if self.spell_ids.is_empty() {
120 return Err(DeclaredSpecMetadataError::new(
121 DeclaredSpecMetadataErrorKind::EmptySpellIds,
122 ));
123 }
124
125 ensure_nonzero_ids("spell IDs", self.spell_ids)?;
126 ensure_nonzero_ids("aura IDs", self.aura_ids)?;
127
128 if self.secondary_resource == Some(self.primary_resource) {
129 return Err(DeclaredSpecMetadataError::new(
130 DeclaredSpecMetadataErrorKind::DuplicateResource {
131 resource: self.primary_resource,
132 },
133 ));
134 }
135
136 let spells: FastSet<u32> = self.spell_ids.iter().copied().collect();
137 let auras: FastSet<u32> = self.aura_ids.iter().copied().collect();
138
139 for &spell_id in self.aura_ids {
140 if !spells.contains(&spell_id) {
141 return Err(DeclaredSpecMetadataError::new(
142 DeclaredSpecMetadataErrorKind::AuraMissingFromSpells { spell_id },
143 ));
144 }
145 }
146
147 validate_server_proc_entries(self.server_proc_entries, &spells)?;
148
149 validate_named_spells("reported spells", self.reported_spells, &spells)?;
150 validate_hero_talent_trees(self.hero_talent_trees, &spells, &auras)?;
151
152 Ok(())
153 }
154
155 fn validate_named_declarations(&self) -> Result<(), DeclaredSpecMetadataError> {
156 ensure_unique_names("talents", self.talents.iter().map(|&(name, _)| name))?;
157 ensure_nonzero_named_ids("talents", self.talents)?;
158 ensure_unique_names(
159 "rotation fields",
160 self.rotation_field_schema.iter().copied(),
161 )?;
162 ensure_unique_names(
163 "hero talent trees",
164 self.hero_talent_trees.iter().map(|tree| tree.name),
165 )?;
166
167 Ok(())
168 }
169}
170
171fn validate_server_proc_entries(
172 entries: &[DeclaredServerProcEntry],
173 spells: &FastSet<u32>,
174) -> Result<(), DeclaredSpecMetadataError> {
175 let mut seen = FastSet::default();
176
177 for entry in entries {
178 if !spells.contains(&entry.spell_id) {
179 return Err(DeclaredSpecMetadataError::new(
180 DeclaredSpecMetadataErrorKind::ServerProcMissingFromSpells {
181 spell_id: entry.spell_id,
182 },
183 ));
184 }
185
186 if entry.attributes == 0 {
187 return Err(DeclaredSpecMetadataError::new(
188 DeclaredSpecMetadataErrorKind::EmptyServerProcAttributes {
189 spell_id: entry.spell_id,
190 },
191 ));
192 }
193
194 if !seen.insert(entry.spell_id) {
195 return Err(DeclaredSpecMetadataError::new(
196 DeclaredSpecMetadataErrorKind::DuplicateServerProc {
197 spell_id: entry.spell_id,
198 },
199 ));
200 }
201 }
202
203 Ok(())
204}
205
206fn validate_hero_talent_trees(
207 trees: &[HeroTalentTreeDesc],
208 spells: &FastSet<u32>,
209 auras: &FastSet<u32>,
210) -> Result<(), DeclaredSpecMetadataError> {
211 for tree in trees {
212 validate_named_spells("hero talent spells", tree.spells, spells)?;
213 validate_named_auras("hero talent auras", tree.auras, spells, auras)?;
214 }
215
216 Ok(())
217}
218
219fn ensure_nonzero_ids(
220 category: &'static str,
221 ids: &[u32],
222) -> Result<(), DeclaredSpecMetadataError> {
223 if ids.contains(&0) {
224 return Err(DeclaredSpecMetadataError::new(
225 DeclaredSpecMetadataErrorKind::ZeroSpellId { category },
226 ));
227 }
228
229 Ok(())
230}
231
232fn ensure_nonzero_named_ids(
233 category: &'static str,
234 entries: &[(&'static str, u32)],
235) -> Result<(), DeclaredSpecMetadataError> {
236 if entries.iter().any(|&(_, spell_id)| spell_id == 0) {
237 return Err(DeclaredSpecMetadataError::new(
238 DeclaredSpecMetadataErrorKind::ZeroSpellId { category },
239 ));
240 }
241
242 Ok(())
243}
244
245fn ensure_unique_names(
246 category: &'static str,
247 names: impl IntoIterator<Item = &'static str>,
248) -> Result<(), DeclaredSpecMetadataError> {
249 let mut seen = FastSet::default();
250
251 for name in names {
252 if name.is_empty() {
253 return Err(DeclaredSpecMetadataError::new(
254 DeclaredSpecMetadataErrorKind::EmptyName { category },
255 ));
256 }
257
258 if !seen.insert(name) {
259 return Err(DeclaredSpecMetadataError::new(
260 DeclaredSpecMetadataErrorKind::DuplicateName { category, name },
261 ));
262 }
263 }
264
265 Ok(())
266}
267
268fn validate_named_spells(
269 category: &'static str,
270 entries: &[(&'static str, u32)],
271 spells: &FastSet<u32>,
272) -> Result<(), DeclaredSpecMetadataError> {
273 ensure_unique_names(category, entries.iter().map(|&(name, _)| name))?;
274 ensure_nonzero_named_ids(category, entries)?;
275
276 for &(name, spell_id) in entries {
277 if !spells.contains(&spell_id) {
278 return Err(DeclaredSpecMetadataError::new(
279 DeclaredSpecMetadataErrorKind::NamedSpellMissingFromSpells {
280 category,
281 name,
282 spell_id,
283 },
284 ));
285 }
286 }
287
288 Ok(())
289}
290
291fn validate_named_auras(
292 category: &'static str,
293 entries: &[(&'static str, u32)],
294 spells: &FastSet<u32>,
295 auras: &FastSet<u32>,
296) -> Result<(), DeclaredSpecMetadataError> {
297 validate_named_spells(category, entries, spells)?;
298
299 for &(name, spell_id) in entries {
300 if !auras.contains(&spell_id) {
301 return Err(DeclaredSpecMetadataError::new(
302 DeclaredSpecMetadataErrorKind::NamedAuraMissingFromAuras {
303 category,
304 name,
305 spell_id,
306 },
307 ));
308 }
309 }
310
311 Ok(())
312}
313
314#[cfg(test)]
315mod tests;