1use wowlab_engine_domain::{
4 encounter::EnemyActorDefinition,
5 pool::PoolIndex,
6 rotation::{BufferRequirements, CatalogHints},
7};
8use wowlab_engine_ports::EngineError;
9use wowlab_types::{
10 data::CooldownCategoryId,
11 game::GearSlot,
12 sim::{ActorId, AuraIdx, AuraKey, AuraOn, FastMap, IntMap, PetIdx, SimTime, SpellIdx},
13};
14
15use super::{
16 BuilderError, BuiltCombatSystem,
17 buffer_init::{InitialBufferConfig, populate_initial_buffer},
18 compile::{
19 CooldownBypass, CostBypass, RotationCompileInput, SpellGate, SpellOverride, bind_rotation,
20 emit_rotation, prepare_rotation,
21 },
22};
23use crate::{
24 AccumulatingImpactProc, AuraBinding, CastHookFn, DriverSpellModifier, ImpactProc,
25 LandedImpactProc, LocalAuraIdx, LocalSpellIdx, LocalThresholdIdx, PassiveDriverEffect,
26 PeriodicDriver, ResourceGainProc, RppmBinding, RppmTracker, SpellBinding, SpellEffectData,
27 SystemRppmBinding, ThresholdTracker, WeaponEnchantBinding,
28 state::{
29 AuraData, AutoAttackData, BaseStats, BuffEffect, BufferBindings, CombatInstance,
30 CombatProgram, CombatRuntime, EmpowerReleaseHookFn, ImpactEffectProc, LocalRppmIdx,
31 PetActionBar, ProgramConfig, ResolvedSystemRppmBinding, ScratchBuffers, SpellData,
32 WeaponImbueBinding, try_push_effect,
33 },
34};
35
36#[derive(Debug)]
37pub(crate) struct DraftDefinitions {
38 pub(crate) enemies: Vec<EnemyActorDefinition>,
39 pub(crate) spells: Vec<SpellData>,
40 pub(crate) empower_cast_times_ms: Vec<u32>,
41 pub(crate) spell_effects: Vec<SpellEffectData>,
42 pub(crate) auras: Vec<AuraData>,
43 pub(crate) aura_bindings: Vec<(AuraBinding, u32)>,
44 pub(crate) spell_bindings: Vec<(SpellBinding, u32)>,
45 pub(crate) auto_attacks: Vec<AutoAttackData>,
46 pub(crate) cast_hooks: Vec<Option<CastHookFn>>,
47 pub(crate) empower_release_hooks: Vec<Option<EmpowerReleaseHookFn>>,
48 pub(crate) tick_hooks: Vec<Option<CastHookFn>>,
49 pub(crate) player_cast_hooks: Vec<CastHookFn>,
50 pub(crate) resource_threshold_triggers: Vec<crate::state::ResourceThresholdTrigger>,
51 pub(crate) impact_procs: Vec<ImpactProc>,
52 pub(crate) landed_impact_procs: Vec<LandedImpactProc>,
53 pub(crate) accumulating_impact_procs: Vec<AccumulatingImpactProc>,
54 pub(crate) impact_effect_procs: Vec<ImpactEffectProc>,
55 pub(crate) driver_spell_modifiers: Vec<DriverSpellModifier>,
56 pub(crate) passive_driver_effects: Vec<PassiveDriverEffect>,
57 pub(crate) periodic_drivers: Vec<PeriodicDriver>,
58 pub(crate) talent_ranks: IntMap<u32, u8>,
59 pub(crate) resource_gain_procs: Vec<ResourceGainProc>,
60 pub(crate) item_use_spells: Vec<(GearSlot, u32)>,
61 pub(crate) rppm_trackers: Vec<RppmTracker>,
62 pub(crate) rppm_bindings: Vec<(RppmBinding, LocalRppmIdx)>,
63 pub(crate) system_rppm_bindings: Vec<(SystemRppmBinding, ResolvedSystemRppmBinding)>,
64 pub(crate) threshold_trackers: Vec<ThresholdTracker>,
65 pub(crate) weapon_enchant_bindings: Vec<WeaponEnchantBinding>,
66 pub(crate) weapon_imbue_bindings: Vec<WeaponImbueBinding>,
67 pub(crate) precombat_auras: Vec<LocalAuraIdx>,
68 pub(crate) talent_aura_stacks: Vec<(LocalAuraIdx, u8)>,
69 pub(crate) stealth_aura_id: Option<u32>,
70}
71
72impl DraftDefinitions {
73 pub(crate) fn new(enemies: Vec<EnemyActorDefinition>) -> Self {
74 Self {
75 enemies,
76 spells: Vec::new(),
77 empower_cast_times_ms: Vec::new(),
78 spell_effects: Vec::new(),
79 auras: Vec::new(),
80 aura_bindings: Vec::new(),
81 spell_bindings: Vec::new(),
82 auto_attacks: Vec::new(),
83 cast_hooks: Vec::new(),
84 empower_release_hooks: Vec::new(),
85 tick_hooks: Vec::new(),
86 player_cast_hooks: Vec::new(),
87 resource_threshold_triggers: Vec::new(),
88 impact_procs: Vec::new(),
89 landed_impact_procs: Vec::new(),
90 accumulating_impact_procs: Vec::new(),
91 impact_effect_procs: Vec::new(),
92 driver_spell_modifiers: Vec::new(),
93 passive_driver_effects: Vec::new(),
94 periodic_drivers: Vec::new(),
95 talent_ranks: IntMap::default(),
96 resource_gain_procs: Vec::new(),
97 item_use_spells: Vec::new(),
98 rppm_trackers: Vec::new(),
99 rppm_bindings: Vec::new(),
100 system_rppm_bindings: Vec::new(),
101 threshold_trackers: Vec::new(),
102 weapon_enchant_bindings: Vec::new(),
103 weapon_imbue_bindings: Vec::new(),
104 precombat_auras: Vec::new(),
105 talent_aura_stacks: Vec::new(),
106 stealth_aura_id: None,
107 }
108 }
109}
110
111#[derive(Debug)]
112pub(crate) struct DraftIndexes {
113 pub(crate) spell_by_id: IntMap<u32, LocalSpellIdx>,
114 pub(crate) aura_by_id: IntMap<u32, LocalAuraIdx>,
115 pub(crate) aura_by_identity: FastMap<(u32, AuraOn), LocalAuraIdx>,
116 pub(crate) cooldown_categories: PoolIndex<CooldownCategoryId, LocalSpellIdx>,
117 pub(crate) charge_categories: PoolIndex<CooldownCategoryId, LocalSpellIdx>,
118 pub(crate) item_rppm_indices: IntMap<u32, LocalRppmIdx>,
119 pub(crate) system_rppm_indices: IntMap<u32, LocalRppmIdx>,
120 pub(crate) enchant_rppm_indices: IntMap<u32, LocalRppmIdx>,
121 pub(crate) enchant_buff_spell_ids: IntMap<u32, u32>,
122}
123
124#[derive(Debug)]
125struct FrozenProgramBindings {
126 scheduled_spells: wowlab_engine_event_authority::ScheduledSpellCatalog,
127 runtime_events: wowlab_engine_event_authority::RuntimeEventCatalog,
128 scheduled_spell_executions: Box<[crate::state::program::ScheduledSpellExecution]>,
129 layout_fingerprint: wowlab_engine_domain::rotation::LayoutFingerprint,
130 buffer_bindings: BufferBindings,
131}
132
133#[derive(Debug)]
135#[must_use]
136pub struct CombatSystemDraft {
138 pub(crate) spec_identity_brand: Option<u32>,
139 pub(crate) definitions: DraftDefinitions,
140 pub(crate) indexes: DraftIndexes,
141 pub(crate) runtime: CombatRuntime,
142 pub(crate) config: ProgramConfig,
143 pub(crate) names: Vec<Box<str>>,
144 pub(crate) spell_ids: FastMap<String, u32>,
145 pub(crate) damage_spell_ids: Vec<u32>,
146 pub(crate) effect_spell_ids: Vec<u32>,
147 pub(crate) generated_damage_identities: Vec<crate::DamageActionId>,
148 pub(crate) aura_ids: FastMap<String, u32>,
149 pub(crate) hints: CatalogHints,
150 pub(crate) gating: Vec<SpellGate>,
151 pub(crate) cost_bypass: Vec<CostBypass>,
152 pub(crate) cooldown_bypass: Vec<CooldownBypass>,
153 pub(crate) overrides: Vec<SpellOverride>,
154 pub(crate) resource_max: f64,
155 pub(crate) talent_ranks: Vec<(String, u8)>,
156 pub(crate) talent_spell_ids: Vec<u32>,
157 pub(crate) selected_talent_spell_ids: Vec<u32>,
158 pub(crate) selected_replaced_spell_ids: Vec<u32>,
159 pub(crate) disabled_spell_ids: Vec<u32>,
160 pub(crate) initial_weapon_imbues: Vec<(&'static str, bool)>,
161 pub(crate) ground_effects: Vec<crate::GroundEffectDefinition>,
162 pub(crate) hook_flat_damage: FastMap<u32, f64>,
163 pub(crate) hook_scalars: FastMap<u32, f64>,
164}
165impl CombatSystemDraft {
168 pub fn bind_aura(&mut self, binding: AuraBinding, aura_id: u32) {
170 self.definitions.aura_bindings.push((binding, aura_id));
171 }
172
173 pub fn bind_spell(&mut self, binding: SpellBinding, spell_id: u32) {
175 self.definitions.spell_bindings.push((binding, spell_id));
176 }
177
178 pub fn set_spec_config<T>(&mut self, config: T)
180 where
181 T: wowlab_engine_ports::SpecConfig,
182 {
183 self.config.spec_config = Box::new(config);
184 }
185
186 pub fn set_spec_runtime<T>(&mut self, runtime: T)
188 where
189 T: wowlab_engine_ports::SpecRuntime,
190 {
191 self.runtime.set_spec_runtime(Box::new(runtime));
192 }
193
194 pub fn set_bug_toggles(&mut self, bugs: wowlab_engine_ports::BugToggles) {
195 self.config.bugs = bugs;
196 }
197
198 pub fn set_cast_latency(&mut self, cast_latency: wowlab_engine_ports::CastLatency) {
199 self.config.cast_latency = cast_latency;
200 }
201
202 pub fn set_raid_events(&mut self, raid_events: &[wowlab_engine_ports::RaidEventConfig]) {
203 self.config.raid_events = raid_events.to_vec();
204 }
205
206 pub fn set_external_buffs(
207 &mut self,
208 external_buffs: &[wowlab_engine_ports::ExternalBuffConfig],
209 ) {
210 self.config.external_buffs = external_buffs.to_vec();
211 }
212
213 pub fn set_pet_action_bar(&mut self, npc_id: u32, action_bar: PetActionBar) -> bool {
215 let Some(attack) = self
216 .definitions
217 .auto_attacks
218 .iter_mut()
219 .find(|attack| attack.is_pet && attack.npc_id == Some(npc_id))
220 else {
221 return false;
222 };
223
224 attack.pet_action_bar = Some(action_bar);
225
226 true
227 }
228
229 pub fn disable_spell(&mut self, spell_id: u32) {
230 if !self.disabled_spell_ids.contains(&spell_id) {
231 self.disabled_spell_ids.push(spell_id);
232 }
233 }
234
235 pub fn register_damage_identity(
237 &mut self,
238 _name: impl Into<String>,
239 identity: crate::DamageActionId,
240 ) {
241 let spell_id = identity.spell_id();
242
243 if !self.damage_spell_ids.contains(&spell_id) {
244 self.damage_spell_ids.push(spell_id);
245 }
246
247 if !self.generated_damage_identities.contains(&identity) {
248 self.generated_damage_identities.push(identity);
249 }
250 }
251
252 pub fn register_damage_effect_identity(
254 &mut self,
255 name: impl Into<String>,
256 effect: crate::DamageEffectId,
257 ) {
258 self.register_damage_identity(name, effect.action());
259 }
260
261 #[cfg(any(test, feature = "test-support"))]
262 #[doc(hidden)]
263 pub fn register_diagnostic_damage_identity(&mut self, name: impl Into<String>, spell_id: u32) {
264 wowlab_types::bench_metrics::record_keyed_lookup();
265 self.register_damage_identity(name, crate::DamageActionId::new(0, spell_id));
266 }
267
268 pub fn patch_spell(
270 &mut self,
271 spell: LocalSpellIdx,
272 patch: impl FnOnce(&mut SpellData),
273 ) -> bool {
274 let Some(data) = self.definitions.spells.get_mut(spell.as_usize()) else {
275 return false;
276 };
277 let identity = (data.spell_id, data.name_idx);
278
279 patch(data);
280 (data.spell_id, data.name_idx) = identity;
281
282 true
283 }
284
285 pub fn patch_spells(&mut self, mut patch: impl FnMut(&mut SpellData)) {
287 for spell in &mut self.definitions.spells {
288 let identity = (spell.spell_id, spell.name_idx);
289
290 patch(spell);
291 (spell.spell_id, spell.name_idx) = identity;
292 }
293 }
294
295 pub fn patch_aura(&mut self, aura: LocalAuraIdx, patch: impl FnOnce(&mut AuraData)) -> bool {
297 let Some(data) = self.definitions.auras.get_mut(aura.as_usize()) else {
298 return false;
299 };
300 let identity = (data.aura_id, data.name_idx, data.on, data.propagates_to_pet);
301
302 patch(data);
303 (data.aura_id, data.name_idx, data.on, data.propagates_to_pet) = identity;
304
305 true
306 }
307
308 pub fn register_aura(
317 &mut self,
318 name: impl Into<Box<str>>,
319 mut aura: AuraData,
320 ) -> Result<LocalAuraIdx, EngineError> {
321 aura.name_idx = u16::try_from(self.names.len()).map_err(|error| {
322 EngineError::spec_construction(format!(
323 "combat definition name registry exceeds u16 capacity: {error}"
324 ))
325 })?;
326 let local =
327 LocalAuraIdx::new(u8::try_from(self.definitions.auras.len()).map_err(|error| {
328 EngineError::spec_construction(format!(
329 "runtime aura registry exceeds u8 capacity: {error}"
330 ))
331 })?);
332 let aura_id = aura.aura_id;
333 let on = aura.on;
334 let propagates_to_pet = aura.propagates_to_pet;
335
336 self.names.push(name.into());
337 self.definitions.auras.push(aura);
338 self.indexes.aura_by_id.insert(aura_id, local);
339 self.indexes
340 .aura_by_identity
341 .entry((aura_id, on))
342 .or_insert(local);
343
344 if propagates_to_pet {
345 self.indexes
346 .aura_by_identity
347 .entry((aura_id, AuraOn::Pet))
348 .or_insert(local);
349 }
350
351 Ok(local)
352 }
353
354 pub fn register_impact_proc(&mut self, proc: ImpactProc) {
356 self.definitions.impact_procs.push(proc);
357 }
358
359 pub fn register_landed_impact_proc(&mut self, proc: LandedImpactProc) {
361 self.definitions.landed_impact_procs.push(proc);
362 }
363
364 pub fn register_accumulating_impact_proc(&mut self, proc: AccumulatingImpactProc) {
366 self.definitions.accumulating_impact_procs.push(proc);
367 }
368
369 pub fn register_resource_gain_proc(&mut self, proc: ResourceGainProc) {
371 self.definitions.resource_gain_procs.push(proc);
372 }
373
374 pub fn register_rppm_tracker(
380 &mut self,
381 tracker: RppmTracker,
382 ) -> Result<LocalRppmIdx, EngineError> {
383 let local = LocalRppmIdx::new(u8::try_from(self.definitions.rppm_trackers.len()).map_err(
384 |error| {
385 EngineError::spec_construction(format!(
386 "RPPM tracker registry exceeds u8 capacity: {error}"
387 ))
388 },
389 )?);
390
391 self.definitions.rppm_trackers.push(tracker);
392
393 Ok(local)
394 }
395
396 pub fn register_weapon_enchant_binding(
404 &mut self,
405 binding: WeaponEnchantBinding,
406 tracker: RppmTracker,
407 ) -> Result<bool, EngineError> {
408 if self
409 .indexes
410 .enchant_rppm_indices
411 .contains_key(&binding.enchantment_id)
412 {
413 return Ok(false);
414 }
415
416 let first = self.definitions.weapon_enchant_bindings.is_empty();
417 let tracker = self.register_rppm_tracker(tracker)?;
418
419 self.indexes
420 .enchant_rppm_indices
421 .insert(binding.enchantment_id, tracker);
422 self.indexes
423 .enchant_buff_spell_ids
424 .insert(binding.enchantment_id, binding.buff_spell_id);
425 self.definitions.weapon_enchant_bindings.push(binding);
426
427 if first {
428 self.register_impact_proc(ImpactProc::new(
429 crate::systems::weapon_enchants_on_player_impact,
430 ));
431 }
432
433 Ok(true)
434 }
435
436 pub fn register_threshold_tracker(
442 &mut self,
443 tracker: ThresholdTracker,
444 ) -> Result<LocalThresholdIdx, EngineError> {
445 let local = LocalThresholdIdx::new(
446 u8::try_from(self.definitions.threshold_trackers.len()).map_err(|error| {
447 EngineError::spec_construction(format!(
448 "threshold tracker registry exceeds u8 capacity: {error}"
449 ))
450 })?,
451 );
452
453 self.definitions.threshold_trackers.push(tracker);
454
455 Ok(local)
456 }
457
458 pub fn push_precombat_aura(&mut self, aura: LocalAuraIdx) {
460 self.definitions.precombat_auras.push(aura);
461 }
462
463 pub fn set_precombat_aura(&mut self, aura: LocalAuraIdx, enabled: bool) {
465 if enabled {
466 if !self.definitions.precombat_auras.contains(&aura) {
467 self.definitions.precombat_auras.push(aura);
468 }
469 } else {
470 self.definitions
471 .precombat_auras
472 .retain(|candidate| *candidate != aura);
473 }
474 }
475
476 pub fn remove_precombat_aura(&mut self, aura: LocalAuraIdx) {
478 self.definitions
479 .precombat_auras
480 .retain(|candidate| *candidate != aura);
481 self.definitions
482 .talent_aura_stacks
483 .retain(|(candidate, _)| *candidate != aura);
484 }
485
486 pub fn register_player_cast_hook(&mut self, hook: CastHookFn) {
488 self.definitions.player_cast_hooks.push(hook);
489 }
490
491 #[must_use]
493 pub fn aura(&self, aura: LocalAuraIdx) -> Option<&AuraData> {
494 self.definitions.auras.get(aura.as_usize())
495 }
496
497 pub fn auras(&self) -> &[AuraData] {
498 &self.definitions.auras
499 }
500
501 pub fn precombat_auras(&self) -> &[LocalAuraIdx] {
502 &self.definitions.precombat_auras
503 }
504
505 #[must_use]
507 pub fn rppm_tracker(&self, tracker: LocalRppmIdx) -> Option<&RppmTracker> {
508 self.definitions.rppm_trackers.get(tracker.as_usize())
509 }
510
511 #[must_use]
513 pub fn impact_proc(&self, index: usize) -> Option<&ImpactProc> {
514 self.definitions.impact_procs.get(index)
515 }
516
517 #[must_use]
519 pub fn impact_proc_count(&self) -> usize {
520 self.definitions.impact_procs.len()
521 }
522
523 #[must_use]
525 pub fn impact_effect_proc(&self, index: usize) -> Option<&ImpactEffectProc> {
526 self.definitions.impact_effect_procs.get(index)
527 }
528
529 #[must_use]
531 pub fn impact_effect_proc_count(&self) -> usize {
532 self.definitions.impact_effect_procs.len()
533 }
534
535 #[must_use]
537 pub fn spell_effects(&self) -> &[SpellEffectData] {
538 &self.definitions.spell_effects
539 }
540
541 #[must_use]
543 pub fn talent_rank(&self, spell_id: u32) -> Option<u8> {
544 self.definitions.talent_ranks.get(&spell_id).copied()
545 }
546
547 #[must_use]
549 pub fn rppm_tracker_count(&self) -> usize {
550 self.definitions.rppm_trackers.len()
551 }
552
553 #[must_use]
555 pub fn player_cast_hook_count(&self) -> usize {
556 self.definitions.player_cast_hooks.len()
557 }
558
559 #[must_use]
561 pub fn spell_count(&self) -> usize {
562 self.definitions.spells.len()
563 }
564
565 #[must_use]
567 pub fn first_auto_attack(&self) -> Option<&AutoAttackData> {
568 self.definitions.auto_attacks.first()
569 }
570
571 pub fn patch_auto_attacks(&mut self, patch: impl FnOnce(&mut [AutoAttackData])) {
573 patch(&mut self.definitions.auto_attacks);
574 }
575
576 pub fn register_system_rppm_from_data(
586 &mut self,
587 driver_spell_id: u32,
588 ) -> Result<LocalRppmIdx, EngineError> {
589 if let Some(local) = self
590 .indexes
591 .system_rppm_indices
592 .get(&driver_spell_id)
593 .copied()
594 {
595 return Ok(local);
596 }
597
598 let driver = SpellIdx::from_raw(driver_spell_id);
599 let Some((rppm, _)) = self.config.game_data.rppm(driver) else {
600 return Err(EngineError::spec_construction_source(
601 BuilderError::missing_system_rppm_data(driver_spell_id),
602 ));
603 };
604
605 if !rppm.is_finite() || rppm <= 0.0 {
606 return Err(EngineError::spec_construction_source(
607 BuilderError::invalid_system_rppm_data(driver_spell_id, rppm),
608 ));
609 }
610
611 let tracker = RppmTracker {
612 rppm,
613 last_attempt_time: 0.0,
614 last_proc_time: 0.0,
615 accumulated_blp: 0.0,
616 haste_scales: self
617 .config
618 .game_data
619 .rppm_haste_scales(driver)
620 .unwrap_or(false),
621 crit_scales: self
622 .config
623 .game_data
624 .rppm_crit_scales(driver)
625 .unwrap_or(false),
626 auto_attack_speed_scales: false,
627 blp_enabled: true,
628 };
629
630 let tracker_count = self.definitions.rppm_trackers.len();
631
632 if let Err(source) = u8::try_from(tracker_count) {
633 return Err(EngineError::spec_construction_source(
634 BuilderError::system_rppm_registry_full(driver_spell_id, tracker_count, source),
635 ));
636 }
637
638 let local = self.register_rppm_tracker(tracker)?;
639
640 self.indexes
641 .system_rppm_indices
642 .insert(driver_spell_id, local);
643
644 Ok(local)
645 }
646
647 pub fn bind_system_rppm(
653 &mut self,
654 binding: SystemRppmBinding,
655 driver_spell_id: u32,
656 ) -> Result<(), EngineError> {
657 let tracker = self.register_system_rppm_from_data(driver_spell_id)?;
658 let recovery_ms = self
659 .config
660 .game_data
661 .proc_category_recovery_ms(SpellIdx::from_raw(driver_spell_id))
662 .unwrap_or(0);
663
664 self.definitions.system_rppm_bindings.push((
665 binding,
666 ResolvedSystemRppmBinding {
667 tracker,
668 recovery_ms,
669 },
670 ));
671
672 Ok(())
673 }
674
675 pub fn patch_base_stats(&mut self, patch: impl FnOnce(&mut BaseStats)) {
677 patch(&mut self.config.base_stats);
678 self.resource_max = self.config.base_stats.resource_max;
679 }
680
681 #[must_use]
683 pub fn base_combat_stats(&self) -> &wowlab_engine_ports::CombatStats {
684 &self.config.base_stats.stats
685 }
686
687 #[must_use]
688 pub fn base_stats(&self) -> &BaseStats {
689 &self.config.base_stats
690 }
691
692 #[must_use]
693 pub fn game_data(&self) -> &wowlab_engine_gamedata::ResolvedGameData {
694 &self.config.game_data
695 }
696
697 #[must_use]
698 pub fn spell_local(&self, spell_id: u32) -> Option<LocalSpellIdx> {
699 self.indexes.spell_by_id.get(&spell_id).copied()
700 }
701
702 #[must_use]
703 pub fn aura_local(&self, aura_id: u32) -> Option<LocalAuraIdx> {
704 self.indexes.aura_by_id.get(&aura_id).copied()
705 }
706
707 #[must_use]
708 pub fn bug_enabled(&self, bug: &wowlab_engine_ports::GameBug) -> bool {
709 self.config.bugs.enabled(bug)
710 }
711
712 #[must_use]
713 pub fn spec_config<T>(&self) -> Option<&T>
714 where
715 T: wowlab_engine_ports::SpecConfig,
716 {
717 self.config
718 .spec_config
719 .as_ref()
720 .as_any()
721 .downcast_ref::<T>()
722 }
723
724 pub fn set_aura_effect(&mut self, aura: LocalAuraIdx, effect: BuffEffect) {
730 let Some(data) = self.definitions.auras.get_mut(aura.as_usize()) else {
731 debug_assert!(false, "aura index {} out of bounds", aura.raw());
732
733 return;
734 };
735 let result = try_push_effect(&mut data.effects, effect);
736
737 assert!(
738 result.is_ok(),
739 "no free effect slot for aura {}",
740 aura.raw()
741 );
742 }
743
744 pub fn mask_aura_dbc_effect(
746 &mut self,
747 aura: LocalAuraIdx,
748 source_spell_id: u32,
749 effect_index: u8,
750 ) -> usize {
751 let Some(data) = self.definitions.auras.get_mut(aura.as_usize()) else {
752 debug_assert!(false, "aura index {} out of bounds", aura.raw());
753
754 return 0;
755 };
756 let mut masked = 0;
757
758 for slot in &mut data.effects {
759 if slot
760 .as_ref()
761 .and_then(BuffEffect::dbc_source)
762 .is_some_and(|source| source == (source_spell_id, effect_index))
763 {
764 *slot = None;
765 masked += 1;
766 }
767 }
768
769 masked
770 }
771
772 pub fn duplicate_aura_dbc_effect(
777 &mut self,
778 aura: LocalAuraIdx,
779 source_spell_id: u32,
780 effect_index: u8,
781 ) -> usize {
782 let Some(data) = self.definitions.auras.get(aura.as_usize()) else {
783 debug_assert!(false, "aura index {} out of bounds", aura.raw());
784
785 return 0;
786 };
787 let effects: Vec<_> = data
788 .effects
789 .iter()
790 .flatten()
791 .copied()
792 .filter(|effect| effect.dbc_source() == Some((source_spell_id, effect_index)))
793 .collect();
794 let duplicated = effects.len();
795
796 for effect in effects {
797 self.set_aura_effect(aura, effect);
798 }
799
800 duplicated
801 }
802
803 pub fn register_driver_spell_modifier(&mut self, modifier: DriverSpellModifier) {
805 self.definitions.driver_spell_modifiers.push(modifier);
806 }
807
808 pub fn register_passive_driver_effect(&mut self, effect: PassiveDriverEffect) {
810 self.definitions.passive_driver_effects.push(effect);
811 }
812
813 pub fn register_weapon_imbue(
815 &mut self,
816 spell_id: u32,
817 key: &'static str,
818 initially_active: bool,
819 ) {
820 self.definitions
821 .weapon_imbue_bindings
822 .push(WeaponImbueBinding { spell_id, key });
823 self.initial_weapon_imbues.push((key, initially_active));
824 }
825
826 pub fn register_periodic_driver(&mut self, driver: PeriodicDriver) {
828 self.definitions.periodic_drivers.push(driver);
829 }
830
831 pub fn register_ground_effect(
837 &mut self,
838 definition: crate::GroundEffectDefinition,
839 ) -> Result<crate::GroundEffectId, EngineError> {
840 let local = u16::try_from(self.ground_effects.len()).map_err(|error| {
841 EngineError::spec_construction(format!(
842 "ground-effect registry exceeds u16 capacity: {error}"
843 ))
844 })?;
845
846 self.ground_effects.push(definition);
847
848 Ok(crate::GroundEffectId(local))
849 }
850
851 pub fn set_hook_flat_damage(&mut self, action: crate::DamageActionId, amount: f64) {
853 self.hook_flat_damage.insert(action.spell_id(), amount);
854 }
855
856 pub fn set_hook_scalar(&mut self, action: crate::DamageActionId, value: f64) {
858 self.hook_scalars.insert(action.spell_id(), value);
859 }
860
861 pub fn finalize(
870 mut self,
871 rotation: &wowlab_types::sim::Rotation,
872 ) -> Result<BuiltCombatSystem, EngineError> {
873 self.validate_identities()?;
874 let mut scheduled_spell_ids = self
875 .definitions
876 .spells
877 .iter()
878 .map(SpellData::idx)
879 .collect::<Vec<_>>();
880
881 let mut missing_health_trigger = None;
882
883 for effect in self
884 .definitions
885 .auras
886 .iter()
887 .flat_map(|aura| aura.effects.iter().flatten())
888 {
889 if let BuffEffect::HealthThresholdTrigger { spell_id, .. } = effect {
890 let spell = SpellIdx::from_raw(*spell_id);
891
892 if !scheduled_spell_ids.contains(&spell) {
893 scheduled_spell_ids.push(spell);
894 }
895 }
896 }
897
898 let scheduled_spells = wowlab_engine_event_authority::ScheduledSpellCatalog::new(
899 scheduled_spell_ids.iter().copied(),
900 );
901
902 for effect in self
903 .definitions
904 .auras
905 .iter_mut()
906 .flat_map(|aura| aura.effects.iter_mut().flatten())
907 {
908 if let BuffEffect::HealthThresholdTrigger {
909 spell_id,
910 scheduled,
911 ..
912 } = effect
913 {
914 *scheduled = scheduled_spells.bind_spell_id(SpellIdx::from_raw(*spell_id));
915
916 if scheduled.is_none() {
917 missing_health_trigger = Some(*spell_id);
918 }
919 }
920 }
921
922 if let Some(spell_id) = missing_health_trigger {
923 return Err(EngineError::spec_construction(format!(
924 "health trigger {spell_id} is absent from the finalized event-spell catalog"
925 )));
926 }
927
928 let mut effects = crate::state::program::EffectPlan::compile(
929 &self.definitions.auras,
930 &self.definitions.passive_driver_effects,
931 );
932 let aura_dependency_graph = crate::state::program::AuraDependencyGraph::compile(
933 &self.definitions.auras,
934 &self.definitions.spells,
935 &self.definitions.spell_effects,
936 &self.config,
937 );
938
939 self.size_scratch_buffers();
940 let reachable_damage_spell_ids = self.reachable_damage_spell_ids();
941 let damage = crate::state::program::DamagePlan::compile(
942 &self.config,
943 &self.definitions.spells,
944 &self.definitions.auras,
945 &self.definitions.passive_driver_effects,
946 &self.definitions.spell_effects,
947 &self.effect_spell_ids,
948 &self.generated_damage_identities,
949 &reachable_damage_spell_ids,
950 &self.damage_spell_ids,
951 &effects,
952 &self.definitions.driver_spell_modifiers,
953 &self.definitions.impact_effect_procs,
954 &self.definitions.periodic_drivers,
955 &self.definitions.landed_impact_procs,
956 &self.definitions.auto_attacks,
957 &self.hook_flat_damage,
958 &self.hook_scalars,
959 )
960 .map_err(EngineError::spec_construction)?;
961 let scheduled_spell_executions = scheduled_spell_ids
962 .iter()
963 .enumerate()
964 .map(|(index, spell)| -> Result<_, EngineError> {
965 if index < self.definitions.spells.len() {
966 Ok(crate::state::program::ScheduledSpellExecution::Registered(
967 LocalSpellIdx::new(u8::try_from(index).map_err(|error| {
968 EngineError::spec_construction(format!(
969 "scheduled spell registry exceeds local-spell capacity: {error}"
970 ))
971 })?),
972 ))
973 } else {
974 Ok(damage.finalize_action_for_spell(spell.as_u32()).map_or(
975 crate::state::program::ScheduledSpellExecution::TriggerOnlyEmpty,
976 crate::state::program::ScheduledSpellExecution::TriggerOnly,
977 ))
978 }
979 })
980 .collect::<Result<Box<[_]>, _>>()?;
981 let ground_effects =
982 crate::state::program::compile_ground_effect_plans(&self.ground_effects, &damage)
983 .map_err(|error| EngineError::spec_construction(error.to_string()))?;
984
985 let rotation_preparation = prepare_rotation(&RotationCompileInput {
986 rotation,
987 spell_ids: &self.spell_ids,
988 aura_ids: &self.aura_ids,
989 secondary_resource_name: self.config.base_stats.secondary_resource_name.as_deref(),
990 primary_resource_type: self.config.base_stats.resource_type,
991 secondary_resource_type: self.config.base_stats.secondary_resource_type,
992 hints: &self.hints,
993 gating: &self.gating,
994 cost_bypass: &self.cost_bypass,
995 cooldown_bypass: &self.cooldown_bypass,
996 overrides: &self.overrides,
997 })
998 .map_err(EngineError::rotation_compile)?;
999 let buffer_requirements = self.buffer_requirements();
1000 let prepared = bind_rotation(rotation_preparation, &buffer_requirements)
1001 .map_err(EngineError::rotation_compile)?;
1002
1003 let disabled_spell_ids = std::mem::take(&mut self.disabled_spell_ids);
1004 let initial_weapon_imbues = std::mem::take(&mut self.initial_weapon_imbues);
1005 let resource_max = self.resource_max;
1006 let talent_ranks = std::mem::take(&mut self.talent_ranks);
1007 let talent_spell_ids = std::mem::take(&mut self.talent_spell_ids);
1008 let selected_talent_spell_ids = std::mem::take(&mut self.selected_talent_spell_ids);
1009 let selected_replaced_spell_ids = std::mem::take(&mut self.selected_replaced_spell_ids);
1010 let buffer_bindings = BufferBindings::build(
1011 prepared.schema(),
1012 &buffer_requirements,
1013 &self.definitions,
1014 &talent_ranks,
1015 &initial_weapon_imbues,
1016 )
1017 .map_err(EngineError::spec_construction_source)?;
1018 let (absorb_capacity, damage_transfer_capacity) = buffer_bindings
1019 .aura_bindings()
1020 .map(|binding| {
1021 self.definitions.auras[binding.local.as_usize()]
1023 .effects
1024 .iter()
1025 .flatten()
1026 .fold((0, 0), |(absorbs, transfers), effect| match effect {
1027 BuffEffect::Absorb(_) => (absorbs + 1, transfers),
1028 BuffEffect::DamageTransfer(_) => (absorbs, transfers + 1),
1029 _ => (absorbs, transfers),
1030 })
1031 })
1032 .fold((0, 0), |(absorbs, transfers), counts| {
1033 (absorbs + counts.0, transfers + counts.1)
1034 });
1035
1036 self.runtime.scratch.size_absorb_scratch(absorb_capacity);
1037 self.runtime
1038 .scratch
1039 .size_damage_transfer_scratch(damage_transfer_capacity);
1040 self.runtime.health.active_absorbs = crate::state::BoundedVec::new(absorb_capacity);
1041 let rotation_plan = crate::state::program::RotationPlan::compile(
1042 &prepared,
1043 crate::state::program::RotationPlanInputs {
1044 spells: &self.definitions.spells,
1045 spell_by_id: &self.indexes.spell_by_id,
1046 auras: &self.definitions.auras,
1047 aura_by_id: &self.indexes.aura_by_id,
1048 bindings: &buffer_bindings,
1049 passive_driver_effects: &self.definitions.passive_driver_effects,
1050 primary_resource_type: self.config.base_stats.resource_type,
1051 secondary_resource_type: self.config.base_stats.secondary_resource_type,
1052 enemy_count: self.definitions.enemies.len(),
1053 item_use_spells: &self.definitions.item_use_spells,
1054 },
1055 &self.config.game_data,
1056 )
1057 .map_err(EngineError::spec_construction_source)?;
1058
1059 self.runtime
1060 .scratch
1061 .size_rotation_masks(rotation_plan.availability().len());
1062
1063 effects.bind_aura_slots(
1064 &buffer_bindings,
1065 &self.definitions.auras,
1066 &self.config.game_data,
1067 self.definitions.enemies.len(),
1068 );
1069 self.runtime.scratch.health_threshold_triggers =
1070 Vec::with_capacity(effects.health_threshold_capacity());
1071 effects.bind_spell_modifier_actions(
1072 &damage,
1073 &self.definitions.auras,
1074 &self.definitions.passive_driver_effects,
1075 );
1076
1077 if !effects.validate(
1078 self.definitions.auras.len(),
1079 self.definitions.passive_driver_effects.len(),
1080 ) {
1081 return Err(EngineError::spec_construction(
1082 "finalized effect plan contains an out-of-range identity".to_string(),
1083 ));
1084 }
1085
1086 let aura_dependencies = crate::state::program::AuraDependencyPlan::bind(
1087 aura_dependency_graph,
1088 &self.definitions.auras,
1089 prepared.schema(),
1090 &buffer_bindings,
1091 self.definitions.enemies.len(),
1092 );
1093
1094 if !aura_dependencies.validate(self.definitions.auras.len(), self.definitions.spells.len())
1095 {
1096 return Err(EngineError::spec_construction(
1097 "finalized aura dependency plan contains an out-of-range identity".to_string(),
1098 ));
1099 }
1100
1101 let engine = emit_rotation(prepared).map_err(EngineError::rotation_compile)?;
1102 let layout_fingerprint = engine.schema().layout_fingerprint();
1103 let buffer = engine.schema().new_bound_buffer();
1104
1105 let (program, mut runtime) = self.into_program_and_runtime(
1106 FrozenProgramBindings {
1107 scheduled_spells,
1108 runtime_events: wowlab_engine_event_authority::RuntimeEventCatalog::new(),
1109 scheduled_spell_executions,
1110 layout_fingerprint,
1111 buffer_bindings,
1112 },
1113 effects,
1114 (damage, ground_effects),
1115 aura_dependencies,
1116 rotation_plan,
1117 );
1118
1119 runtime.companions.pet_actions = vec![None; program.auto_attacks.len()];
1120 runtime.pools.resize_finalized(
1121 program.buffer_bindings.aura_binding_count(),
1122 program.spells.len(),
1123 );
1124 let mut instance = CombatInstance::new(&program, runtime, buffer)?;
1125
1126 validate_layout_fingerprints(&program, &instance)?;
1127
1128 populate_initial_buffer(
1129 &program,
1130 &mut instance,
1131 &InitialBufferConfig {
1132 resource_max,
1133 talent_ranks: &talent_ranks,
1134 talent_spell_ids: &talent_spell_ids,
1135 selected_talent_spell_ids: &selected_talent_spell_ids,
1136 selected_replaced_spell_ids: &selected_replaced_spell_ids,
1137 },
1138 );
1139
1140 #[cfg(feature = "rotation-optimizations")]
1141 crate::systems::initialize_cooldown_projections(&program, &mut instance);
1142
1143 for spell_id in disabled_spell_ids {
1144 if let Some(local) = program.spell_local(spell_id) {
1145 let slot = instance
1146 .buffer
1147 .spell_mut_at(program.buffer_bindings.spell(local));
1148
1149 slot.is_enabled = 0;
1150 }
1151 }
1152
1153 for (key, active) in initial_weapon_imbues {
1154 if let Some(slot) = program.buffer_weapon_imbue_mut(&mut instance, key) {
1155 slot.is_active = i32::from(active);
1156 slot.is_inactive = i32::from(!active);
1157 }
1158 }
1159
1160 Ok(BuiltCombatSystem {
1161 program,
1162 instance,
1163 rotation: engine,
1164 })
1165 }
1166
1167 pub(crate) fn size_scratch_buffers(&mut self) {
1168 self.runtime.scratch = ScratchBuffers {
1169 absorb_indices: crate::state::BoundedVec::default(),
1170 active_damage_transfers: Vec::new(),
1171 hooks: Vec::with_capacity(self.definitions.player_cast_hooks.len()),
1172 impacts: Vec::with_capacity(self.definitions.impact_procs.len()),
1173 landed_impacts: Vec::with_capacity(self.definitions.landed_impact_procs.len()),
1174 landed_impact_events: Vec::with_capacity(u8::MAX as usize + 1),
1175 accumulating_impacts: Vec::with_capacity(
1176 self.definitions.accumulating_impact_procs.len(),
1177 ),
1178 impact_effects: Vec::with_capacity(self.definitions.impact_effect_procs.len()),
1179 resource_gains: Vec::with_capacity(self.definitions.resource_gain_procs.len()),
1180 rotation_masks: Vec::new(),
1181 locked_action_masks: Vec::new(),
1182 rotation_candidates: Vec::new(),
1183 rotation_trace_reasons: None,
1184 effect_program_frames: Vec::new(),
1185 effect_program_frame_count: 0,
1186 target_workspaces: Vec::new(),
1187 health_threshold_triggers: Vec::new(),
1188 };
1189 self.runtime.scratch.size_execution_scratch(
1190 self.definitions.spell_effects.len(),
1191 self.definitions.spells.len(),
1192 self.definitions.enemies.len(),
1193 );
1194
1195 for (capacity, required) in [
1196 (
1197 self.runtime.scratch.hooks.capacity(),
1198 self.definitions.player_cast_hooks.len(),
1199 ),
1200 (
1201 self.runtime.scratch.impacts.capacity(),
1202 self.definitions.impact_procs.len(),
1203 ),
1204 (
1205 self.runtime.scratch.landed_impacts.capacity(),
1206 self.definitions.landed_impact_procs.len(),
1207 ),
1208 (
1209 self.runtime.scratch.accumulating_impacts.capacity(),
1210 self.definitions.accumulating_impact_procs.len(),
1211 ),
1212 (
1213 self.runtime.scratch.impact_effects.capacity(),
1214 self.definitions.impact_effect_procs.len(),
1215 ),
1216 (
1217 self.runtime.scratch.resource_gains.capacity(),
1218 self.definitions.resource_gain_procs.len(),
1219 ),
1220 ] {
1221 debug_assert!(
1222 capacity >= required,
1223 "scratch capacity must cover its finalized plan"
1224 );
1225 }
1226 }
1227
1228 fn reachable_damage_spell_ids(&self) -> Vec<u32> {
1230 let mut spell_ids = self.spell_ids.values().copied().collect::<Vec<_>>();
1231
1232 spell_ids.extend_from_slice(&self.damage_spell_ids);
1233 spell_ids.extend_from_slice(&self.effect_spell_ids);
1234
1235 for spell in &self.definitions.spells {
1236 spell_ids.extend([
1237 spell.spell_id,
1238 spell.damage_effect.spell_id,
1239 spell.channel.tick_spell_id,
1240 spell.channel.tick_damage_alt_spell_id,
1241 ]);
1242 }
1243
1244 for attack in &self.definitions.auto_attacks {
1245 spell_ids.push(attack.spell_id);
1246 }
1247
1248 for effect in &self.definitions.spell_effects {
1249 match effect {
1250 SpellEffectData::Damage { effect, .. } => spell_ids.push(effect.spell_id),
1251 SpellEffectData::Heal { effect, .. } | SpellEffectData::Energize { effect, .. } => {
1252 if let Some(effect) = effect {
1253 spell_ids.push(effect.spell_id);
1254 }
1255 }
1256 SpellEffectData::Delayed {
1257 profile_spell_id, ..
1258 } => spell_ids.push(*profile_spell_id),
1259 SpellEffectData::Conditional { .. }
1260 | SpellEffectData::ApplyAura { .. }
1261 | SpellEffectData::AddAuraStack { .. }
1262 | SpellEffectData::RemoveAura { .. }
1263 | SpellEffectData::InterruptCast { .. }
1264 | SpellEffectData::Dispel { .. }
1265 | SpellEffectData::EnergizePercent { .. }
1266 | SpellEffectData::ExtendAura { .. }
1267 | SpellEffectData::MutateCooldown { .. } => {}
1268 }
1269 }
1270
1271 for aura in &self.definitions.auras {
1272 spell_ids.extend([
1273 aura.aura_id,
1274 aura.apply_profile_spell_id,
1275 aura.tick_profile_spell_id,
1276 aura.expire_trigger_spell_id,
1277 ]);
1278
1279 if let Some(periodic) = aura.periodic {
1280 spell_ids.push(periodic.damage_spell_id_or(aura.aura_id));
1281
1282 if let Some(effect) = periodic.effect_ref {
1283 spell_ids.push(effect.spell_id);
1284 }
1285 }
1286
1287 for effect in aura.effects.iter().flatten() {
1288 if let Some((spell_id, _)) = effect.dbc_source() {
1289 spell_ids.push(spell_id);
1290 }
1291
1292 if let BuffEffect::DamageReplication(replication) = effect {
1293 spell_ids.push(replication.effect.spell_id);
1294 spell_ids.extend_from_slice(replication.source_spells);
1295 }
1296 }
1297 }
1298
1299 for modifier in &self.definitions.driver_spell_modifiers {
1300 spell_ids.push(modifier.driver_spell_id);
1301 spell_ids.extend_from_slice(modifier.spells);
1302 }
1303
1304 for driver in &self.definitions.passive_driver_effects {
1305 spell_ids.push(driver.driver_spell_id);
1306
1307 if let Some((spell_id, _)) = driver.effect.dbc_source() {
1308 spell_ids.push(spell_id);
1309 }
1310
1311 if let BuffEffect::DamageReplication(replication) = driver.effect {
1312 spell_ids.push(replication.effect.spell_id);
1313 spell_ids.extend_from_slice(replication.source_spells);
1314 }
1315 }
1316
1317 for driver in &self.definitions.periodic_drivers {
1318 spell_ids.push(driver.driver_spell_id);
1319 }
1320
1321 for proc in &self.definitions.impact_effect_procs {
1322 spell_ids.extend([proc.driver_spell_id, proc.profile_spell_id]);
1323
1324 if let crate::state::ImpactSource::Spell(spell_id) = proc.source {
1325 spell_ids.push(spell_id);
1326 }
1327 }
1328
1329 spell_ids.retain(|spell_id| *spell_id != 0);
1330 spell_ids.sort_unstable();
1331 spell_ids.dedup();
1332
1333 spell_ids
1334 }
1335
1336 fn buffer_requirements(&self) -> BufferRequirements {
1338 let mut requirements = BufferRequirements::new();
1339
1340 for spell in &self.definitions.spells {
1341 let spell = SpellIdx::from_raw(spell.spell_id);
1342
1343 requirements.spell(spell);
1344 requirements.cooldown(spell);
1345 requirements.history(spell);
1346 }
1347
1348 if let Some(resource) = self.config.base_stats.resource_type {
1349 requirements.resource(resource);
1350 }
1351
1352 if let Some(resource) = self.config.base_stats.secondary_resource_type {
1353 requirements.resource(resource);
1354 }
1355
1356 let pets = PetIdx::iter(PetIdx::MAX)
1357 .map(ActorId::Pet)
1358 .collect::<Vec<_>>();
1359 let base_actors = [ActorId::Player, ActorId::External];
1360 let mut actors =
1361 Vec::with_capacity(base_actors.len() + pets.len() + self.definitions.enemies.len());
1362
1363 actors.extend(base_actors);
1364
1365 actors.extend(pets.iter().copied());
1366
1367 actors.extend(
1368 self.definitions
1369 .enemies
1370 .iter()
1371 .map(|enemy| ActorId::Enemy(enemy.id())),
1372 );
1373
1374 for aura in &self.definitions.auras {
1375 let aura_idx = AuraIdx(aura.aura_id);
1376
1377 match aura.on {
1378 AuraOn::Player => {
1379 for &source in &actors {
1380 requirements.aura(AuraKey::new(
1381 aura_idx,
1382 source,
1383 ActorId::Player,
1384 AuraOn::Player,
1385 ));
1386 }
1387 }
1388 AuraOn::Target => {
1389 for enemy in &self.definitions.enemies {
1390 for &source in &actors {
1391 requirements.aura(AuraKey::new(
1392 aura_idx,
1393 source,
1394 ActorId::Enemy(enemy.id()),
1395 AuraOn::Target,
1396 ));
1397 }
1398 }
1399 }
1400 AuraOn::Pet => {
1401 requirements.aura(AuraKey::new(
1402 aura_idx,
1403 ActorId::Player,
1404 ActorId::Pet(PetIdx::PRIMARY),
1405 AuraOn::Pet,
1406 ));
1407
1408 for &pet in &pets {
1409 requirements.aura(AuraKey::new(aura_idx, pet, pet, AuraOn::Pet));
1410 }
1411 }
1412 }
1413
1414 for &affected in &actors {
1418 requirements.aura(AuraKey::new(aura_idx, ActorId::Player, affected, aura.on));
1419 }
1420
1421 for &affected in &actors {
1422 requirements.aura(AuraKey::new(aura_idx, ActorId::External, affected, aura.on));
1423 }
1424
1425 for &pet in &pets {
1426 requirements.aura(AuraKey::new(aura_idx, pet, pet, aura.on));
1427 requirements.aura(AuraKey::new(aura_idx, pet, ActorId::Player, aura.on));
1428
1429 for enemy in &self.definitions.enemies {
1430 requirements.aura(AuraKey::new(
1431 aura_idx,
1432 pet,
1433 ActorId::Enemy(enemy.id()),
1434 aura.on,
1435 ));
1436 }
1437 }
1438
1439 for enemy in &self.definitions.enemies {
1440 let source = ActorId::Enemy(enemy.id());
1441
1442 for &affected in &actors {
1443 requirements.aura(AuraKey::new(aura_idx, source, affected, aura.on));
1444 }
1445 }
1446
1447 requirements.aura(AuraKey::new(
1450 aura_idx,
1451 ActorId::Player,
1452 ActorId::Player,
1453 AuraOn::Player,
1454 ));
1455
1456 if aura.propagates_to_pet {
1457 for &source in &actors {
1458 requirements.aura(AuraKey::new(
1459 aura_idx,
1460 source,
1461 ActorId::Pet(PetIdx::PRIMARY),
1462 AuraOn::Pet,
1463 ));
1464 }
1465 }
1466 }
1467
1468 for (name, _) in &self.talent_ranks {
1469 requirements.talent(name.as_str());
1470 }
1471
1472 for (index, _) in self.definitions.auto_attacks.iter().enumerate() {
1473 requirements.swing(crate::keys::auto_attack_key(index));
1474 }
1475
1476 for &(slot, _) in &self.definitions.item_use_spells {
1477 requirements.item(slot.slug());
1478 }
1479
1480 for &(key, _) in &self.initial_weapon_imbues {
1481 requirements.weapon_imbue(key);
1482 }
1483
1484 requirements.unit(crate::keys::UnitKey::TARGET);
1485
1486 requirements
1487 }
1488
1489 fn validate_identities(&self) -> Result<(), EngineError> {
1491 let mut spell_ids = IntMap::<u32, LocalSpellIdx>::default();
1492
1493 for (raw, spell) in self.definitions.spells.iter().enumerate() {
1494 let local = LocalSpellIdx::new(u8::try_from(raw).map_err(|error| {
1495 EngineError::spec_construction(format!("spell index {raw} exceeds u8: {error}"))
1496 })?);
1497
1498 if let Some(previous) = spell_ids.insert(spell.spell_id, local) {
1499 return Err(EngineError::spec_construction(format!(
1500 "duplicate spell identity {} at local indexes {} and {}",
1501 spell.spell_id,
1502 previous.raw(),
1503 local.raw()
1504 )));
1505 }
1506 }
1507
1508 let mut aura_ids = FastMap::<(u32, AuraOn), LocalAuraIdx>::default();
1509
1510 for (raw, aura) in self.definitions.auras.iter().enumerate() {
1511 let local = LocalAuraIdx::new(u8::try_from(raw).map_err(|error| {
1512 EngineError::spec_construction(format!("aura index {raw} exceeds u8: {error}"))
1513 })?);
1514
1515 if let Some(previous) = aura_ids.insert((aura.aura_id, aura.on), local) {
1516 return Err(EngineError::spec_construction(format!(
1517 "duplicate aura identity ({}, {:?}) at local indexes {} and {}",
1518 aura.aura_id,
1519 aura.on,
1520 previous.raw(),
1521 local.raw()
1522 )));
1523 }
1524 }
1525
1526 for (&spell_id, &local) in &self.indexes.spell_by_id {
1527 if self
1528 .definitions
1529 .spells
1530 .get(local.as_usize())
1531 .is_none_or(|spell| spell.spell_id != spell_id)
1532 {
1533 return Err(EngineError::spec_construction(format!(
1534 "unresolved spell index binding {spell_id} -> {}",
1535 local.raw()
1536 )));
1537 }
1538 }
1539
1540 Ok(())
1541 }
1542
1543 fn into_program_and_runtime(
1546 self,
1547 bindings: FrozenProgramBindings,
1548 effects: crate::state::program::EffectPlan,
1549 damage_plans: (
1550 crate::state::program::DamagePlan,
1551 Box<[crate::state::program::GroundEffectPlan]>,
1552 ),
1553 aura_dependencies: crate::state::program::AuraDependencyPlan,
1554 rotation: crate::state::program::RotationPlan,
1555 ) -> (CombatProgram, CombatRuntime) {
1556 let (damage, ground_effects) = damage_plans;
1557 let spec_identity_brand = self.spec_identity_brand;
1558 let FrozenProgramBindings {
1559 scheduled_spells,
1560 runtime_events,
1561 scheduled_spell_executions,
1562 layout_fingerprint,
1563 buffer_bindings,
1564 } = bindings;
1565
1566 let DraftDefinitions {
1567 enemies,
1568 spells,
1569 empower_cast_times_ms,
1570 spell_effects,
1571 auras,
1572 aura_bindings,
1573 spell_bindings,
1574 auto_attacks,
1575 cast_hooks,
1576 empower_release_hooks,
1577 tick_hooks,
1578 player_cast_hooks,
1579 resource_threshold_triggers,
1580 impact_procs,
1581 landed_impact_procs,
1582 accumulating_impact_procs,
1583 impact_effect_procs,
1584 driver_spell_modifiers,
1585 passive_driver_effects,
1586 periodic_drivers,
1587 talent_ranks,
1588 resource_gain_procs,
1589 item_use_spells,
1590 rppm_trackers,
1591 rppm_bindings,
1592 system_rppm_bindings,
1593 threshold_trackers,
1594 weapon_enchant_bindings,
1595 weapon_imbue_bindings,
1596 precombat_auras,
1597 talent_aura_stacks,
1598 stealth_aura_id,
1599 } = self.definitions;
1600 let DraftIndexes {
1601 spell_by_id,
1602 aura_by_id,
1603 cooldown_categories,
1604 charge_categories,
1605 item_rppm_indices,
1606 system_rppm_indices,
1607 enchant_rppm_indices,
1608 enchant_buff_spell_ids,
1609 ..
1610 } = self.indexes;
1611
1612 let mut runtime = self.runtime;
1613
1614 runtime.procs.rppm_trackers = rppm_trackers;
1615 runtime.procs.threshold_trackers = threshold_trackers;
1616 runtime.procs.accumulating_impact_accumulators = accumulating_impact_procs
1617 .iter()
1618 .map(|proc| proc.accumulator)
1619 .collect();
1620 runtime.procs.resource_gain_attempts = resource_gain_procs
1621 .iter()
1622 .map(|proc| proc.attempts)
1623 .collect();
1624 runtime.procs.impact_effect_ready_at = vec![0.0; impact_effect_procs.len()];
1625 runtime
1626 .procs
1627 .resize_target_icds_for_known_targets(impact_effect_procs.len());
1628 runtime.procs.impact_effect_charges = impact_effect_procs
1629 .iter()
1630 .map(|proc| {
1631 if proc.uses_stacks_for_charges {
1632 0
1633 } else {
1634 proc.charges
1635 }
1636 })
1637 .collect();
1638 runtime.procs.impact_effect_rng = impact_effect_procs
1639 .iter()
1640 .map(crate::state::ImpactEffectRngState::from_proc)
1641 .collect();
1642
1643 let telemetry_targets = crate::state::program::finalize_target_metadata(&enemies);
1644 let talent_ranks = damage.dense_talent_ranks(&talent_ranks);
1645 let weapon_enchant_bindings = weapon_enchant_bindings
1646 .into_iter()
1647 .map(|binding| crate::state::ResolvedWeaponEnchantBinding {
1648 rppm: *enchant_rppm_indices
1649 .get(&binding.enchantment_id)
1650 .expect("registered weapon enchant has a finalized RPPM coordinate"),
1651 buff_aura: *aura_by_id
1652 .get(&binding.buff_spell_id)
1653 .expect("registered weapon enchant has a finalized buff-aura coordinate"),
1654 })
1655 .collect();
1656 let aura_bindings = {
1657 let len = aura_bindings
1658 .iter()
1659 .map(|(binding, _)| binding.as_usize() + 1)
1660 .max()
1661 .unwrap_or(0);
1662 let mut resolved = vec![None; len];
1663
1664 for (binding, aura_id) in aura_bindings {
1665 resolved[binding.as_usize()] = aura_by_id.get(&aura_id).copied();
1667 }
1668
1669 resolved.into_boxed_slice()
1670 };
1671 let spell_bindings = {
1672 let len = spell_bindings
1673 .iter()
1674 .map(|(binding, _)| binding.as_usize() + 1)
1675 .max()
1676 .unwrap_or(0);
1677 let mut resolved = vec![None; len];
1678
1679 for (binding, spell_id) in spell_bindings {
1680 resolved[binding.as_usize()] = spell_by_id.get(&spell_id).copied();
1682 }
1683
1684 resolved.into_boxed_slice()
1685 };
1686 let rppm_bindings = {
1687 let len = rppm_bindings
1688 .iter()
1689 .map(|(binding, _)| binding.as_usize() + 1)
1690 .max()
1691 .unwrap_or(0);
1692 let mut resolved = vec![None; len];
1693
1694 for (binding, local) in rppm_bindings {
1695 resolved[binding.as_usize()] = Some(local);
1697 }
1698
1699 resolved.into_boxed_slice()
1700 };
1701 let system_rppm_bindings = {
1702 let len = system_rppm_bindings
1703 .iter()
1704 .map(|(binding, _)| binding.as_usize() + 1)
1705 .max()
1706 .unwrap_or(0);
1707 let mut resolved = vec![None; len];
1708
1709 for (binding, value) in system_rppm_bindings {
1710 resolved[binding.as_usize()] = Some(value);
1712 }
1713
1714 resolved.into_boxed_slice()
1715 };
1716 let weapon_imbue_bindings = weapon_imbue_bindings
1717 .into_iter()
1718 .map(|binding| crate::state::ResolvedWeaponImbueBinding {
1719 spell_local: *spell_by_id
1720 .get(&binding.spell_id)
1721 .expect("registered weapon imbue has a finalized spell coordinate"),
1722 slot: buffer_bindings
1723 .weapon_imbue(binding.key)
1724 .expect("registered weapon imbue has a finalized buffer coordinate"),
1725 })
1726 .collect::<Box<[_]>>();
1727 let mut weapon_imbue_by_spell = vec![None; spells.len()];
1728
1729 for binding in &weapon_imbue_bindings {
1730 weapon_imbue_by_spell[binding.spell_local.as_usize()] = Some(binding.slot);
1732 }
1733
1734 let weapon_imbue_by_spell = weapon_imbue_by_spell.into_boxed_slice();
1735
1736 runtime.procs.system_rppm_ready_at = vec![SimTime::ZERO; system_rppm_bindings.len()];
1737 let aura_followups = auras
1738 .iter()
1739 .map(|aura| {
1740 (aura.application_followup_aura_id != 0)
1741 .then(|| aura_by_id.get(&aura.application_followup_aura_id).copied())
1742 .flatten()
1743 })
1744 .collect::<Vec<_>>()
1745 .into_boxed_slice();
1746 let aura_exclusions = auras
1747 .iter()
1748 .map(|aura| {
1749 std::array::from_fn(|index| {
1750 let aura_id = aura.application_excluded_auras[index];
1752
1753 (aura_id != 0)
1754 .then(|| aura_by_id.get(&aura_id).copied())
1755 .flatten()
1756 })
1757 })
1758 .collect::<Vec<_>>()
1759 .into_boxed_slice();
1760 let stealth_aura = stealth_aura_id.and_then(|aura_id| aura_by_id.get(&aura_id).copied());
1761 let external_buff_auras = self
1762 .config
1763 .external_buffs
1764 .iter()
1765 .map(|config| aura_by_id.get(&config.spell_id).copied())
1766 .collect::<Vec<_>>()
1767 .into_boxed_slice();
1768 let proc_category_recovery_ms = crate::generated_identities::proc_category::ALL
1769 .iter()
1770 .map(|(_, driver)| {
1771 self.config
1772 .game_data
1773 .proc_category_recovery_ms(SpellIdx::from_raw(*driver))
1774 .unwrap_or(0)
1775 })
1776 .collect::<Vec<_>>()
1777 .into_boxed_slice();
1778
1779 runtime.procs.proc_category_ready_at_dense =
1780 vec![SimTime::ZERO; proc_category_recovery_ms.len()];
1781 let periodic_driver_bindings = crate::generated_identities::periodic_driver::ALL
1782 .iter()
1783 .map(|driver_spell_id| {
1784 periodic_drivers
1785 .iter()
1786 .position(|driver| driver.driver_spell_id == *driver_spell_id)
1787 })
1788 .collect::<Vec<_>>()
1789 .into_boxed_slice();
1790 let empty_category_members: &[LocalSpellIdx] = &[];
1791 let cooldown_category_members = spells
1792 .iter()
1793 .map(|spell| {
1794 spell
1795 .cooldown
1796 .cooldown_category
1797 .map_or(empty_category_members, |category| {
1798 cooldown_categories.members(&category)
1799 })
1800 .to_vec()
1801 .into_boxed_slice()
1802 })
1803 .collect();
1804 let charge_category_members = spells
1805 .iter()
1806 .map(|spell| {
1807 spell
1808 .cooldown
1809 .charge_category
1810 .map_or(empty_category_members, |category| {
1811 charge_categories.members(&category)
1812 })
1813 .to_vec()
1814 .into_boxed_slice()
1815 })
1816 .collect();
1817 let charge_category_owners = spells
1818 .iter()
1819 .enumerate()
1820 .map(|(index, spell)| {
1821 spell
1822 .cooldown
1823 .charge_category
1824 .and_then(|category| charge_categories.owner(&category).copied())
1825 .unwrap_or_else(|| {
1826 LocalSpellIdx::new(
1827 u8::try_from(index).expect("finalized spell count fits local identity"),
1828 )
1829 })
1830 })
1831 .collect();
1832 let start_recovery_category_owners = spells
1833 .iter()
1834 .enumerate()
1835 .map(|(index, spell)| {
1836 let owner = spell.cooldown.start_recovery_category.and_then(|category| {
1837 spells.iter().position(|candidate| {
1838 candidate.cooldown.start_recovery_category == Some(category)
1839 })
1840 });
1841 let owner = owner.unwrap_or(index);
1842
1843 LocalSpellIdx::new(
1844 u8::try_from(owner).expect("finalized spell count fits local identity"),
1845 )
1846 })
1847 .collect();
1848 let aura_expire_trigger_spells = auras
1849 .iter()
1850 .map(|aura| {
1851 (aura.expire_trigger_spell_id != 0)
1852 .then(|| spell_by_id.get(&aura.expire_trigger_spell_id).copied())
1853 .flatten()
1854 })
1855 .collect();
1856 let pet_action_plans: Box<[u16]> = auto_attacks
1857 .iter()
1858 .enumerate()
1859 .filter(|(_, attack)| attack.pet_action_bar.is_some())
1860 .map(|(index, _)| u16::try_from(index).expect("finalized auto-attack count fits u16"))
1861 .collect();
1862 let primary_pet_action_auto = pet_action_plans.first().copied();
1863 let pet_auto_attacks = auto_attacks
1864 .iter()
1865 .enumerate()
1866 .filter(|(_, attack)| attack.is_pet && attack.swing_ms > 0)
1867 .map(|(index, _)| u16::try_from(index).expect("finalized auto-attack count fits u16"))
1868 .collect();
1869 let mut pet_action_plan_by_auto = vec![None; auto_attacks.len()];
1870
1871 for (plan, auto) in pet_action_plans.iter().copied().enumerate() {
1872 pet_action_plan_by_auto[usize::from(auto)] =
1874 Some(u16::try_from(plan).expect("finalized pet plan count fits u16"));
1875 }
1876
1877 let program = CombatProgram {
1878 spec_identity_brand,
1879 enemies,
1880 telemetry_targets,
1881 spells,
1882 scheduled_spells,
1883 runtime_events,
1884 scheduled_spell_executions,
1885 empower_cast_times_ms,
1886 spell_effects,
1887 auras,
1888 aura_bindings,
1889 spell_bindings,
1890 rppm_bindings,
1891 system_rppm_bindings,
1892 aura_followups,
1893 aura_exclusions,
1894 aura_expire_trigger_spells,
1895 external_buff_auras,
1896 proc_category_recovery_ms,
1897 auto_attacks,
1898 pet_action_plans,
1899 pet_action_plan_by_auto: pet_action_plan_by_auto.into_boxed_slice(),
1900 primary_pet_action_auto,
1901 pet_auto_attacks,
1902 cast_hooks,
1903 empower_release_hooks,
1904 tick_hooks,
1905 player_cast_hooks,
1906 resource_threshold_triggers,
1907 impact_procs,
1908 landed_impact_procs,
1909 accumulating_impact_procs,
1910 impact_effect_procs,
1911 driver_spell_modifiers,
1912 passive_driver_effects,
1913 effects,
1914 damage,
1915 ground_effects,
1916 aura_dependencies,
1917 rotation,
1918 periodic_drivers,
1919 periodic_driver_bindings,
1920 talent_ranks,
1921 resource_gain_procs,
1922 item_use_spells,
1923 weapon_enchant_bindings,
1924 weapon_imbue_bindings,
1925 weapon_imbue_by_spell,
1926 precombat_auras,
1927 talent_aura_stacks,
1928 stealth_aura,
1929 spell_by_id,
1930 #[cfg(any(test, feature = "test-support"))]
1931 aura_by_id,
1932 cooldown_category_members,
1933 charge_category_members,
1934 charge_category_owners,
1935 start_recovery_category_owners,
1936 item_rppm_indices,
1937 system_rppm_indices,
1938 enchant_rppm_indices,
1939 enchant_buff_spell_ids,
1940 config: self.config,
1941 names: self.names,
1942 layout_fingerprint,
1943 buffer_bindings,
1944 };
1945
1946 (program, runtime)
1947 }
1948}
1949
1950fn validate_layout_fingerprints(
1951 program: &CombatProgram,
1952 instance: &CombatInstance,
1953) -> Result<(), EngineError> {
1954 if program.layout_fingerprint == instance.buffer.layout_fingerprint() {
1955 return Ok(());
1956 }
1957
1958 Err(EngineError::spec_construction(format!(
1959 "program/buffer layout fingerprint mismatch: program={}, buffer={}",
1960 program.layout_fingerprint.as_u64(),
1961 instance.buffer.layout_fingerprint().as_u64(),
1962 )))
1963}
1964
1965#[cfg(test)]
1966#[path = "draft_tests.rs"]
1967mod tests;