Skip to main content

wowlab_engine_domain/targeting/
plan.rs

1//! Compilation of DBC TargetA/TargetB into an ordered, compositional target program.
2
3use super::{TargetPlanOverlay, registered_target_plan_overlay};
4use crate::dbc::{
5    ImplicitTargetCheck, ImplicitTargetDirection, ImplicitTargetObject, ImplicitTargetReference,
6    ImplicitTargetSelection, SpellEffectTargetSemantic, implicit_target_semantic,
7    spell_effect_target_semantic,
8};
9
10const TARGET_OPERATION_SLOTS: usize = 2;
11
12/// One selector with `TrinityCore`'s five axes kept independent.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub struct TargetSelector {
15    pub raw: Option<i32>,
16    pub object: ImplicitTargetObject,
17    pub reference: ImplicitTargetReference,
18    pub algorithm: ImplicitTargetSelection,
19    pub check: ImplicitTargetCheck,
20    pub direction: ImplicitTargetDirection,
21}
22
23impl TargetSelector {
24    #[must_use]
25    pub const fn custom(
26        object: ImplicitTargetObject,
27        reference: ImplicitTargetReference,
28        algorithm: ImplicitTargetSelection,
29        check: ImplicitTargetCheck,
30        direction: ImplicitTargetDirection,
31    ) -> Self {
32        Self {
33            raw: None,
34            object,
35            reference,
36            algorithm,
37            check,
38            direction,
39        }
40    }
41}
42
43/// Ordered operation emitted by one implicit-target selector.
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45#[non_exhaustive]
46pub enum TargetPlanOperation {
47    AssignSource(TargetSelector),
48    AssignDestination(TargetSelector),
49    Select(TargetSelector),
50    SelectAndAssignDestination(TargetSelector),
51}
52
53/// TargetA/TargetB operations in authored order without allocation.
54#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55pub struct TargetPlanOperations {
56    first: Option<TargetPlanOperation>,
57    second: Option<TargetPlanOperation>,
58}
59
60impl TargetPlanOperations {
61    /// Creates an empty target-operation sequence.
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            first: None,
66            second: None,
67        }
68    }
69
70    #[must_use]
71    pub const fn one(operation: TargetPlanOperation) -> Self {
72        Self {
73            first: Some(operation),
74            second: None,
75        }
76    }
77
78    #[must_use]
79    pub const fn two(first: TargetPlanOperation, second: TargetPlanOperation) -> Self {
80        Self {
81            first: Some(first),
82            second: Some(second),
83        }
84    }
85
86    #[must_use]
87    pub const fn into_array(self) -> [Option<TargetPlanOperation>; TARGET_OPERATION_SLOTS] {
88        [self.first, self.second]
89    }
90
91    fn push(&mut self, operation: TargetPlanOperation) -> Result<(), TargetPlanError> {
92        if self.first.is_none() {
93            self.first = Some(operation);
94
95            return Ok(());
96        }
97
98        if self.second.is_none() {
99            self.second = Some(operation);
100
101            return Ok(());
102        }
103
104        Err(TargetPlanError::too_many_operations())
105    }
106}
107
108/// Origin of the final program used by both runtime behavior and Forge conformance.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110#[non_exhaustive]
111pub enum TargetPlanProvenance {
112    Dbc {
113        target_a: i32,
114        target_b: i32,
115    },
116    Overlay {
117        name: &'static str,
118        reason: &'static str,
119    },
120}
121
122/// Ordered target program plus the effect-kind fallback Trinity applies when it selects nothing.
123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub struct TargetPlan {
125    pub operations: TargetPlanOperations,
126    pub fallback: SpellEffectTargetSemantic,
127    pub provenance: TargetPlanProvenance,
128}
129
130/// Spell-effect coordinate and its DBC inputs.
131#[derive(Clone, Copy, Debug, Eq, PartialEq)]
132pub struct TargetPlanInput {
133    pub spell_id: u32,
134    pub effect_index: u8,
135    pub effect_type: i32,
136    pub target_a: i32,
137    pub target_b: i32,
138}
139
140wowlab_engine_macros::define_error! {
141/// Failure to compile canonical data or find one unambiguous content overlay.
142#[derive(Clone, Copy, Debug, Eq, PartialEq)]
143pub struct TargetPlanError {
144    kind: TargetPlanErrorKind,
145}
146
147#[derive(Clone, Copy, Debug, thiserror::Error, Eq, PartialEq)]
148enum TargetPlanErrorKind {
149    #[error("implicit target selector {selector} is outside the canonical TrinityCore table")]
150    UnknownSelector { selector: i32 },
151    #[error("spell effect kind {effect_type} is outside the canonical TrinityCore table")]
152    UnknownEffectType { effect_type: i32 },
153    #[error("target plan contains more operations than TargetA and TargetB can encode")]
154    TooManyOperations,
155    #[error("spell {spell_id} effect {effect_index} has multiple target-plan overlays")]
156    DuplicateOverlay { spell_id: u32, effect_index: u8 },
157    #[error("spell {spell_id} effect {effect_index} has multiple target-geometry overlays")]
158    DuplicateGeometryOverlay { spell_id: u32, effect_index: u8 },
159}
160}
161
162impl TargetPlanError {
163    const fn new(kind: TargetPlanErrorKind) -> Self {
164        Self { kind }
165    }
166
167    const fn unknown_selector(selector: i32) -> Self {
168        Self::new(TargetPlanErrorKind::UnknownSelector { selector })
169    }
170
171    const fn unknown_effect_type(effect_type: i32) -> Self {
172        Self::new(TargetPlanErrorKind::UnknownEffectType { effect_type })
173    }
174
175    const fn too_many_operations() -> Self {
176        Self::new(TargetPlanErrorKind::TooManyOperations)
177    }
178
179    pub(super) const fn duplicate_overlay(spell_id: u32, effect_index: u8) -> Self {
180        Self::new(TargetPlanErrorKind::DuplicateOverlay {
181            spell_id,
182            effect_index,
183        })
184    }
185
186    pub(super) const fn duplicate_geometry_overlay(spell_id: u32, effect_index: u8) -> Self {
187        Self::new(TargetPlanErrorKind::DuplicateGeometryOverlay {
188            spell_id,
189            effect_index,
190        })
191    }
192}
193
194/// Compile DBC selectors, applying the unique content overlay registered for this effect.
195///
196/// # Errors
197///
198/// Returns an error for duplicate overlays or unsupported DBC target semantics.
199pub fn compile_target_plan(input: TargetPlanInput) -> Result<TargetPlan, TargetPlanError> {
200    let overlay = registered_target_plan_overlay(input.spell_id, input.effect_index)?;
201
202    compile_target_plan_with_overlay(input, overlay)
203}
204
205fn compile_target_plan_with_overlay(
206    input: TargetPlanInput,
207    overlay: Option<&TargetPlanOverlay>,
208) -> Result<TargetPlan, TargetPlanError> {
209    let fallback = spell_effect_target_semantic(input.effect_type)
210        .ok_or_else(|| TargetPlanError::unknown_effect_type(input.effect_type))?;
211
212    if let Some(overlay) = overlay {
213        return Ok(TargetPlan {
214            operations: overlay.operations,
215            fallback,
216            provenance: TargetPlanProvenance::Overlay {
217                name: overlay.name,
218                reason: overlay.reason,
219            },
220        });
221    }
222
223    compile_dbc_target_plan(input.effect_type, input.target_a, input.target_b)
224}
225
226/// Translate TargetA/TargetB into operations without deciding which axes the runtime supports.
227///
228/// # Errors
229///
230/// Returns an error when an effect type or implicit target selector is unknown.
231pub fn compile_dbc_target_plan(
232    effect_type: i32,
233    target_a: i32,
234    target_b: i32,
235) -> Result<TargetPlan, TargetPlanError> {
236    let fallback = spell_effect_target_semantic(effect_type)
237        .ok_or_else(|| TargetPlanError::unknown_effect_type(effect_type))?;
238    let mut operations = TargetPlanOperations::default();
239
240    for raw in [target_a, target_b] {
241        if raw == 0 {
242            continue;
243        }
244
245        let semantic =
246            implicit_target_semantic(raw).ok_or_else(|| TargetPlanError::unknown_selector(raw))?;
247        let selector = TargetSelector {
248            raw: Some(raw),
249            object: semantic.object,
250            reference: semantic.reference,
251            algorithm: semantic.selection,
252            check: semantic.check,
253            direction: semantic.direction,
254        };
255
256        operations.push(operation_for(selector))?;
257    }
258
259    Ok(TargetPlan {
260        operations,
261        fallback,
262        provenance: TargetPlanProvenance::Dbc { target_a, target_b },
263    })
264}
265
266const fn operation_for(selector: TargetSelector) -> TargetPlanOperation {
267    match selector.object {
268        ImplicitTargetObject::Source => TargetPlanOperation::AssignSource(selector),
269        ImplicitTargetObject::Destination => TargetPlanOperation::AssignDestination(selector),
270        ImplicitTargetObject::UnitAndDestination => {
271            TargetPlanOperation::SelectAndAssignDestination(selector)
272        }
273        _ => TargetPlanOperation::Select(selector),
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use googletest::prelude::*;
280    use rstest::rstest;
281
282    use super::*;
283
284    fn operations(plan: TargetPlan) -> Vec<TargetPlanOperation> {
285        plan.operations.into_array().into_iter().flatten().collect()
286    }
287
288    const fn selector(operation: TargetPlanOperation) -> TargetSelector {
289        match operation {
290            TargetPlanOperation::AssignSource(selector)
291            | TargetPlanOperation::AssignDestination(selector)
292            | TargetPlanOperation::Select(selector)
293            | TargetPlanOperation::SelectAndAssignDestination(selector) => selector,
294        }
295    }
296
297    #[gtest]
298    #[rstest]
299    #[case::blade_flurry(
300        18,
301        16,
302        ImplicitTargetReference::Caster,
303        ImplicitTargetReference::Destination
304    )]
305    #[case::blade_rush(
306        53,
307        16,
308        ImplicitTargetReference::Target,
309        ImplicitTargetReference::Destination
310    )]
311    fn ordered_location_assignment_and_selection_remain_separate(
312        #[case] target_a: i32,
313        #[case] target_b: i32,
314        #[case] assignment_reference: ImplicitTargetReference,
315        #[case] selection_reference: ImplicitTargetReference,
316    ) {
317        let plan = compile_dbc_target_plan(2, target_a, target_b).expect("known DBC program");
318        let operations = operations(plan);
319
320        expect_that!(
321            matches!(
322                operations[0],
323                TargetPlanOperation::AssignDestination(selector)
324                    if selector.reference == assignment_reference
325            ),
326            is_true()
327        );
328        expect_that!(
329            matches!(
330                operations[1],
331                TargetPlanOperation::Select(selector)
332                    if selector.reference == selection_reference
333                        && selector.algorithm == ImplicitTargetSelection::Area
334                        && selector.check == ImplicitTargetCheck::Enemy
335            ),
336            is_true()
337        );
338    }
339
340    #[gtest]
341    #[rstest]
342    #[case::raid_target(57, ImplicitTargetCheck::Raid)]
343    #[case::line_target(134, ImplicitTargetCheck::Enemy)]
344    #[case::trajectory_target(89, ImplicitTargetCheck::Default)]
345    fn unsupported_axes_are_preserved_instead_of_rejected_during_compilation(
346        #[case] raw: i32,
347        #[case] check: ImplicitTargetCheck,
348    ) {
349        let plan = compile_dbc_target_plan(2, raw, 0).expect("known selector compiles to IR");
350        let selector = selector(operations(plan)[0]);
351
352        expect_that!(selector.raw, eq(Some(raw)));
353        expect_that!(selector.check, eq(check));
354    }
355
356    #[gtest]
357    fn overlay_replaces_operations_without_erasing_effect_fallback() {
358        let selector = TargetSelector::custom(
359            ImplicitTargetObject::Unit,
360            ImplicitTargetReference::Caster,
361            ImplicitTargetSelection::Area,
362            ImplicitTargetCheck::Enemy,
363            ImplicitTargetDirection::None,
364        );
365        let overlay = TargetPlanOverlay::new(
366            crate::targeting::TargetEffectCoordinate {
367                spell_id: 77,
368                effect_index: 1,
369            },
370            crate::targeting::TargetOverlayMetadata {
371                name: "custom path",
372                reason: "the server spell omits its impact selection",
373            },
374            TargetPlanOperations::one(TargetPlanOperation::Select(selector)),
375        );
376        let plan = compile_target_plan_with_overlay(overlay.input(2, 134, 0), Some(&overlay))
377            .expect("overlay provides complete operations");
378
379        expect_that!(plan.operations, eq(overlay.operations));
380        expect_that!(plan.fallback, eq(spell_effect_target_semantic(2).unwrap()));
381        expect_that!(
382            matches!(
383                plan.provenance,
384                TargetPlanProvenance::Overlay {
385                    name: "custom path",
386                    ..
387                }
388            ),
389            is_true()
390        );
391    }
392}