1use wowlab_engine_domain::{
4 dbc::{
5 EffectSemanticCoverage, ImplicitTargetSelector, SemanticSupport, SpellCastTargetFlags,
6 effect_semantic_coverage, spell_attribute_semantic, spell_cast_target_flag_semantic,
7 spell_target_masks,
8 },
9 targeting::{
10 TargetPlanInput, TargetPlanOperation, TargetPlanProvenance, compile_target_plan,
11 lower_target_plan, registered_target_geometry_overlay,
12 },
13};
14use wowlab_manifest_schema::ContentEffectStatus;
15use wowlab_types::game::SpecId;
16
17use super::{
18 AttributeAuditRow, ContentEffectLedger, EffectAuditCounts, EffectAuditRow, TargetFlagAuditRow,
19 TargetPlanAuditRow,
20};
21use crate::talent_target_plan_summary::{
22 TargetPlanAxisFilter, TargetPlanObservation, TargetPlanSummaries,
23};
24
25pub(super) fn audit_spell_attributes(
26 spell_id: i32,
27 spell_name: &str,
28 attributes: &[i32],
29 rows: &mut Vec<AttributeAuditRow>,
30 counts: &mut EffectAuditCounts,
31) {
32 for (block, value) in attributes.iter().copied().enumerate() {
33 for bit in 0..i32::BITS {
34 if value & 1_i32.wrapping_shl(bit) == 0 {
35 continue;
36 }
37
38 let raw = block * i32::BITS as usize + bit as usize;
39 let Ok(attribute) = u16::try_from(raw) else {
40 continue;
41 };
42
43 counts.attributes += 1;
44 let (name, coverage, handler) =
45 if let Some(semantic) = spell_attribute_semantic(i32::from(attribute)) {
46 match semantic.support {
47 SemanticSupport::Generic => {
48 counts.attribute_generic += 1;
49
50 (semantic.name, "generic", semantic.handler)
51 }
52 SemanticSupport::Partial | SemanticSupport::ContentRequired => {
53 counts.attribute_partial += 1;
54
55 (semantic.name, "partial", semantic.handler)
56 }
57 SemanticSupport::Ignored => {
58 counts.attribute_ignored += 1;
59
60 (semantic.name, "ignored", semantic.handler)
61 }
62 SemanticSupport::Unsupported => {
63 counts.attribute_unsupported += 1;
64
65 (semantic.name, "unsupported", semantic.handler)
66 }
67 _ => {
68 counts.attribute_unsupported += 1;
69
70 (
71 semantic.name,
72 "unsupported",
73 "unrecognized support classification",
74 )
75 }
76 }
77 } else {
78 counts.attribute_unsupported += 1;
79
80 (
81 "Unregistered",
82 "unsupported",
83 "spell attribute is absent from registry",
84 )
85 };
86
87 rows.push(AttributeAuditRow {
88 spell: format!("{spell_name} ({spell_id})"),
89 attribute,
90 name,
91 coverage,
92 handler,
93 });
94 }
95 }
96}
97
98pub(super) struct TargetPlanAuditContext<'a> {
99 pub(super) spec: SpecId,
100 pub(super) rows: &'a mut Vec<TargetPlanAuditRow>,
101 pub(super) counts: &'a mut EffectAuditCounts,
102 pub(super) filter: &'a [TargetPlanAxisFilter],
103 pub(super) summaries: &'a mut TargetPlanSummaries,
104}
105
106pub(super) fn audit_effect_target_plan(
107 spell_id: i32,
108 spell_name: &str,
109 effect: &wowlab_types::data::SpellEffect,
110 context: &mut TargetPlanAuditContext<'_>,
111) {
112 if ImplicitTargetSelector::try_from(effect.implicit_target_a).ok()
113 == Some(ImplicitTargetSelector::None)
114 && ImplicitTargetSelector::try_from(effect.implicit_target_b).ok()
115 == Some(ImplicitTargetSelector::None)
116 {
117 return;
118 }
119
120 context.counts.target_plans += 1;
121 let effect_index = effect.index + 1;
122 let (Ok(target_spell_id), Ok(target_effect_index)) =
123 (u32::try_from(spell_id), u8::try_from(effect_index))
124 else {
125 context.counts.target_plan_unsupported += 1;
126 context.rows.push(TargetPlanAuditRow {
127 spell: format!("{spell_name} ({spell_id})"),
128 effect_index,
129 selectors: format!(
130 "{} / {}",
131 effect.implicit_target_a, effect.implicit_target_b
132 ),
133 program: "compile failed".to_string(),
134 provenance: "DBC".to_string(),
135 coverage: "unsupported",
136 handler: "spell/effect coordinate is outside the engine index range".to_string(),
137 });
138
139 return;
140 };
141 let input = TargetPlanInput {
142 spell_id: target_spell_id,
143 effect_index: target_effect_index,
144 effect_type: effect.effect,
145 target_a: effect.implicit_target_a,
146 target_b: effect.implicit_target_b,
147 };
148 let geometry_overlay =
149 match registered_target_geometry_overlay(target_spell_id, target_effect_index) {
150 Ok(overlay) => overlay,
151 Err(error) => {
152 context.counts.target_plan_unsupported += 1;
153 context.rows.push(TargetPlanAuditRow {
154 spell: format!("{spell_name} ({spell_id})"),
155 effect_index,
156 selectors: format!(
157 "{} / {}",
158 effect.implicit_target_a, effect.implicit_target_b
159 ),
160 program: "compile failed".to_string(),
161 provenance: "geometry overlay".to_string(),
162 coverage: "unsupported",
163 handler: error.to_string(),
164 });
165
166 return;
167 }
168 };
169 let (program, provenance, coverage, handler) = match compile_target_plan(input) {
170 Ok(plan) => {
171 let program = plan
172 .operations
173 .into_array()
174 .into_iter()
175 .flatten()
176 .map(target_operation_label)
177 .collect::<Vec<_>>()
178 .join(" -> ");
179 let (mut provenance, plan_reason) = match plan.provenance {
180 TargetPlanProvenance::Dbc { .. } => ("DBC".to_string(), None),
181 TargetPlanProvenance::Overlay { name, reason } => {
182 (format!("selector overlay: {name}"), Some(reason))
183 }
184 _ => ("unknown".to_string(), None),
185 };
186
187 if let Some(geometry) = geometry_overlay {
188 provenance.push_str(" + geometry overlay: ");
189 provenance.push_str(geometry.name);
190 }
191
192 let overlaid = plan_reason.is_some() || geometry_overlay.is_some();
193
194 match lower_target_plan(&plan) {
195 Ok(_) if overlaid => {
196 context.counts.target_plan_overlaid += 1;
197 let handler = [
198 plan_reason,
199 geometry_overlay.map(|geometry| geometry.reason),
200 ]
201 .into_iter()
202 .flatten()
203 .collect::<Vec<_>>()
204 .join("; ");
205
206 (program, provenance, "content overlay", handler)
207 }
208 Ok(_) => {
209 context.counts.target_plan_generic += 1;
210
211 (
212 program,
213 provenance,
214 "generic",
215 "all program axes are supported".to_string(),
216 )
217 }
218 Err(gap) => {
219 context.counts.target_plan_unsupported += 1;
220 context.summaries.observe(
221 TargetPlanObservation {
222 spec: context.spec,
223 spell_id,
224 spell_name,
225 effect_index,
226 target_a: effect.implicit_target_a,
227 target_b: effect.implicit_target_b,
228 },
229 gap,
230 context.filter,
231 );
232
233 (program, provenance, "unsupported", gap.to_string())
234 }
235 }
236 }
237 Err(error) => {
238 context.counts.target_plan_unsupported += 1;
239
240 (
241 "compile failed".to_string(),
242 "DBC".to_string(),
243 "unsupported",
244 error.to_string(),
245 )
246 }
247 };
248
249 context.rows.push(TargetPlanAuditRow {
250 spell: format!("{spell_name} ({spell_id})"),
251 effect_index,
252 selectors: format!(
253 "{} / {}",
254 effect.implicit_target_a, effect.implicit_target_b
255 ),
256 program,
257 provenance,
258 coverage,
259 handler,
260 });
261}
262
263fn target_operation_label(operation: TargetPlanOperation) -> String {
264 let (name, selector) = match operation {
265 TargetPlanOperation::AssignSource(selector) => ("source", selector),
266 TargetPlanOperation::AssignDestination(selector) => ("destination", selector),
267 TargetPlanOperation::Select(selector) => ("select", selector),
268 TargetPlanOperation::SelectAndAssignDestination(selector) => {
269 ("select+destination", selector)
270 }
271 _ => return "unknown".to_string(),
272 };
273
274 selector.raw.map_or_else(
275 || format!("{name}(overlay)"),
276 |raw| format!("{name}({raw})"),
277 )
278}
279
280pub(super) fn audit_spell_target_flags(
281 spell_id: i32,
282 spell_name: &str,
283 spell: &wowlab_types::data::SpellDataFlat,
284 rows: &mut Vec<TargetFlagAuditRow>,
285 counts: &mut EffectAuditCounts,
286) {
287 let masks = spell_target_masks(spell);
288
289 for bit in 0..u32::BITS {
290 let flag = SpellCastTargetFlags::from_bits_retain(1 << bit);
291
292 if !masks.explicit.contains(flag) {
293 continue;
294 }
295
296 counts.target_flags += 1;
297 let (name, coverage, handler) =
298 if let Some(semantic) = spell_cast_target_flag_semantic(flag) {
299 match semantic.support {
300 SemanticSupport::Generic => {
301 counts.target_flag_generic += 1;
302
303 (semantic.name, "generic", semantic.handler)
304 }
305 SemanticSupport::Partial | SemanticSupport::ContentRequired => {
306 counts.target_flag_partial += 1;
307
308 (semantic.name, "partial", semantic.handler)
309 }
310 SemanticSupport::Ignored => {
311 counts.target_flag_ignored += 1;
312
313 (semantic.name, "ignored", semantic.handler)
314 }
315 SemanticSupport::Unsupported => {
316 counts.target_flag_unsupported += 1;
317
318 (semantic.name, "unsupported", semantic.handler)
319 }
320 _ => {
321 counts.target_flag_unsupported += 1;
322
323 (
324 semantic.name,
325 "unsupported",
326 "unrecognized support classification",
327 )
328 }
329 }
330 } else {
331 counts.target_flag_unsupported += 1;
332
333 (
334 "Unregistered",
335 "unsupported",
336 "cast-target bit is absent from the canonical registry",
337 )
338 };
339
340 rows.push(TargetFlagAuditRow {
341 spell: format!("{spell_name} ({spell_id})"),
342 bit,
343 name,
344 required: masks.required.contains(flag),
345 coverage,
346 handler,
347 });
348 }
349}
350
351pub(super) fn effect_audit_row(
352 spell_id: i32,
353 spell_name: &str,
354 effect: &wowlab_types::data::SpellEffect,
355 content_effects: &ContentEffectLedger<'_>,
356 counts: &mut EffectAuditCounts,
357) -> EffectAuditRow {
358 let effect_index = effect.index + 1;
359 let (coverage, handler) = match effect_semantic_coverage(effect) {
360 EffectSemanticCoverage::Supported {
361 effect,
362 aura,
363 property,
364 } => {
365 counts.generic += 1;
366 let handler = property.map_or_else(
367 || aura.map_or(effect.handler, |semantic| semantic.handler),
368 |semantic| semantic.handler,
369 );
370
371 ("generic", handler.to_string())
372 }
373 EffectSemanticCoverage::Partial {
374 effect,
375 aura,
376 property,
377 } => {
378 counts.partial += 1;
379 let handler = property.map_or_else(
380 || aura.map_or(effect.handler, |semantic| semantic.handler),
381 |semantic| semantic.handler,
382 );
383
384 ("partial", handler.to_string())
385 }
386 EffectSemanticCoverage::ContentRequired {
387 effect,
388 aura,
389 property,
390 } => {
391 counts.content_required += 1;
392
393 if let Some(declaration) = content_effects.get(spell_id, effect_index) {
394 match declaration.status {
395 ContentEffectStatus::Modeled => {
396 counts.content_modeled += 1;
397
398 (
399 "content modeled",
400 format!("{}: {}", declaration.name, declaration.reason),
401 )
402 }
403 ContentEffectStatus::Ignored => {
404 counts.content_ignored += 1;
405
406 (
407 "content ignored",
408 format!("{}: {}", declaration.name, declaration.reason),
409 )
410 }
411 _ => {
412 counts.content_undeclared += 1;
413
414 (
415 "content undeclared",
416 "unrecognized content-effect status".to_string(),
417 )
418 }
419 }
420 } else {
421 counts.content_undeclared += 1;
422 let handler = property.map_or_else(
423 || aura.map_or(effect.handler, |semantic| semantic.handler),
424 |semantic| semantic.handler,
425 );
426
427 (
428 "content undeclared",
429 format!("missing [content_effects] declaration ({handler})"),
430 )
431 }
432 }
433 EffectSemanticCoverage::Ignored {
434 effect,
435 aura,
436 property,
437 } => {
438 counts.ignored += 1;
439 let handler = property.map_or_else(
440 || aura.map_or(effect.handler, |semantic| semantic.handler),
441 |semantic| semantic.handler,
442 );
443
444 ("ignored", handler.to_string())
445 }
446 EffectSemanticCoverage::UnsupportedRegistered {
447 effect,
448 aura,
449 property,
450 } => {
451 counts.unsupported += 1;
452 let handler = property.map_or_else(
453 || aura.map_or(effect.handler, |semantic| semantic.handler),
454 |semantic| semantic.handler,
455 );
456
457 ("unsupported", handler.to_string())
458 }
459 EffectSemanticCoverage::UnsupportedEffectType(raw) => {
460 counts.unsupported += 1;
461
462 ("unsupported", format!("effect type {raw}"))
463 }
464 EffectSemanticCoverage::UnsupportedAuraSubtype(raw) => {
465 counts.unsupported += 1;
466
467 ("unsupported", format!("aura subtype {raw}"))
468 }
469 EffectSemanticCoverage::UnsupportedModifierProperty(raw) => {
470 counts.unsupported += 1;
471
472 ("unsupported", format!("modifier property {raw}"))
473 }
474 EffectSemanticCoverage::UnsupportedModifierCombination { aura, property } => {
475 counts.unsupported += 1;
476
477 ("unsupported", format!("{} + {}", aura.name, property.name))
478 }
479 _ => {
480 counts.unsupported += 1;
481
482 (
483 "unsupported",
484 "unrecognized semantic coverage variant".to_string(),
485 )
486 }
487 };
488
489 EffectAuditRow {
490 spell: format!("{spell_name} ({spell_id})"),
491 effect_index,
492 effect_type: effect.effect,
493 aura_subtype: effect.aura,
494 modifier_property: effect.misc_value_0,
495 trigger_spell: effect.trigger_spell,
496 coverage,
497 handler,
498 }
499}
500
501#[cfg(test)]
502mod tests;