1use std::fmt;
4
5use indexmap::IndexSet;
6
7use crate::{EventEffectDef, ImpactProcDef, ImpactProcRngDef, Manifest};
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11#[non_exhaustive]
12pub enum ManifestReferenceKind {
13 Aura,
14 Spell,
15 Talent,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
20#[non_exhaustive]
21pub enum ManifestDiagnosticLocation {
22 SpecPrecombatAuras,
23 SpecStealthAura,
24 Aura { name: String },
25 Spell { name: String },
26 AutoAttack { name: String },
27 TalentCompanionAuras { talent: String },
28 TalentGatedAuraEffect { index: usize },
29 SpellGroup { index: usize },
30 ImpactProc { index: usize },
31}
32
33impl fmt::Display for ManifestDiagnosticLocation {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 Self::SpecPrecombatAuras => f.write_str("spec.precombat_auras"),
37 Self::SpecStealthAura => f.write_str("spec.stealth_aura"),
38 Self::Aura { name } => write!(f, "auras.{name}"),
39 Self::Spell { name } => write!(f, "spells.{name}"),
40 Self::AutoAttack { name } => write!(f, "auto_attacks.{name}"),
41 Self::TalentCompanionAuras { .. } => f.write_str("talent_companion_auras"),
42 Self::TalentGatedAuraEffect { .. } => f.write_str("talent_gated_aura_effects"),
43 Self::SpellGroup { .. } => f.write_str("spell_groups"),
44 Self::ImpactProc { .. } => f.write_str("impact_procs"),
45 }
46 }
47}
48
49#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
51#[non_exhaustive]
52pub enum ManifestDiagnostic {
53 #[error("{location} references missing `{reference}`")]
54 MissingReference {
55 location: ManifestDiagnosticLocation,
56 kind: ManifestReferenceKind,
57 reference: String,
58 },
59 #[error("{location} uses unknown auto_attack_hand `{hand}`")]
60 UnknownAutoAttackHand {
61 location: ManifestDiagnosticLocation,
62 hand: String,
63 },
64 #[error("exactly one of spell, auto_attack_hand, or proc_type_mask_spell_id must be set")]
65 InvalidImpactProcSource {
66 location: ManifestDiagnosticLocation,
67 },
68 #[error(
69 "{location} must set proc_type_mask_effect to a positive DBC effect index exactly when proc_type_mask_spell_id is set"
70 )]
71 InvalidImpactProcDriverCoordinate {
72 location: ManifestDiagnosticLocation,
73 },
74 #[error("{location} has an invalid shuffled RNG deck")]
75 InvalidImpactProcRng {
76 location: ManifestDiagnosticLocation,
77 },
78 #[error("{location} source spell {spell_id} is not a declared aura")]
79 UnknownAuraEffectSource {
80 location: ManifestDiagnosticLocation,
81 spell_id: u32,
82 },
83 #[error("{location} uses invalid DBC effect index 0")]
84 InvalidAuraEffectIndex {
85 location: ManifestDiagnosticLocation,
86 },
87}
88
89impl ManifestDiagnostic {
90 #[must_use]
92 pub fn location(&self) -> &ManifestDiagnosticLocation {
93 match self {
94 Self::MissingReference { location, .. }
95 | Self::UnknownAutoAttackHand { location, .. }
96 | Self::InvalidImpactProcSource { location }
97 | Self::InvalidImpactProcDriverCoordinate { location }
98 | Self::InvalidImpactProcRng { location }
99 | Self::UnknownAuraEffectSource { location, .. }
100 | Self::InvalidAuraEffectIndex { location } => location,
101 }
102 }
103}
104
105impl Manifest {
106 #[must_use]
108 pub fn diagnostics(&self) -> Vec<ManifestDiagnostic> {
109 ManifestValidator::new(self).validate()
110 }
111}
112
113struct ManifestValidator<'a> {
114 manifest: &'a Manifest,
115 auras: IndexSet<&'a str>,
116 spells: IndexSet<&'a str>,
117 talents: IndexSet<&'a str>,
118 diagnostics: Vec<ManifestDiagnostic>,
119}
120
121impl<'a> ManifestValidator<'a> {
122 fn new(manifest: &'a Manifest) -> Self {
123 Self {
124 manifest,
125 auras: manifest.auras.keys().map(String::as_str).collect(),
126 spells: manifest.spells.keys().map(String::as_str).collect(),
127 talents: manifest.talents.keys().map(String::as_str).collect(),
128 diagnostics: Vec::with_capacity(manifest.auras.len() + manifest.spells.len()),
129 }
130 }
131
132 fn validate(mut self) -> Vec<ManifestDiagnostic> {
133 self.validate_spec_and_auras();
134 self.validate_spells_and_auto_attacks();
135 self.validate_collections();
136
137 self.diagnostics
138 }
139
140 fn validate_spec_and_auras(&mut self) {
142 if let Some(precombat) = &self.manifest.spec.precombat_auras {
143 let location = ManifestDiagnosticLocation::SpecPrecombatAuras;
144
145 for aura in precombat {
146 require_reference(
147 &self.auras,
148 aura,
149 ManifestReferenceKind::Aura,
150 &location,
151 &mut self.diagnostics,
152 );
153 }
154 }
155
156 if let Some(aura) = &self.manifest.spec.stealth_aura {
157 require_reference(
158 &self.auras,
159 aura,
160 ManifestReferenceKind::Aura,
161 &ManifestDiagnosticLocation::SpecStealthAura,
162 &mut self.diagnostics,
163 );
164 }
165
166 for (name, aura) in &self.manifest.auras {
167 let location = ManifestDiagnosticLocation::Aura { name: name.clone() };
168
169 if let Some(periodic) = &aura.periodic_apply_aura {
170 require_reference(
171 &self.auras,
172 &periodic.aura,
173 ManifestReferenceKind::Aura,
174 &location,
175 &mut self.diagnostics,
176 );
177 }
178
179 validate_event_aura_references(
180 &aura.on_consume,
181 &self.auras,
182 &location,
183 &mut self.diagnostics,
184 );
185
186 for spell in aura.scoped_spell_names() {
187 require_reference(
188 &self.spells,
189 spell,
190 ManifestReferenceKind::Spell,
191 &location,
192 &mut self.diagnostics,
193 );
194 }
195 }
196 }
197
198 fn validate_spells_and_auto_attacks(&mut self) {
200 for (name, spell) in &self.manifest.spells {
201 let location = ManifestDiagnosticLocation::Spell { name: name.clone() };
202
203 for aura in spell
204 .applies_aura
205 .iter()
206 .chain(spell.applies_auras.iter())
207 .chain(spell.extends_auras.iter().map(|extend| &extend.aura))
208 .chain(spell.cooldown_bypass_when_aura.iter())
209 .chain(spell.instant_when_aura.iter())
210 .chain(spell.override_when_aura.iter())
211 .chain(
212 spell
213 .empower
214 .iter()
215 .filter_map(|empower| empower.max_when_aura.as_ref()),
216 )
217 {
218 require_reference(
219 &self.auras,
220 aura,
221 ManifestReferenceKind::Aura,
222 &location,
223 &mut self.diagnostics,
224 );
225 }
226
227 self.validate_spell_reference_fields(spell, &location);
228 }
229
230 for (name, auto_attack) in &self.manifest.auto_attacks {
231 if let Some(reduction) = &auto_attack.on_crit_reduce_charge {
232 require_reference(
233 &self.spells,
234 &reduction.target,
235 ManifestReferenceKind::Spell,
236 &ManifestDiagnosticLocation::AutoAttack { name: name.clone() },
237 &mut self.diagnostics,
238 );
239 }
240 }
241 }
242
243 fn validate_spell_reference_fields(
244 &mut self,
245 spell: &crate::ManifestSpellDef,
246 location: &ManifestDiagnosticLocation,
247 ) {
248 if let Some(override_spell) = &spell.override_with_spell {
249 require_reference(
250 &self.spells,
251 override_spell,
252 ManifestReferenceKind::Spell,
253 location,
254 &mut self.diagnostics,
255 );
256 }
257
258 if let Some(reductions) = &spell.reduces_cd {
259 for reduction in reductions {
260 require_reference(
261 &self.spells,
262 &reduction.target,
263 ManifestReferenceKind::Spell,
264 location,
265 &mut self.diagnostics,
266 );
267 }
268 }
269
270 if let Some(reduction) = &spell.reduces_cd_chance {
271 require_reference(
272 &self.spells,
273 &reduction.target,
274 ManifestReferenceKind::Spell,
275 location,
276 &mut self.diagnostics,
277 );
278 }
279
280 if let Some(reset) = &spell.resets_cd_while {
281 require_reference(
282 &self.spells,
283 &reset.target,
284 ManifestReferenceKind::Spell,
285 location,
286 &mut self.diagnostics,
287 );
288 require_reference(
289 &self.auras,
290 &reset.aura,
291 ManifestReferenceKind::Aura,
292 location,
293 &mut self.diagnostics,
294 );
295 }
296
297 if let Some(channel) = &spell.channel {
298 if let Some(alternative) = &channel.tick_cost_alt {
299 require_reference(
300 &self.auras,
301 &alternative.aura,
302 ManifestReferenceKind::Aura,
303 location,
304 &mut self.diagnostics,
305 );
306 }
307
308 validate_event_aura_references(
309 &channel.tick_effects,
310 &self.auras,
311 location,
312 &mut self.diagnostics,
313 );
314 validate_event_aura_references(
315 &channel.complete_effects,
316 &self.auras,
317 location,
318 &mut self.diagnostics,
319 );
320 }
321 }
322
323 fn validate_collections(&mut self) {
325 for (talent, companion_auras) in &self.manifest.talent_companion_auras {
326 let location = ManifestDiagnosticLocation::TalentCompanionAuras {
327 talent: talent.clone(),
328 };
329
330 require_reference(
331 &self.talents,
332 talent,
333 ManifestReferenceKind::Talent,
334 &location,
335 &mut self.diagnostics,
336 );
337
338 for aura in companion_auras {
339 require_reference(
340 &self.auras,
341 aura,
342 ManifestReferenceKind::Aura,
343 &location,
344 &mut self.diagnostics,
345 );
346 }
347 }
348
349 self.validate_talent_gated_aura_effects();
350
351 for (index, group) in self.manifest.spell_groups.iter().enumerate() {
352 let location = ManifestDiagnosticLocation::SpellGroup { index };
353
354 for aura in &group.auras {
355 require_reference(
356 &self.auras,
357 aura,
358 ManifestReferenceKind::Aura,
359 &location,
360 &mut self.diagnostics,
361 );
362 }
363 }
364
365 for (index, proc) in self.manifest.impact_procs.iter().enumerate() {
366 let location = ManifestDiagnosticLocation::ImpactProc { index };
367
368 if let Some(spell) = &proc.spell {
369 require_reference(
370 &self.spells,
371 spell,
372 ManifestReferenceKind::Spell,
373 &location,
374 &mut self.diagnostics,
375 );
376 }
377
378 if let Some(talent) = &proc.talent {
379 require_reference(
380 &self.talents,
381 talent,
382 ManifestReferenceKind::Talent,
383 &location,
384 &mut self.diagnostics,
385 );
386 }
387
388 if let Some(hand) = &proc.auto_attack_hand {
389 if hand != "mainhand" && hand != "offhand" {
390 self.diagnostics
391 .push(ManifestDiagnostic::UnknownAutoAttackHand {
392 location: location.clone(),
393 hand: hand.clone(),
394 });
395 }
396 }
397
398 let source_count = [
399 proc.spell.is_some(),
400 proc.auto_attack_hand.is_some(),
401 proc.proc_type_mask_spell_id.is_some(),
402 ]
403 .into_iter()
404 .filter(|present| *present)
405 .count();
406
407 if source_count != 1 {
408 self.diagnostics
409 .push(ManifestDiagnostic::InvalidImpactProcSource {
410 location: location.clone(),
411 });
412 }
413
414 validate_impact_proc_driver_coordinate(proc, &location, &mut self.diagnostics);
415
416 if matches!(
417 proc.rng_model,
418 ImpactProcRngDef::Shuffled {
419 success_entries,
420 total_entries,
421 } if total_entries == 0 || success_entries > total_entries
422 ) {
423 self.diagnostics
424 .push(ManifestDiagnostic::InvalidImpactProcRng {
425 location: location.clone(),
426 });
427 }
428
429 validate_event_aura_references(
430 &proc.effects,
431 &self.auras,
432 &location,
433 &mut self.diagnostics,
434 );
435 }
436 }
437
438 fn validate_talent_gated_aura_effects(&mut self) {
439 for (index, gate) in self.manifest.talent_gated_aura_effects.iter().enumerate() {
440 let location = ManifestDiagnosticLocation::TalentGatedAuraEffect { index };
441
442 require_reference(
443 &self.talents,
444 &gate.talent,
445 ManifestReferenceKind::Talent,
446 &location,
447 &mut self.diagnostics,
448 );
449
450 if let Some(target_aura) = &gate.target_aura {
451 require_reference(
452 &self.auras,
453 target_aura,
454 ManifestReferenceKind::Aura,
455 &location,
456 &mut self.diagnostics,
457 );
458 }
459
460 if !self
461 .manifest
462 .auras
463 .values()
464 .any(|aura| aura.id == gate.source.spell_id)
465 {
466 self.diagnostics
467 .push(ManifestDiagnostic::UnknownAuraEffectSource {
468 location: ManifestDiagnosticLocation::TalentGatedAuraEffect { index },
469 spell_id: gate.source.spell_id,
470 });
471 }
472
473 if gate.source.effect == 0 {
474 self.diagnostics
475 .push(ManifestDiagnostic::InvalidAuraEffectIndex {
476 location: ManifestDiagnosticLocation::TalentGatedAuraEffect { index },
477 });
478 }
479 }
480 }
481}
482
483fn validate_impact_proc_driver_coordinate(
484 proc: &ImpactProcDef,
485 location: &ManifestDiagnosticLocation,
486 diagnostics: &mut Vec<ManifestDiagnostic>,
487) {
488 if matches!(
489 (proc.proc_type_mask_spell_id, proc.proc_type_mask_effect),
490 (None, None) | (Some(_), Some(1..))
491 ) {
492 return;
493 }
494
495 diagnostics.push(ManifestDiagnostic::InvalidImpactProcDriverCoordinate {
496 location: location.clone(),
497 });
498}
499
500fn require_reference(
501 available: &IndexSet<&str>,
502 reference: &str,
503 kind: ManifestReferenceKind,
504 location: &ManifestDiagnosticLocation,
505 diagnostics: &mut Vec<ManifestDiagnostic>,
506) {
507 if !available.contains(reference) {
508 diagnostics.push(ManifestDiagnostic::MissingReference {
509 location: location.clone(),
510 kind,
511 reference: reference.to_owned(),
512 });
513 }
514}
515
516fn validate_event_aura_references(
517 effects: &[EventEffectDef],
518 auras: &IndexSet<&str>,
519 location: &ManifestDiagnosticLocation,
520 diagnostics: &mut Vec<ManifestDiagnostic>,
521) {
522 for effect in effects {
523 if let EventEffectDef::ExtendAura { aura, .. } = effect {
524 require_reference(
525 auras,
526 aura,
527 ManifestReferenceKind::Aura,
528 location,
529 diagnostics,
530 );
531 }
532 }
533}