Skip to main content

wowlab_engine_combat/state/program/
mod.rs

1//! Immutable executable combat definitions and indexes.
2
3mod aura_dependencies;
4mod buffer_bindings;
5pub(crate) mod config;
6mod damage;
7mod definitions;
8mod effects;
9mod ground_effects;
10mod indexes;
11mod rotation;
12mod telemetry;
13
14#[cfg(feature = "rotation-optimizations")]
15use wowlab_engine_domain::rotation::PreparedRotationChangeDirection;
16use wowlab_engine_domain::{encounter::EnemyActorDefinition, rotation::LayoutFingerprint};
17use wowlab_engine_telemetry::TargetMetadata;
18#[cfg(feature = "rotation-optimizations")]
19use wowlab_types::combat::ResourceType;
20use wowlab_types::{game::GearSlot, sim::IntMap};
21
22use super::{
23    AccumulatingImpactProc, AuraData, AutoAttackData, CastHookFn, DriverSpellModifier,
24    EmpowerReleaseHookFn, ImpactEffectProc, ImpactProc, LandedImpactProc, LocalAuraIdx,
25    LocalRppmIdx, LocalSpellIdx, PassiveDriverEffect, PeriodicDriver, ResolvedSystemRppmBinding,
26    ResolvedWeaponEnchantBinding, ResolvedWeaponImbueBinding, ResourceGainProc,
27    ResourceThresholdTrigger, SpellData, SpellEffectData,
28};
29
30#[rustfmt::skip]
31pub(crate) use config::ProgramConfig;
32pub(crate) use aura_dependencies::{
33    AuraDependencies, AuraDependencyGraph, AuraDependencyPlan, AuraProjectionDependencyIdx,
34    ProjectionDependency, SpellConsumerBinding,
35};
36pub(crate) use buffer_bindings::{BufferBindings, ConcreteAuraBinding};
37pub use damage::{DamageActionId, DamageEffectId};
38pub(crate) use damage::{
39    DamagePlan, DamageProfileAoe, DamageRequestBinding, FinalizedBuffDamageBinding,
40    FinalizedDamageReplication, FinalizedTargetExecution, LocalDamageActionIdx,
41    LocalDamageEffectIdx, PeriodicDamageBinding, SpellDamageBinding,
42};
43#[cfg(feature = "rotation-optimizations")]
44pub(crate) use effects::ActiveAuraEffectPlan;
45pub(crate) use effects::{EffectPlan, PlannedAuraEffect};
46pub(crate) use ground_effects::{GroundEffectPlan, compile_ground_effect_plans};
47pub(crate) use rotation::{
48    CastAuraRequirement, CastProfile, RotationPlan, RotationPlanInputs, RotationSpellBinding,
49};
50pub(crate) use telemetry::finalize_target_metadata;
51
52#[cfg(feature = "rotation-optimizations")]
53const LOCAL_SPELL_WORD_COUNT: usize = 4;
54#[cfg(feature = "rotation-optimizations")]
55const ROTATION_DECISION_MODE_COUNT: usize = 4;
56#[cfg(feature = "rotation-optimizations")]
57const EFFECTIVE_CANDIDATE_COUNT: usize = 2;
58#[cfg(feature = "rotation-optimizations")]
59const UNLOCKED_DECISION_MODE_INDEX: usize = 0;
60#[cfg(feature = "rotation-optimizations")]
61const GCD_DECISION_MODE_INDEX: usize = 1;
62#[cfg(feature = "rotation-optimizations")]
63const CASTING_DECISION_MODE_INDEX: usize = 2;
64#[cfg(feature = "rotation-optimizations")]
65const FULLY_LOCKED_DECISION_MODE_INDEX: usize = 3;
66#[cfg(feature = "rotation-optimizations")]
67const DECISION_AURA_STATE: u16 = 1 << 0;
68#[cfg(feature = "rotation-optimizations")]
69const DECISION_TARGET_HEALTH: u16 = 1 << 1;
70#[cfg(feature = "rotation-optimizations")]
71const DECISION_SPATIAL: u16 = 1 << 2;
72#[cfg(feature = "rotation-optimizations")]
73const DECISION_ENCOUNTER_POPULATION: u16 = 1 << 3;
74#[cfg(feature = "rotation-optimizations")]
75const DECISION_PLAYER_HEALTH: u16 = 1 << 5;
76#[cfg(feature = "rotation-optimizations")]
77const DECISION_ACTOR_STATE: u16 = 1 << 6;
78#[cfg(feature = "rotation-optimizations")]
79const DECISION_SPELL_STATE: u16 = 1 << 7;
80#[cfg(feature = "rotation-optimizations")]
81const DECISION_EQUIPMENT_STATE: u16 = 1 << 8;
82#[cfg(feature = "rotation-optimizations")]
83const DECISION_HISTORY_STATE: u16 = 1 << 9;
84#[cfg(feature = "rotation-optimizations")]
85const DECISION_AURA_TIMING: u16 = 1 << 10;
86#[cfg(feature = "rotation-optimizations")]
87const DECISION_TARGET_IDENTITY: u16 = 1 << 11;
88
89#[cfg(feature = "rotation-optimizations")]
90const ROTATION_DECISION_MODES: [RotationDecisionMode; ROTATION_DECISION_MODE_COUNT] = [
91    RotationDecisionMode::Unlocked,
92    RotationDecisionMode::Gcd,
93    RotationDecisionMode::Casting,
94    RotationDecisionMode::Both,
95];
96
97#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98#[cfg(feature = "rotation-optimizations")]
99pub(crate) enum RotationDecisionMode {
100    Unlocked,
101    Gcd,
102    Casting,
103    Both,
104}
105
106#[cfg(feature = "rotation-optimizations")]
107impl RotationDecisionMode {
108    const fn index(self) -> usize {
109        match self {
110            Self::Unlocked => UNLOCKED_DECISION_MODE_INDEX,
111            Self::Gcd => GCD_DECISION_MODE_INDEX,
112            Self::Casting => CASTING_DECISION_MODE_INDEX,
113            Self::Both => FULLY_LOCKED_DECISION_MODE_INDEX,
114        }
115    }
116}
117
118#[cfg(feature = "rotation-optimizations")]
119impl std::fmt::Display for RotationDecisionMode {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        formatter.write_str(match self {
122            Self::Unlocked => "unlocked",
123            Self::Gcd => "GCD-locked",
124            Self::Casting => "cast-locked",
125            Self::Both => "cast-and-GCD-locked",
126        })
127    }
128}
129
130/// Fixed mutation set that can make an action executable in one decision mode.
131#[derive(Clone, Copy, Debug)]
132#[cfg(feature = "rotation-optimizations")]
133pub(crate) struct RotationDecisionSubscription {
134    cooldown_rises: [u64; LOCAL_SPELL_WORD_COUNT],
135    cooldown_falls: [u64; LOCAL_SPELL_WORD_COUNT],
136    resource_rises: u32,
137    resource_falls: u32,
138    semantic_rises: u16,
139    semantic_falls: u16,
140    complete: bool,
141    has_eligible_actions: bool,
142}
143
144#[cfg(feature = "rotation-optimizations")]
145impl RotationDecisionSubscription {
146    const fn complete() -> Self {
147        Self {
148            cooldown_rises: [0; LOCAL_SPELL_WORD_COUNT],
149            cooldown_falls: [0; LOCAL_SPELL_WORD_COUNT],
150            resource_rises: 0,
151            resource_falls: 0,
152            semantic_rises: 0,
153            semantic_falls: 0,
154            complete: true,
155            has_eligible_actions: false,
156        }
157    }
158
159    pub(crate) const fn is_complete(&self) -> bool {
160        self.complete
161    }
162
163    pub(crate) const fn has_eligible_actions(&self) -> bool {
164        self.has_eligible_actions
165    }
166
167    pub(crate) const fn cooldown_rises(&self) -> &[u64; LOCAL_SPELL_WORD_COUNT] {
168        &self.cooldown_rises
169    }
170
171    pub(crate) const fn cooldown_falls(&self) -> &[u64; LOCAL_SPELL_WORD_COUNT] {
172        &self.cooldown_falls
173    }
174
175    pub(crate) const fn resource_rises(&self) -> u32 {
176        self.resource_rises
177    }
178
179    pub(crate) const fn resource_falls(&self) -> u32 {
180        self.resource_falls
181    }
182
183    pub(crate) const fn semantic_rises(&self) -> u16 {
184        self.semantic_rises
185    }
186
187    pub(crate) const fn semantic_falls(&self) -> u16 {
188        self.semantic_falls
189    }
190
191    #[cfg(test)]
192    pub(crate) fn contains_cooldown(
193        &self,
194        spell: LocalSpellIdx,
195        direction: PreparedRotationChangeDirection,
196    ) -> bool {
197        let index = spell.as_usize();
198        let word = index / u64::BITS as usize;
199        let bit = 1_u64 << (index % u64::BITS as usize);
200        let words = match direction {
201            PreparedRotationChangeDirection::Rise => &self.cooldown_rises,
202            PreparedRotationChangeDirection::Fall => &self.cooldown_falls,
203            _ => return false,
204        };
205
206        words.get(word).is_some_and(|value| value & bit != 0)
207    }
208
209    #[cfg(test)]
210    pub(crate) fn contains_resource(
211        &self,
212        resource: ResourceType,
213        direction: PreparedRotationChangeDirection,
214    ) -> bool {
215        let bit = 1_u32 << u8::from(resource);
216        let resources = match direction {
217            PreparedRotationChangeDirection::Rise => self.resource_rises,
218            PreparedRotationChangeDirection::Fall => self.resource_falls,
219            _ => return false,
220        };
221
222        resources & bit != 0
223    }
224
225    #[cfg(test)]
226    pub(crate) const fn contains_aura_state(
227        &self,
228        direction: PreparedRotationChangeDirection,
229    ) -> bool {
230        self.contains_semantic(DECISION_AURA_STATE, direction)
231    }
232
233    #[cfg(test)]
234    pub(crate) const fn contains_target_health(
235        &self,
236        direction: PreparedRotationChangeDirection,
237    ) -> bool {
238        self.contains_semantic(DECISION_TARGET_HEALTH, direction)
239    }
240
241    #[cfg(test)]
242    pub(crate) const fn contains_spatial(
243        &self,
244        direction: PreparedRotationChangeDirection,
245    ) -> bool {
246        self.contains_semantic(DECISION_SPATIAL, direction)
247    }
248
249    #[cfg(test)]
250    pub(crate) const fn contains_encounter_population(
251        &self,
252        direction: PreparedRotationChangeDirection,
253    ) -> bool {
254        self.contains_semantic(DECISION_ENCOUNTER_POPULATION, direction)
255    }
256
257    #[cfg(test)]
258    const fn contains_semantic(
259        &self,
260        bit: u16,
261        direction: PreparedRotationChangeDirection,
262    ) -> bool {
263        let semantics = match direction {
264            PreparedRotationChangeDirection::Rise => self.semantic_rises,
265            PreparedRotationChangeDirection::Fall => self.semantic_falls,
266            _ => return false,
267        };
268
269        semantics & bit != 0
270    }
271
272    fn insert_cooldown(
273        &mut self,
274        spell: LocalSpellIdx,
275        direction: PreparedRotationChangeDirection,
276    ) {
277        let index = spell.as_usize();
278        let word = index / u64::BITS as usize;
279        let bit = 1_u64 << (index % u64::BITS as usize);
280        let words = match direction {
281            PreparedRotationChangeDirection::Rise => &mut self.cooldown_rises,
282            PreparedRotationChangeDirection::Fall => &mut self.cooldown_falls,
283            _ => return,
284        };
285
286        if let Some(value) = words.get_mut(word) {
287            *value |= bit;
288        }
289    }
290
291    fn insert_resource(
292        &mut self,
293        resource: ResourceType,
294        direction: PreparedRotationChangeDirection,
295    ) {
296        let bit = 1_u32 << u8::from(resource);
297        let resources = match direction {
298            PreparedRotationChangeDirection::Rise => &mut self.resource_rises,
299            PreparedRotationChangeDirection::Fall => &mut self.resource_falls,
300            _ => return,
301        };
302
303        *resources |= bit;
304    }
305
306    fn insert_semantic(&mut self, bit: u16, direction: PreparedRotationChangeDirection) {
307        let semantics = match direction {
308            PreparedRotationChangeDirection::Rise => &mut self.semantic_rises,
309            PreparedRotationChangeDirection::Fall => &mut self.semantic_falls,
310            _ => return,
311        };
312
313        *semantics |= bit;
314    }
315}
316
317/// Immutable executable definition produced by combat-system finalization.
318#[derive(Debug)]
319pub struct CombatProgram {
320    pub(crate) spec_identity_brand: Option<u32>,
321    pub(crate) enemies: Vec<EnemyActorDefinition>,
322    pub(crate) telemetry_targets: Vec<TargetMetadata>,
323    pub(crate) spells: Vec<SpellData>,
324    pub(crate) scheduled_spells: wowlab_engine_event_authority::ScheduledSpellCatalog,
325    pub(crate) runtime_events: wowlab_engine_event_authority::RuntimeEventCatalog,
326    pub(crate) scheduled_spell_executions: Box<[ScheduledSpellExecution]>,
327    pub(crate) empower_cast_times_ms: Vec<u32>,
328    pub(crate) spell_effects: Vec<SpellEffectData>,
329    pub(crate) auras: Vec<AuraData>,
330    pub(crate) aura_bindings: Box<[Option<LocalAuraIdx>]>,
331    pub(crate) spell_bindings: Box<[Option<LocalSpellIdx>]>,
332    pub(crate) rppm_bindings: Box<[Option<LocalRppmIdx>]>,
333    pub(crate) system_rppm_bindings: Box<[Option<ResolvedSystemRppmBinding>]>,
334    pub(crate) aura_followups: Box<[Option<LocalAuraIdx>]>,
335    pub(crate) aura_exclusions:
336        Box<[[Option<LocalAuraIdx>; crate::MAX_AURA_APPLICATION_EXCLUSIONS]]>,
337    pub(crate) aura_expire_trigger_spells: Box<[Option<LocalSpellIdx>]>,
338    pub(crate) external_buff_auras: Box<[Option<LocalAuraIdx>]>,
339    pub(crate) proc_category_recovery_ms: Box<[u32]>,
340    pub(crate) auto_attacks: Vec<AutoAttackData>,
341    pub(crate) pet_action_plans: Box<[u16]>,
342    pub(crate) pet_action_plan_by_auto: Box<[Option<u16>]>,
343    pub(crate) primary_pet_action_auto: Option<u16>,
344    pub(crate) pet_auto_attacks: Box<[u16]>,
345    pub(crate) cast_hooks: Vec<Option<CastHookFn>>,
346    pub(crate) empower_release_hooks: Vec<Option<EmpowerReleaseHookFn>>,
347    pub(crate) tick_hooks: Vec<Option<CastHookFn>>,
348    pub(crate) player_cast_hooks: Vec<CastHookFn>,
349    pub(crate) resource_threshold_triggers: Vec<ResourceThresholdTrigger>,
350    pub(crate) impact_procs: Vec<ImpactProc>,
351    pub(crate) landed_impact_procs: Vec<LandedImpactProc>,
352    pub(crate) accumulating_impact_procs: Vec<AccumulatingImpactProc>,
353    pub(crate) impact_effect_procs: Vec<ImpactEffectProc>,
354    pub(crate) driver_spell_modifiers: Vec<DriverSpellModifier>,
355    pub(crate) passive_driver_effects: Vec<PassiveDriverEffect>,
356    pub(crate) effects: EffectPlan,
357    pub(crate) damage: DamagePlan,
358    pub(crate) ground_effects: Box<[GroundEffectPlan]>,
359    pub(crate) aura_dependencies: AuraDependencyPlan,
360    pub(crate) rotation: RotationPlan,
361    pub(crate) periodic_drivers: Vec<PeriodicDriver>,
362    pub(crate) periodic_driver_bindings: Box<[Option<usize>]>,
363    pub(crate) talent_ranks: Box<[u8]>,
364    pub(crate) resource_gain_procs: Vec<ResourceGainProc>,
365    pub(crate) item_use_spells: Vec<(GearSlot, u32)>,
366    pub(crate) weapon_enchant_bindings: Box<[ResolvedWeaponEnchantBinding]>,
367    pub(crate) weapon_imbue_bindings: Box<[ResolvedWeaponImbueBinding]>,
368    pub(crate) weapon_imbue_by_spell:
369        Box<[Option<wowlab_engine_domain::rotation::WeaponImbueSlotHandle>]>,
370    pub(crate) precombat_auras: Vec<LocalAuraIdx>,
371    pub(crate) talent_aura_stacks: Vec<(LocalAuraIdx, u8)>,
372    pub(crate) stealth_aura: Option<LocalAuraIdx>,
373    pub(crate) spell_by_id: IntMap<u32, LocalSpellIdx>,
374    #[cfg(any(test, feature = "test-support"))]
375    pub(crate) aura_by_id: IntMap<u32, LocalAuraIdx>,
376    pub(crate) cooldown_category_members: Box<[Box<[LocalSpellIdx]>]>,
377    pub(crate) charge_category_members: Box<[Box<[LocalSpellIdx]>]>,
378    pub(crate) charge_category_owners: Box<[LocalSpellIdx]>,
379    pub(crate) start_recovery_category_owners: Box<[LocalSpellIdx]>,
380    pub(crate) item_rppm_indices: IntMap<u32, LocalRppmIdx>,
381    pub(crate) system_rppm_indices: IntMap<u32, LocalRppmIdx>,
382    pub(crate) enchant_rppm_indices: IntMap<u32, LocalRppmIdx>,
383    pub(crate) enchant_buff_spell_ids: IntMap<u32, u32>,
384    pub(crate) config: ProgramConfig,
385    pub(crate) names: Vec<Box<str>>,
386    pub(crate) layout_fingerprint: LayoutFingerprint,
387    pub(crate) buffer_bindings: BufferBindings,
388}
389
390impl CombatProgram {
391    pub(crate) fn resolve_spell_identity(&self, local: LocalSpellIdx) -> Option<LocalSpellIdx> {
392        ((local.brand == 0 || self.spec_identity_brand == Some(local.brand))
393            && self.spells.get(local.index as usize).is_some())
394        .then(|| LocalSpellIdx::new(local.index))
395    }
396
397    pub(crate) fn resolve_aura_identity(&self, local: LocalAuraIdx) -> Option<LocalAuraIdx> {
398        ((local.brand == 0 || self.spec_identity_brand == Some(local.brand))
399            && self.auras.get(local.index as usize).is_some())
400        .then(|| LocalAuraIdx::new(local.index))
401    }
402}
403
404#[derive(Clone, Copy, Debug)]
405pub(crate) enum ScheduledSpellExecution {
406    Registered(LocalSpellIdx),
407    TriggerOnly(LocalDamageActionIdx),
408    TriggerOnlyEmpty,
409}