wowlab_engine_application/audit/
data_values.rs1mod 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
28pub(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
40pub(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;