wowlab_engine_application/audit/
checks.rs1use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
6use wowlab_manifest_schema::{ManifestDamageDef, ScalarRef};
7
8use super::{AuditCtx, AuditIssue, AuditSink};
9
10const AUTO_ATTACK_PLACEHOLDER_ID: u32 = 1;
12
13pub(super) async fn audit_spells(ctx: &mut AuditCtx<'_>) {
14 let manifest = ctx.manifest;
15 let resolver = ctx.resolver;
16 let sink = &mut *ctx.sink;
17
18 for (name, spell) in &manifest.spells {
19 if !warn_zero_id("spell", name, spell.id, sink) {
20 continue;
21 }
22
23 check_spell_exists(resolver, "spell", name, spell.id, sink).await;
24
25 if let Some(ManifestDamageDef::EffectRef {
26 spell_id, effect, ..
27 }) = &spell.damage
28 {
29 if *spell_id != 0
30 && resolver
31 .get_spell_effect(
32 SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(*spell_id)),
33 *effect,
34 )
35 .await
36 .is_err()
37 {
38 sink.error(AuditIssue {
39 category: "spell",
40 name: name.clone(),
41 id: spell.id,
42 message: format!("damage ref spell_id={spell_id} effect={effect} not found"),
43 });
44 }
45 }
46
47 for scalar in spell.scalar_refs() {
48 audit_scalar_ref(resolver, scalar, "spell", name, spell.id, sink).await;
49 }
50 }
51}
52
53pub(super) async fn audit_auras(ctx: &mut AuditCtx<'_>) {
54 let manifest = ctx.manifest;
55 let resolver = ctx.resolver;
56 let sink = &mut *ctx.sink;
57
58 for (name, aura) in &manifest.auras {
59 if !warn_zero_id("aura", name, aura.id, sink) {
60 continue;
61 }
62
63 check_spell_exists(resolver, "aura", name, aura.id, sink).await;
64
65 for scalar in aura.scalar_refs() {
66 audit_scalar_ref(resolver, scalar, "aura", name, aura.id, sink).await;
67 }
68 }
69}
70
71async fn audit_scalar_ref(
72 resolver: &DynDataResolver<'_>,
73 scalar: &ScalarRef,
74 category: &'static str,
75 name: &str,
76 id: u32,
77 sink: &mut AuditSink,
78) {
79 let message = match scalar {
80 ScalarRef::EffectRef {
81 spell_id, effect, ..
82 } if *spell_id != 0
83 && resolver
84 .get_spell_effect(
85 SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(*spell_id)),
86 *effect,
87 )
88 .await
89 .is_err() =>
90 {
91 Some(format!(
92 "scalar ref spell_id={spell_id} effect={effect} not found"
93 ))
94 }
95 ScalarRef::SpellRef { spell_id, .. }
96 if *spell_id != 0
97 && resolver
98 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
99 *spell_id,
100 )))
101 .await
102 .is_err() =>
103 {
104 Some(format!("scalar ref spell_id={spell_id} not found"))
105 }
106 _ => None,
107 };
108
109 if let Some(message) = message {
110 sink.error(AuditIssue {
111 category,
112 name: name.to_string(),
113 id,
114 message,
115 });
116 }
117}
118
119pub(super) async fn audit_talents(ctx: &mut AuditCtx<'_>) {
120 let manifest = ctx.manifest;
121 let resolver = ctx.resolver;
122 let sink = &mut *ctx.sink;
123
124 for (name, &id) in &manifest.talents {
125 if !warn_zero_id("talent", name, id, sink) {
126 continue;
127 }
128
129 check_spell_exists(resolver, "talent", name, id, sink).await;
130 }
131}
132
133pub(super) async fn audit_auto_attacks(ctx: &mut AuditCtx<'_>) {
134 let manifest = ctx.manifest;
135 let resolver = ctx.resolver;
136 let sink = &mut *ctx.sink;
137
138 for (name, aa) in &manifest.auto_attacks {
139 if aa.spell_id > AUTO_ATTACK_PLACEHOLDER_ID {
140 check_spell_exists(resolver, "auto_attack", name, aa.spell_id, sink).await;
141 }
142 }
143}
144
145pub(super) async fn audit_hero_talents(ctx: &mut AuditCtx<'_>) -> (usize, usize) {
146 let manifest = ctx.manifest;
147 let resolver = ctx.resolver;
148 let sink = &mut *ctx.sink;
149
150 let mut hero_spell_count = 0usize;
151 let mut hero_aura_count = 0usize;
152
153 for (tree_name, tree) in &manifest.hero_talents {
154 for (name, &id) in &tree.spells {
155 hero_spell_count += 1;
156
157 if !warn_zero_id(format!("hero/{tree_name}/spell"), name, id, sink) {
158 continue;
159 }
160
161 check_spell_exists(
162 resolver,
163 "hero_spell",
164 &format!("{tree_name}/{name}"),
165 id,
166 sink,
167 )
168 .await;
169 }
170
171 for (name, &id) in &tree.auras {
172 hero_aura_count += 1;
173
174 if !warn_zero_id(format!("hero/{tree_name}/aura"), name, id, sink) {
175 continue;
176 }
177
178 check_spell_exists(
179 resolver,
180 "hero_aura",
181 &format!("{tree_name}/{name}"),
182 id,
183 sink,
184 )
185 .await;
186 }
187 }
188
189 (hero_spell_count, hero_aura_count)
190}
191
192async fn check_spell_exists(
193 resolver: &DynDataResolver<'_>,
194 category: &'static str,
195 name: &str,
196 id: u32,
197 sink: &mut AuditSink,
198) {
199 if resolver
200 .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
201 id,
202 )))
203 .await
204 .is_err()
205 {
206 sink.error(AuditIssue {
207 category,
208 name: name.to_string(),
209 id,
210 message: "not found in game.spells".into(),
211 });
212 }
213}
214
215#[must_use]
216fn warn_zero_id(kind: impl std::fmt::Display, name: &str, id: u32, sink: &mut AuditSink) -> bool {
217 if id == 0 {
218 sink.warning(format!("{kind} {name} id=0"));
219
220 return false;
221 }
222
223 true
224}