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        {
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}