Skip to main content

wowlab_engine_application/audit/
checks.rs

1// #t(file: rust_clone_in_loop, rust_alloc_in_loop) audit clones name strings for per-issue diagnostics, bounded by manifest size
2
3//! ID and reference audit checks: verify manifest spell/aura/talent IDs exist and cross-references resolve.
4
5use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
6use wowlab_manifest_schema::{ManifestDamageDef, ScalarRef};
7
8use super::{AuditCtx, AuditIssue, AuditSink};
9
10/// `auto_attacks` entries with this id (or below) are placeholders, not real spells to verify.
11const 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}