Skip to main content

wowlab_engine_application/audit/
data_values.rs

1//! Data-value checks: compare manifest literals (scalars, resource values) against resolved game data.
2
3mod periodic;
4mod resources;
5mod scalar;
6
7#[cfg(test)]
8use periodic::{AuraScalarCheck, check_redundant_aura_scalar};
9use periodic::{
10    PeriodicCoefficientCheck, check_periodic_aura_values, check_redundant_damage_mult,
11    check_redundant_periodic_coefficient,
12};
13use resources::{check_resource_values, check_secondary_resource_values};
14#[cfg(test)]
15use scalar::resolved_scalar_value;
16use scalar::{check_redundant_resolved_override, check_scalar_literal, warn_if_redundant};
17use wowlab_engine_domain::dbc::{PowerModifiers, cast_time_ms, primary_cost};
18use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
19use wowlab_manifest_schema::{ManifestAuraDef, ScalarField, ScalarRef};
20#[cfg(test)]
21use wowlab_manifest_schema::{ResourceSection, SecondaryResourceSection};
22#[cfg(test)]
23use wowlab_types::data::SpellEffect;
24use wowlab_types::{constants::MS_PER_SECOND, sim::FastSet};
25
26use super::{AuditCtx, AuditSink};
27
28/// Tolerance for comparing manifest literals against game data (f32 DBC vs f64 literal).
29pub(super) const DATA_EPS: f64 = 1e-6;
30
31struct CoreScalarCheck<'a> {
32    entity: &'static str,
33    name: &'a str,
34    owner_id: u32,
35    field: &'a str,
36    value: Option<&'a ScalarRef>,
37    resolved: f64,
38}
39
40// #t(fn: rust_max_fn_lines) the audit intentionally traverses aura and spell scalar families in one pass
41pub(super) async fn check_data_values(ctx: &mut AuditCtx<'_>) {
42    let manifest = ctx.manifest;
43    let resolver = ctx.resolver;
44    let sink = &mut *ctx.sink;
45
46    for (name, aura) in &manifest.auras {
47        if aura.id == 0 {
48            continue;
49        }
50
51        check_scalar_literal(
52            resolver,
53            name,
54            aura.id,
55            "haste_per_stack",
56            aura.haste_per_stack.as_ref(),
57            sink,
58        )
59        .await;
60        check_scalar_literal(
61            resolver,
62            name,
63            aura.id,
64            "crit_per_stack",
65            aura.crit_per_stack.as_ref(),
66            sink,
67        )
68        .await;
69        check_redundant_damage_mult(resolver, name, aura.id, aura.damage_mult.as_ref(), sink).await;
70        check_core_aura_overrides(resolver, name, aura, sink).await;
71
72        if let Some(periodic) = &aura.periodic_damage {
73            let source_id = periodic.spell_id.unwrap_or(aura.id);
74
75            check_redundant_periodic_coefficient(
76                resolver,
77                PeriodicCoefficientCheck {
78                    name,
79                    spell_id: source_id,
80                    field_name: "ap_coef",
81                    value: periodic.ap_coef.as_ref(),
82                    field: ScalarField::ApCoef,
83                },
84                sink,
85            )
86            .await;
87            check_redundant_periodic_coefficient(
88                resolver,
89                PeriodicCoefficientCheck {
90                    name,
91                    spell_id: source_id,
92                    field_name: "sp_coef",
93                    value: periodic.sp_coef.as_ref(),
94                    field: ScalarField::SpCoef,
95                },
96                sink,
97            )
98            .await;
99        }
100
101        check_periodic_aura_values(resolver, name, aura, sink).await;
102    }
103
104    let power_types = resolver.get_power_types().await.unwrap_or_default();
105
106    check_resource_values(&manifest.resource, &power_types, sink);
107
108    if let Some(sr) = &manifest.secondary_resource {
109        check_secondary_resource_values(sr, &power_types, sink);
110    }
111
112    let modifiers = PowerModifiers::from_pairs(
113        power_types
114            .iter()
115            .map(|power| (power.power_type_enum, f64::from(power.display_modifier))),
116    );
117
118    for (name, spell) in &manifest.spells {
119        if spell.id == 0 {
120            continue;
121        }
122
123        let Ok(data) = resolver
124            .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
125                spell.id,
126            )))
127            .await
128        else {
129            continue;
130        };
131
132        check_core_scalar(
133            resolver,
134            CoreScalarCheck {
135                entity: "spell",
136                name,
137                owner_id: spell.id,
138                field: "cost",
139                value: spell.cost.as_ref(),
140                resolved: primary_cost(
141                    &data,
142                    i32::from(manifest.resource.type_id),
143                    &modifiers,
144                    &FastSet::default(),
145                )
146                .0,
147            },
148            sink,
149        )
150        .await;
151        check_core_scalar(
152            resolver,
153            CoreScalarCheck {
154                entity: "spell",
155                name,
156                owner_id: spell.id,
157                field: "cooldown",
158                value: spell.cooldown.as_ref(),
159                resolved: f64::from(data.recovery_time.max(0)) / MS_PER_SECOND,
160            },
161            sink,
162        )
163        .await;
164        check_core_scalar(
165            resolver,
166            CoreScalarCheck {
167                entity: "spell",
168                name,
169                owner_id: spell.id,
170                field: "cast_time_ms",
171                value: spell.cast_time_ms.as_ref(),
172                resolved: f64::from(cast_time_ms(&data)),
173            },
174            sink,
175        )
176        .await;
177        check_core_scalar(
178            resolver,
179            CoreScalarCheck {
180                entity: "spell",
181                name,
182                owner_id: spell.id,
183                field: "charges",
184                value: spell.charges.as_ref(),
185                resolved: f64::from(data.max_charges),
186            },
187            sink,
188        )
189        .await;
190        check_core_scalar(
191            resolver,
192            CoreScalarCheck {
193                entity: "spell",
194                name,
195                owner_id: spell.id,
196                field: "charge_cd",
197                value: spell.charge_cd.as_ref(),
198                resolved: f64::from(data.charge_recovery_time) / MS_PER_SECOND,
199            },
200            sink,
201        )
202        .await;
203    }
204}
205
206async fn check_core_aura_overrides(
207    resolver: &DynDataResolver<'_>,
208    name: &str,
209    aura: &ManifestAuraDef,
210    sink: &mut AuditSink,
211) {
212    let Ok(data) = resolver
213        .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
214            aura.id,
215        )))
216        .await
217    else {
218        return;
219    };
220    let duration = if data.duration < 0 {
221        0.0
222    } else {
223        f64::from(data.duration)
224    };
225
226    check_core_scalar(
227        resolver,
228        CoreScalarCheck {
229            entity: "aura",
230            name,
231            owner_id: aura.id,
232            field: "duration_ms",
233            value: aura.duration_ms.as_ref(),
234            resolved: duration,
235        },
236        sink,
237    )
238    .await;
239    check_core_scalar(
240        resolver,
241        CoreScalarCheck {
242            entity: "aura",
243            name,
244            owner_id: aura.id,
245            field: "max_stacks",
246            value: aura.max_stacks.as_ref(),
247            resolved: f64::from(data.max_stacks),
248        },
249        sink,
250    )
251    .await;
252}
253
254async fn check_core_scalar(
255    resolver: &DynDataResolver<'_>,
256    check: CoreScalarCheck<'_>,
257    sink: &mut AuditSink,
258) {
259    let label = format!("{} {} {}", check.entity, check.name, check.field);
260
261    match check.value {
262        Some(ScalarRef::Literal(literal)) => {
263            warn_if_redundant(*literal, check.resolved, &label, sink);
264        }
265        Some(value) => {
266            check_redundant_resolved_override(
267                resolver,
268                value,
269                check.owner_id,
270                check.resolved,
271                &label,
272                sink,
273            )
274            .await;
275        }
276        None => {}
277    }
278}
279
280#[cfg(test)]
281#[path = "data_values/scalar_tests/tests.rs"]
282mod scalar_tests;
283#[cfg(test)]
284mod tests;
285#[cfg(test)]
286#[path = "data_values/transform_tests/tests.rs"]
287mod transform_tests;