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