1mod 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#[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#[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}