Skip to main content

forge/talent_conformance/audit/
target.rs

1// #t(file: rust_alloc_in_loop) target diagnostics own stable selector programs and evidence.
2
3use 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;