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