wowlab_engine_application/audit/data_values/
periodic.rs1use wowlab_engine_ports::{DataResolver, DynDataResolver, SpellId};
4use wowlab_manifest_schema::{ManifestAuraDef, ScalarField, ScalarRef};
5use wowlab_types::constants::HUNDRED;
6
7use super::{
8 AuditSink, DATA_EPS,
9 scalar::{resolve_scalar_ref, scalar_field_value},
10};
11
12pub(super) struct PeriodicCoefficientCheck<'a> {
13 pub name: &'a str,
14 pub spell_id: u32,
15 pub field_name: &'a str,
16 pub value: Option<&'a ScalarRef>,
17 pub field: ScalarField,
18}
19
20pub(super) struct AuraScalarCheck<'a> {
21 pub name: &'a str,
22 pub aura_id: u32,
23 pub field_name: &'a str,
24 pub value: &'a ScalarRef,
25 pub field: ScalarField,
26}
27
28pub(super) async fn check_periodic_aura_values(
29 resolver: &DynDataResolver<'_>,
30 name: &str,
31 aura: &ManifestAuraDef,
32 sink: &mut AuditSink,
33) {
34 let periodic_scalars = [
35 aura.periodic_hook
36 .as_ref()
37 .map(|value| ("periodic_hook.tick_ms", &value.tick_ms, ScalarField::Period)),
38 aura.periodic_residual_damage.as_ref().map(|value| {
39 (
40 "periodic_residual_damage.tick_ms",
41 &value.tick_ms,
42 ScalarField::Period,
43 )
44 }),
45 aura.periodic_resource.as_ref().map(|value| {
46 (
47 "periodic_resource.tick_ms",
48 &value.tick_ms,
49 ScalarField::Period,
50 )
51 }),
52 aura.periodic_resource_drain.as_ref().map(|value| {
53 (
54 "periodic_resource_drain.tick_ms",
55 &value.tick_ms,
56 ScalarField::Period,
57 )
58 }),
59 aura.periodic_apply_aura.as_ref().map(|value| {
60 (
61 "periodic_apply_aura.tick_ms",
62 &value.tick_ms,
63 ScalarField::Period,
64 )
65 }),
66 aura.periodic_remove_stack.as_ref().map(|value| {
67 (
68 "periodic_remove_stack.tick_ms",
69 &value.tick_ms,
70 ScalarField::Period,
71 )
72 }),
73 ];
74
75 for (field_name, value, field) in periodic_scalars.into_iter().flatten() {
76 check_redundant_aura_scalar(
77 resolver,
78 AuraScalarCheck {
79 name,
80 aura_id: aura.id,
81 field_name,
82 value,
83 field,
84 },
85 sink,
86 )
87 .await;
88 }
89
90 let amounts = [
91 aura.periodic_resource
92 .as_ref()
93 .map(|periodic| ("periodic_resource.amount", &periodic.amount)),
94 aura.periodic_resource_drain
95 .as_ref()
96 .map(|periodic| ("periodic_resource_drain.amount", &periodic.amount)),
97 ];
98
99 for (field_name, value) in amounts.into_iter().flatten() {
100 check_redundant_aura_scalar(
101 resolver,
102 AuraScalarCheck {
103 name,
104 aura_id: aura.id,
105 field_name,
106 value,
107 field: ScalarField::BasePoints,
108 },
109 sink,
110 )
111 .await;
112 }
113}
114
115pub(super) async fn check_redundant_aura_scalar(
116 resolver: &DynDataResolver<'_>,
117 check: AuraScalarCheck<'_>,
118 sink: &mut AuditSink,
119) {
120 let ScalarRef::Literal(literal) = check.value else {
121 return;
122 };
123 let effects = resolver
124 .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
125 check.aura_id,
126 )))
127 .await
128 .unwrap_or_default();
129
130 if effects.iter().any(|effect| {
131 let resolved = scalar_field_value(effect, check.field);
132
133 resolved.abs() > DATA_EPS && (*literal - resolved).abs() <= DATA_EPS
134 }) {
135 sink.warning(format!(
136 "aura {} {}={literal} is a redundant override; use a ScalarRef",
137 check.name, check.field_name
138 ));
139 }
140}
141
142pub(super) async fn check_redundant_damage_mult(
143 resolver: &DynDataResolver<'_>,
144 name: &str,
145 aura_id: u32,
146 value: Option<&ScalarRef>,
147 sink: &mut AuditSink,
148) {
149 check_resolved_scalar(resolver, name, "damage_mult", value, sink).await;
150 let Some(ScalarRef::Literal(literal)) = value else {
151 return;
152 };
153 let effects = resolver
154 .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
155 aura_id,
156 )))
157 .await
158 .unwrap_or_default();
159
160 if effects.iter().any(|effect| {
161 effect.base_points.abs() > DATA_EPS
162 && (*literal - (1.0 + effect.base_points / HUNDRED)).abs() <= DATA_EPS
163 }) {
164 sink.warning(format!(
165 "aura {name} damage_mult={literal} is a redundant override; use an affine ScalarRef"
166 ));
167 }
168}
169
170pub(super) async fn check_redundant_periodic_coefficient(
171 resolver: &DynDataResolver<'_>,
172 check: PeriodicCoefficientCheck<'_>,
173 sink: &mut AuditSink,
174) {
175 check_resolved_scalar(resolver, check.name, check.field_name, check.value, sink).await;
176 let Some(ScalarRef::Literal(literal)) = check.value else {
177 return;
178 };
179 let effects = resolver
180 .get_spell_effects(SpellId::new(wowlab_types::numeric::u32_to_i32_saturating(
181 check.spell_id,
182 )))
183 .await
184 .unwrap_or_default();
185
186 if effects.iter().any(|effect| {
187 let resolved = scalar_field_value(effect, check.field);
188
189 resolved.abs() > DATA_EPS && (*literal - resolved).abs() <= DATA_EPS
190 }) {
191 sink.warning(format!(
192 "aura {} periodic_damage.{}={literal} is a redundant override; use a ScalarRef",
193 check.name, check.field_name
194 ));
195 }
196}
197
198async fn check_resolved_scalar(
199 resolver: &DynDataResolver<'_>,
200 name: &str,
201 field_name: &str,
202 value: Option<&ScalarRef>,
203 sink: &mut AuditSink,
204) {
205 let resolved = resolve_scalar_ref(resolver, value).await;
206
207 if resolved.is_some_and(|resolved| !resolved.is_finite()) {
208 sink.warning(format!(
209 "aura {name} {field_name} resolves to a non-finite value"
210 ));
211 }
212}