Skip to main content

wowlab_engine_application/audit/data_values/
scalar.rs

1//! Scalar-reference resolution and redundant-override comparison.
2
3use wowlab_engine_domain::dbc::cast_time_ms;
4use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
5use wowlab_manifest_schema::{ScalarField, ScalarRef, SpellScalarField};
6use wowlab_types::{
7    constants::MS_PER_SECOND,
8    data::{SpellDataFlat, SpellEffect},
9    numeric::round_to_decimals,
10};
11
12use super::{AuditSink, DATA_EPS};
13
14pub(super) fn warn_if_redundant(literal: f64, resolved: f64, label: &str, sink: &mut AuditSink) {
15    if (literal - resolved).abs() <= DATA_EPS {
16        sink.warning(format!(
17            "{label}={literal} is a redundant override; game data resolves {resolved}"
18        ));
19    }
20}
21
22pub(super) async fn check_redundant_resolved_override(
23    resolver: &DynDataResolver<'_>,
24    value: &ScalarRef,
25    owner_id: u32,
26    target_resolved: f64,
27    label: &str,
28    sink: &mut AuditSink,
29) {
30    let Some(resolved) = resolve_scalar_ref(resolver, Some(value)).await else {
31        return;
32    };
33
34    if !resolved.is_finite() {
35        sink.warning(format!("{label} resolves to a non-finite value"));
36
37        return;
38    }
39
40    if value.effect_spell_id() != Some(owner_id) {
41        return;
42    }
43
44    warn_if_redundant(resolved, target_resolved, label, sink);
45}
46
47pub(super) async fn resolve_scalar_ref(
48    resolver: &DynDataResolver<'_>,
49    value: Option<&ScalarRef>,
50) -> Option<f64> {
51    match value {
52        Some(
53            value @ ScalarRef::EffectRef {
54                spell_id, effect, ..
55            },
56        ) => {
57            let Ok(effect_data) = resolver
58                .get_spell_effect(
59                    SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(*spell_id)),
60                    *effect,
61                )
62                .await
63            else {
64                return None;
65            };
66
67            resolved_scalar_value(value, &effect_data)
68        }
69        Some(value @ ScalarRef::SpellRef { spell_id, .. }) => {
70            let Ok(spell_data) = resolver
71                .get_spell(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
72                    *spell_id,
73                )))
74                .await
75            else {
76                return None;
77            };
78
79            resolved_spell_scalar_value(value, &spell_data)
80        }
81        _ => None,
82    }
83}
84
85pub(super) fn scalar_field_value(effect: &SpellEffect, field: ScalarField) -> f64 {
86    match field {
87        ScalarField::BasePoints => effect.base_points,
88        ScalarField::ApCoef => effect.bonus_coefficient_from_ap,
89        ScalarField::SpCoef => effect.bonus_coefficient,
90        ScalarField::Amplitude => f64::from(effect.amplitude),
91        ScalarField::Period => f64::from(effect.period),
92        ScalarField::Coefficient => f64::from(effect.coefficient),
93        _ => 0.0,
94    }
95}
96
97pub(super) fn resolved_scalar_value(value: &ScalarRef, effect: &SpellEffect) -> Option<f64> {
98    let ScalarRef::EffectRef {
99        field,
100        multiplier,
101        offset,
102        round_decimals,
103        ..
104    } = value
105    else {
106        return None;
107    };
108
109    Some(apply_scalar_transform(
110        scalar_field_value(effect, *field),
111        *multiplier,
112        *offset,
113        *round_decimals,
114    ))
115}
116
117fn resolved_spell_scalar_value(value: &ScalarRef, spell: &SpellDataFlat) -> Option<f64> {
118    let ScalarRef::SpellRef {
119        field,
120        multiplier,
121        offset,
122        round_decimals,
123        ..
124    } = value
125    else {
126        return None;
127    };
128    let source = match field {
129        SpellScalarField::DurationMs => f64::from(spell.duration.max(0)),
130        SpellScalarField::MaxStacks => f64::from(spell.max_stacks),
131        SpellScalarField::Cooldown => f64::from(spell.recovery_time.max(0)) / MS_PER_SECOND,
132        SpellScalarField::Cost => 0.0,
133        SpellScalarField::CastTimeMs => f64::from(cast_time_ms(spell)),
134        SpellScalarField::Charges => f64::from(spell.max_charges),
135        SpellScalarField::ChargeCd => f64::from(spell.charge_recovery_time) / MS_PER_SECOND,
136        SpellScalarField::GcdMs => f64::from(spell.start_recovery_time),
137        SpellScalarField::ProcCategoryRecoveryMs => {
138            f64::from(spell.proc_category_recovery_ms.max(0))
139        }
140        _ => return None,
141    };
142
143    Some(apply_scalar_transform(
144        source,
145        *multiplier,
146        *offset,
147        *round_decimals,
148    ))
149}
150
151fn apply_scalar_transform(
152    mut resolved: f64,
153    multiplier: Option<f64>,
154    offset: Option<f64>,
155    round_decimals: Option<u8>,
156) -> f64 {
157    resolved *= multiplier.unwrap_or(1.0);
158    resolved += offset.unwrap_or(0.0);
159
160    if let Some(decimals) = round_decimals {
161        resolved = round_to_decimals(resolved, decimals);
162    }
163
164    resolved
165}
166
167pub(super) async fn check_scalar_literal(
168    resolver: &DynDataResolver<'_>,
169    name: &str,
170    aura_id: u32,
171    field: &str,
172    value: Option<&ScalarRef>,
173    sink: &mut AuditSink,
174) {
175    let Some(ScalarRef::Literal(literal)) = value else {
176        return;
177    };
178    let effects = resolver
179        .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
180            aura_id,
181        )))
182        .await
183        .unwrap_or_default();
184
185    if effects
186        .iter()
187        .any(|effect| (effect.base_points - *literal).abs() < DATA_EPS)
188    {
189        return;
190    }
191
192    let base_points = effects
193        .iter()
194        .map(|effect| effect.base_points.to_string())
195        .collect::<Vec<_>>()
196        .join(", ");
197
198    sink.warning(format!(
199        "aura {name} {field}={literal} not found in game data (effects base_points: [{base_points}])"
200    ));
201}