1use 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#[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#[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#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
55pub struct TargetPlanOperations {
56 first: Option<TargetPlanOperation>,
57 second: Option<TargetPlanOperation>,
58}
59
60impl TargetPlanOperations {
61 #[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#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub struct TargetPlan {
125 pub operations: TargetPlanOperations,
126 pub fallback: SpellEffectTargetSemantic,
127 pub provenance: TargetPlanProvenance,
128}
129
130#[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#[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
194pub 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
226pub 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}