Skip to main content

wowlab_engine_combat/state/program/
aura_dependencies.rs

1// #t(file: rust_duplicate_strings) finalization bound diagnostics deliberately share the same invariant wording across finalized plans.
2// #t(file: rust_similar_fns) actor-range indexing intentionally mirrors the effect plan while retaining dependency-specific element types.
3// #t(file: rust_similar_structs) graph entries and bound entries intentionally share semantic ranges across the explicit bind transition.
4
5//! Finalized aura-to-consumer and aura-to-local-identity adjacency.
6
7use std::ops::Range;
8
9use wowlab_engine_domain::{
10    dbc::{ResolvedGameDataSemanticExt as _, StatModifierKind},
11    rotation::{AuraProjectionSlotHandle, AuraSlotHandle, ContextSchema, SpellSlotHandle},
12};
13use wowlab_types::{
14    data::ResourceCostPercentBasis,
15    game::Attribute,
16    sim::{ActorId, AuraIdx, AuraKey, AuraOn, EnemyIdx, PetIdx, SpellIdx},
17};
18
19use super::{BufferBindings, ConcreteAuraBinding, ProgramConfig};
20use crate::state::{
21    AuraData, BuffEffect, LocalAuraIdx, LocalSpellIdx, SpellData, SpellEffectData, SpellGroupRule,
22};
23
24bitflags::bitflags! {
25    /// Immediate non-projection work named by one finalized aura definition.
26    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27    pub(crate) struct AuraDependencies: u16 {
28        const GATES = 1 << 0;
29        const PRIMARY_STAT = 1 << 1;
30        const STAMINA = 1 << 2;
31        const RECHARGE_RATE = 1 << 3;
32        const MAXIMUM_CHARGES = 1 << 4;
33        const AURA_TIME = 1 << 5;
34        const PLAYER_SPEED = 1 << 6;
35        const COMPANION_SPEED = 1 << 7;
36        const MAXIMUM_RESOURCE = 1 << 8;
37        const PET_STATS = 1 << 9;
38        const RESOURCE_REGEN = 1 << 10;
39        const PET_ACTIONS = 1 << 11;
40        const TARGET_RANGE = 1 << 12;
41    }
42}
43
44#[derive(Clone, Debug, Default)]
45struct AuraDependencyEntry {
46    consumers: AuraDependencies,
47    costs: Range<usize>,
48    recharge: Range<usize>,
49    maximum_charges: Range<usize>,
50    gates: Range<usize>,
51    aura_timing: Range<usize>,
52    projections: Range<usize>,
53    exclusive_spells: Range<usize>,
54}
55
56#[derive(Clone, Debug, Default)]
57struct AuraDependencyGraphEntry {
58    consumers: AuraDependencies,
59    costs: Range<usize>,
60    recharge: Range<usize>,
61    maximum_charges: Range<usize>,
62    gates: Range<usize>,
63    aura_timing: Range<usize>,
64    exclusive_spells: Range<usize>,
65}
66
67#[derive(Clone, Copy, Debug)]
68struct UnboundExclusiveSpellDependency {
69    local: LocalSpellIdx,
70    enabled: bool,
71}
72
73/// Layout-free flattened dependency adjacency compiled before rotation preparation.
74#[derive(Debug)]
75pub(crate) struct AuraDependencyGraph {
76    entries: Box<[AuraDependencyGraphEntry]>,
77    cost_spells: Box<[LocalSpellIdx]>,
78    recharge_spells: Box<[LocalSpellIdx]>,
79    maximum_charge_spells: Box<[LocalSpellIdx]>,
80    gate_spells: Box<[LocalSpellIdx]>,
81    aura_timing: Box<[LocalAuraIdx]>,
82    exclusive_spells: Box<[UnboundExclusiveSpellDependency]>,
83}
84
85#[derive(Clone, Debug, Default)]
86struct ConcreteAuraDependencyEntry {
87    exclusive_peers: Range<usize>,
88}
89
90const FIXED_ACTOR_BUCKET_COUNT: usize = 2;
91
92#[derive(Clone, Debug)]
93struct ActorRanges<T> {
94    entries: Box<[T]>,
95    player: Range<usize>,
96    external: Range<usize>,
97    pets: Box<[Range<usize>]>,
98    enemies: Box<[Range<usize>]>,
99}
100
101#[derive(Clone, Copy, Debug)]
102pub(crate) struct ControlPeerDependency {
103    pub(crate) handle: AuraSlotHandle,
104    pub(crate) matches_kind: bool,
105    pub(crate) matches_group: bool,
106    pub(crate) permanent: bool,
107}
108
109impl<T> ActorRanges<T> {
110    fn for_actor(&self, actor: ActorId) -> &[T] {
111        let range = match actor {
112            ActorId::Player => Some(self.player.clone()),
113            ActorId::External => Some(self.external.clone()),
114            ActorId::Pet(pet) => self.pets.get(pet.as_usize()).cloned(),
115            ActorId::Enemy(enemy) => self.enemies.get(enemy.as_usize()).cloned(),
116        };
117
118        range.map_or(&[], |range| {
119            // BOUNDS: every actor range was assembled from this exact flattened element array.
120            &self.entries[range]
121        })
122    }
123}
124
125impl<T> Default for ActorRanges<T> {
126    fn default() -> Self {
127        Self {
128            entries: Box::default(),
129            player: 0..0,
130            external: 0..0,
131            pets: Box::default(),
132            enemies: Box::default(),
133        }
134    }
135}
136
137#[derive(Clone, Copy, Debug)]
138pub(crate) struct ProjectionDependency {
139    pub(crate) destination: AuraProjectionSlotHandle,
140    source_offset: usize,
141    index: AuraProjectionDependencyIdx,
142}
143
144/// Stable finalized identity for one aura projection dependency.
145#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
146pub(crate) struct AuraProjectionDependencyIdx(usize);
147
148impl AuraProjectionDependencyIdx {
149    #[expect(
150        clippy::trivially_copy_pass_by_ref,
151        reason = "C-CONV requires borrowed as_ receivers"
152    )]
153    pub(crate) const fn as_usize(&self) -> usize {
154        self.0
155    }
156}
157
158/// One exact spell-slot update for an applied exclusive-group aura.
159#[derive(Clone, Copy, Debug)]
160pub(crate) struct ExclusiveSpellDependency {
161    pub(crate) handle: SpellSlotHandle,
162    pub(crate) enabled: bool,
163}
164
165/// One exact finalized spell consumer and its construction-validated slot.
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub(crate) struct SpellConsumerBinding {
168    pub(crate) local: LocalSpellIdx,
169    pub(crate) handle: SpellSlotHandle,
170}
171
172/// Flattened finalized dependency graph indexed by stable local aura identity.
173#[derive(Debug)]
174pub(crate) struct AuraDependencyPlan {
175    entries: Box<[AuraDependencyEntry]>,
176    concrete_entries: Box<[ConcreteAuraDependencyEntry]>,
177    cost_spells: Box<[SpellConsumerBinding]>,
178    recharge_spells: Box<[LocalSpellIdx]>,
179    maximum_charge_spells: Box<[LocalSpellIdx]>,
180    gate_spells: Box<[SpellConsumerBinding]>,
181    aura_timing: Box<[LocalAuraIdx]>,
182    aura_timing_bindings: Box<[ActorRanges<ConcreteAuraBinding>]>,
183    control_peers: Box<[ActorRanges<ControlPeerDependency>]>,
184    projections: Box<[ProjectionDependency]>,
185    projection_sources: Box<[Option<AuraSlotHandle>]>,
186    projection_target_count: usize,
187    exclusive_peers: Box<[ConcreteAuraBinding]>,
188    exclusive_spells: Box<[ExclusiveSpellDependency]>,
189}
190
191impl AuraDependencyGraph {
192    // #t(fn: rust_max_fn_lines) the finalizer assembles every semantic dependency family in one local-aura transaction before layout freeze.
193    // #t(fn: rust_cyclomatic_complexity) every BuffEffect selector contributes to an explicit finalized consumer family.
194    // #t(fn: rust_large_fn_params) compilation explicitly receives the finalized definition and effect inputs whose identities it joins.
195    pub(crate) fn compile(
196        auras: &[AuraData],
197        spells: &[SpellData],
198        spell_effects: &[SpellEffectData],
199        config: &ProgramConfig,
200    ) -> Self {
201        let mut entries = Vec::with_capacity(auras.len());
202        let mut cost_spells = Vec::new();
203        let mut recharge_spells = Vec::new();
204        let mut maximum_charge_spells = Vec::new();
205        let mut gate_spells = Vec::new();
206        let mut aura_timing = Vec::new();
207        let mut exclusive_spells = Vec::new();
208
209        for (raw_local, aura) in auras.iter().enumerate() {
210            let local = LocalAuraIdx::new(
211                u8::try_from(raw_local).expect("finalized aura count is validated below 256"),
212            );
213            let mut consumers = AuraDependencies::empty();
214            let mut local_costs = Vec::new();
215            let mut local_recharge = Vec::new();
216            let mut local_maximum_charges = Vec::new();
217            let mut local_gates = Vec::new();
218            let mut local_timing = Vec::new();
219
220            for effect in aura.effects.iter().flatten() {
221                classify_consumers(effect, config, &mut consumers);
222                collect_cost_spells(effect, spells, config, &mut local_costs);
223                collect_recharge_spells(effect, spells, config, &mut local_recharge);
224                collect_maximum_charge_spells(effect, spells, &mut local_maximum_charges);
225                collect_gate_spells(effect, spells, &mut local_gates);
226                collect_aura_timing(effect, auras, config, &mut local_timing);
227            }
228
229            if consumers.contains(AuraDependencies::MAXIMUM_RESOURCE) {
230                local_costs.extend(
231                    spells
232                        .iter()
233                        .enumerate()
234                        .filter(|&(_index, spell)| spell_uses_maximum_resource_cost(spell))
235                        .map(|(index, _spell)| local_spell(index)),
236                );
237            }
238
239            normalize(&mut local_costs);
240            normalize(&mut local_recharge);
241            normalize(&mut local_maximum_charges);
242            normalize(&mut local_gates);
243            normalize(&mut local_timing);
244
245            let costs = append_range(&mut cost_spells, local_costs);
246            let recharge = append_range(&mut recharge_spells, local_recharge);
247            let maximum_charges = append_range(&mut maximum_charge_spells, local_maximum_charges);
248            let gates = append_range(&mut gate_spells, local_gates);
249            let aura_timing = append_range(&mut aura_timing, local_timing);
250            let local_exclusive_spells =
251                collect_exclusive_spell_dependencies(local, aura, auras, spells, spell_effects);
252            let exclusive_spells = append_range(&mut exclusive_spells, local_exclusive_spells);
253
254            entries.push(AuraDependencyGraphEntry {
255                consumers,
256                costs,
257                recharge,
258                maximum_charges,
259                gates,
260                aura_timing,
261                exclusive_spells,
262            });
263        }
264
265        Self {
266            entries: entries.into_boxed_slice(),
267            cost_spells: cost_spells.into_boxed_slice(),
268            recharge_spells: recharge_spells.into_boxed_slice(),
269            maximum_charge_spells: maximum_charge_spells.into_boxed_slice(),
270            gate_spells: gate_spells.into_boxed_slice(),
271            aura_timing: aura_timing.into_boxed_slice(),
272            exclusive_spells: exclusive_spells.into_boxed_slice(),
273        }
274    }
275}
276
277impl AuraDependencyPlan {
278    // #t(fn: rust_max_fn_lines) the finalizer assembles every dependency family in one auditable local-aura transaction.
279    // #t(fn: rust_cyclomatic_complexity) every BuffEffect selector contributes to an explicit finalized consumer family.
280    // #t(fn: rust_large_fn_params) compilation explicitly receives the finalized definition, effect, layout, binding, and encounter inputs it joins.
281    pub(crate) fn bind(
282        graph: AuraDependencyGraph,
283        auras: &[AuraData],
284        schema: &ContextSchema,
285        bindings: &BufferBindings,
286        enemy_count: usize,
287    ) -> Self {
288        let mut entries = Vec::with_capacity(auras.len());
289        let cost_spells = bind_spell_consumers(graph.cost_spells.iter().copied(), bindings);
290        let gate_spells = bind_spell_consumers(graph.gate_spells.iter().copied(), bindings);
291        let mut projections = Vec::new();
292        let mut projection_sources = Vec::new();
293        let exclusive_spells = graph
294            .exclusive_spells
295            .iter()
296            .map(|dependency| ExclusiveSpellDependency {
297                handle: bindings.spell(dependency.local),
298                enabled: dependency.enabled,
299            })
300            .collect::<Vec<_>>();
301        let projection_target_count = enemy_count.saturating_add(1);
302
303        for (aura, graph_entry) in auras.iter().zip(graph.entries.iter()) {
304            let projection_start = projections.len();
305
306            schema
307                .buffer_finalizer()
308                .visit_aura_projections(|projection, destination| {
309                    if projection.aura().as_u32() != aura.aura_id || projection.on() != aura.on {
310                        return;
311                    }
312
313                    let source_offset = projection_sources.len();
314
315                    for target_index in 0..projection_target_count {
316                        let target = (target_index > 0).then(|| {
317                            EnemyIdx(
318                                u16::try_from(target_index - 1)
319                                    .expect("finalized enemy count fits in EnemyIdx"),
320                            )
321                        });
322                        let source =
323                            exact_projection_key(projection.aura(), projection.on(), target)
324                                .and_then(|key| schema.buffer_finalizer().bind_aura(key));
325
326                        projection_sources.push(source);
327                    }
328
329                    projections.push(ProjectionDependency {
330                        destination,
331                        source_offset,
332                        index: AuraProjectionDependencyIdx(projections.len()),
333                    });
334                });
335
336            let projection_range = projection_start..projections.len();
337
338            entries.push(AuraDependencyEntry {
339                consumers: graph_entry.consumers,
340                costs: graph_entry.costs.start..graph_entry.costs.end,
341                recharge: graph_entry.recharge.start..graph_entry.recharge.end,
342                maximum_charges: graph_entry.maximum_charges.start..graph_entry.maximum_charges.end,
343                gates: graph_entry.gates.start..graph_entry.gates.end,
344                aura_timing: graph_entry.aura_timing.start..graph_entry.aura_timing.end,
345                projections: projection_range,
346                exclusive_spells: graph_entry.exclusive_spells.start
347                    ..graph_entry.exclusive_spells.end,
348            });
349        }
350
351        let (concrete_entries, exclusive_peers) = compile_exclusive_peers(auras, bindings);
352        let aura_timing_bindings = entries
353            .iter()
354            .map(|entry| {
355                // BOUNDS: entry timing ranges were appended to this exact flattened local array.
356                let locals = &graph.aura_timing[entry.aura_timing.clone()];
357                let bucket_count = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy_count;
358                let mut buckets = vec![Vec::new(); bucket_count];
359                let mut local_membership = vec![false; auras.len()];
360
361                for &local in locals {
362                    // BOUNDS: timing locals were compiled from this finalized aura slice.
363                    local_membership[local.as_usize()] = true;
364                }
365
366                for binding in bindings.aura_bindings() {
367                    // BOUNDS: concrete bindings contain locals from this finalized aura slice.
368                    if !local_membership[binding.local.as_usize()] {
369                        continue;
370                    }
371
372                    if let Some(bucket) = concrete_actor_bucket(binding.key.affected(), enemy_count)
373                    {
374                        // BOUNDS: concrete_actor_bucket validates the returned bucket.
375                        buckets[bucket].push(binding);
376                    }
377                }
378
379                flatten_actor_buckets(buckets)
380            })
381            .collect();
382        let control_peers = auras
383            .iter()
384            .map(|aura| {
385                let bucket_count = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy_count;
386                let mut buckets = vec![Vec::new(); bucket_count];
387
388                if let Some(control) = aura.crowd_control {
389                    for binding in bindings.aura_bindings() {
390                        // BOUNDS: concrete bindings contain local identities from this finalized aura slice.
391                        let candidate = &auras[binding.local.as_usize()];
392                        let Some(candidate_control) = candidate.crowd_control else {
393                            continue;
394                        };
395                        let matches_kind = candidate_control.kind == control.kind;
396                        let matches_group = control.diminishing
397                            != wowlab_engine_domain::dbc::DiminishingGroup::None
398                            && candidate_control.diminishing == control.diminishing;
399
400                        if !(matches_kind || matches_group) {
401                            continue;
402                        }
403
404                        if let Some(bucket) =
405                            concrete_actor_bucket(binding.key.affected(), enemy_count)
406                        {
407                            // BOUNDS: concrete_actor_bucket validates the returned bucket.
408                            buckets[bucket].push(ControlPeerDependency {
409                                handle: binding.handle,
410                                matches_kind,
411                                matches_group,
412                                permanent: candidate.base_duration_ms == 0,
413                            });
414                        }
415                    }
416                }
417
418                flatten_actor_buckets(buckets)
419            })
420            .collect();
421
422        Self {
423            entries: entries.into_boxed_slice(),
424            concrete_entries,
425            cost_spells: cost_spells.into_boxed_slice(),
426            recharge_spells: graph.recharge_spells,
427            maximum_charge_spells: graph.maximum_charge_spells,
428            gate_spells: gate_spells.into_boxed_slice(),
429            aura_timing: graph.aura_timing,
430            aura_timing_bindings,
431            control_peers,
432            projections: projections.into_boxed_slice(),
433            projection_sources: projection_sources.into_boxed_slice(),
434            projection_target_count,
435            exclusive_peers,
436            exclusive_spells: exclusive_spells.into_boxed_slice(),
437        }
438    }
439
440    pub(crate) fn consumers(&self, aura: LocalAuraIdx) -> AuraDependencies {
441        self.entry(aura).consumers
442    }
443
444    pub(crate) fn cost_spells(&self, aura: LocalAuraIdx) -> &[SpellConsumerBinding] {
445        // BOUNDS: each range was appended to this exact flattened array during finalization.
446        &self.cost_spells[self.entry(aura).costs.clone()]
447    }
448
449    pub(crate) fn recharge_spells(&self, aura: LocalAuraIdx) -> &[LocalSpellIdx] {
450        // BOUNDS: each range was appended to this exact flattened array during finalization.
451        &self.recharge_spells[self.entry(aura).recharge.clone()]
452    }
453
454    pub(crate) fn maximum_charge_spells(&self, aura: LocalAuraIdx) -> &[LocalSpellIdx] {
455        // BOUNDS: each range was appended to this exact flattened array during finalization.
456        &self.maximum_charge_spells[self.entry(aura).maximum_charges.clone()]
457    }
458
459    pub(crate) fn gate_spells(&self, aura: LocalAuraIdx) -> &[SpellConsumerBinding] {
460        // BOUNDS: each range was appended to this exact flattened array during finalization.
461        &self.gate_spells[self.entry(aura).gates.clone()]
462    }
463
464    pub(crate) fn aura_timing(&self, aura: LocalAuraIdx) -> &[LocalAuraIdx] {
465        // BOUNDS: each range was appended to this exact flattened array during finalization.
466        &self.aura_timing[self.entry(aura).aura_timing.clone()]
467    }
468
469    pub(crate) fn aura_timing_bindings(
470        &self,
471        aura: LocalAuraIdx,
472        actor: ActorId,
473    ) -> &[ConcreteAuraBinding] {
474        // BOUNDS: local aura identities and concrete timing ranges share the finalized aura count.
475        self.aura_timing_bindings[aura.as_usize()].for_actor(actor)
476    }
477
478    pub(crate) fn control_peers(
479        &self,
480        aura: LocalAuraIdx,
481        actor: ActorId,
482    ) -> &[ControlPeerDependency] {
483        // BOUNDS: local aura identities and control-peer ranges share the finalized aura count.
484        self.control_peers[aura.as_usize()].for_actor(actor)
485    }
486
487    pub(crate) fn projections(&self, aura: LocalAuraIdx) -> &[ProjectionDependency] {
488        // BOUNDS: each range was appended to this exact flattened array during finalization.
489        &self.projections[self.entry(aura).projections.clone()]
490    }
491
492    pub(crate) fn all_projections(&self) -> &[ProjectionDependency] {
493        &self.projections
494    }
495
496    pub(crate) fn projection(&self, index: AuraProjectionDependencyIdx) -> ProjectionDependency {
497        // BOUNDS: projection identities are created from this exact finalized slice.
498        self.projections[index.as_usize()]
499    }
500
501    pub(crate) const fn projection_count(&self) -> usize {
502        self.projections.len()
503    }
504
505    #[expect(
506        clippy::unused_self,
507        reason = "the plan retains authority over extracting its private dependency identity"
508    )]
509    pub(crate) fn projection_index(
510        &self,
511        dependency: ProjectionDependency,
512    ) -> AuraProjectionDependencyIdx {
513        dependency.index
514    }
515
516    pub(crate) fn exclusive_peers(&self, binding: ConcreteAuraBinding) -> &[ConcreteAuraBinding] {
517        // BOUNDS: concrete binding ordinals and entries are finalized in the same stable order.
518        let entry = &self.concrete_entries[binding.index().as_usize()];
519
520        // BOUNDS: each range was appended to this exact flattened array during finalization.
521
522        &self.exclusive_peers[entry.exclusive_peers.clone()]
523    }
524
525    pub(crate) fn exclusive_spells(&self, aura: LocalAuraIdx) -> &[ExclusiveSpellDependency] {
526        // BOUNDS: each range was appended to this exact flattened array during finalization.
527        &self.exclusive_spells[self.entry(aura).exclusive_spells.clone()]
528    }
529
530    pub(crate) fn projection_source(
531        &self,
532        dependency: ProjectionDependency,
533        target: Option<EnemyIdx>,
534    ) -> Option<AuraSlotHandle> {
535        let target_index = target.map_or(0, |enemy| enemy.as_usize().saturating_add(1));
536
537        if target_index >= self.projection_target_count {
538            return None;
539        }
540
541        self.projection_sources
542            .get(dependency.source_offset + target_index)
543            .copied()
544            .flatten()
545    }
546
547    pub(crate) fn validate(&self, aura_count: usize, spell_count: usize) -> bool {
548        self.entries.len() == aura_count
549            && self
550                .concrete_entries
551                .iter()
552                .all(|entry| entry.exclusive_peers.end <= self.exclusive_peers.len())
553            && self
554                .cost_spells
555                .iter()
556                .all(|binding| binding.local.as_usize() < spell_count)
557            && self
558                .recharge_spells
559                .iter()
560                .all(|local| local.as_usize() < spell_count)
561            && self
562                .maximum_charge_spells
563                .iter()
564                .all(|local| local.as_usize() < spell_count)
565            && self
566                .gate_spells
567                .iter()
568                .all(|binding| binding.local.as_usize() < spell_count)
569            && self
570                .aura_timing
571                .iter()
572                .all(|local| local.as_usize() < aura_count)
573            && self.aura_timing_bindings.len() == aura_count
574            && self.control_peers.len() == aura_count
575    }
576
577    fn entry(&self, aura: LocalAuraIdx) -> &AuraDependencyEntry {
578        // BOUNDS: local aura identities and dependency entries come from the same finalized slice.
579        &self.entries[aura.as_usize()]
580    }
581}
582
583fn concrete_actor_bucket(actor: ActorId, enemy_count: usize) -> Option<usize> {
584    match actor {
585        ActorId::Player => Some(0),
586        ActorId::External => Some(1),
587        ActorId::Pet(pet) => {
588            (pet.as_usize() < PetIdx::MAX).then_some(FIXED_ACTOR_BUCKET_COUNT + pet.as_usize())
589        }
590        ActorId::Enemy(enemy) => (enemy.as_usize() < enemy_count)
591            .then_some(FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX + enemy.as_usize()),
592    }
593}
594
595fn flatten_actor_buckets<T>(buckets: Vec<Vec<T>>) -> ActorRanges<T> {
596    let mut entries = Vec::new();
597    let mut ranges = Vec::with_capacity(buckets.len());
598
599    for bucket in buckets {
600        let start = entries.len();
601
602        entries.extend(bucket);
603        ranges.push(start..entries.len());
604    }
605
606    // BOUNDS: the two fixed actor buckets always prefix the pet/enemy buckets.
607
608    let player = ranges[0].clone();
609    // BOUNDS: the two fixed actor buckets always prefix the pet/enemy buckets.
610    let external = ranges[1].clone();
611    let enemies_start = FIXED_ACTOR_BUCKET_COUNT + PetIdx::MAX;
612    // BOUNDS: the bucket vector contains every fixed pet bucket.
613    let pets = ranges[FIXED_ACTOR_BUCKET_COUNT..enemies_start]
614        .to_vec()
615        .into_boxed_slice();
616    // BOUNDS: all remaining buckets belong to finalized enemies.
617    let enemies = ranges[enemies_start..].to_vec().into_boxed_slice();
618
619    ActorRanges {
620        entries: entries.into_boxed_slice(),
621        player,
622        external,
623        pets,
624        enemies,
625    }
626}
627
628fn append_range<T>(target: &mut Vec<T>, values: Vec<T>) -> Range<usize> {
629    let start = target.len();
630
631    target.extend(values);
632
633    start..target.len()
634}
635
636fn bind_spell_consumers(
637    locals: impl IntoIterator<Item = LocalSpellIdx>,
638    bindings: &BufferBindings,
639) -> Vec<SpellConsumerBinding> {
640    locals
641        .into_iter()
642        .map(|local| SpellConsumerBinding {
643            local,
644            handle: bindings.spell(local),
645        })
646        .collect()
647}
648
649fn normalize<T>(values: &mut Vec<T>)
650where
651    T: Ord,
652{
653    values.sort_unstable();
654    values.dedup();
655}
656
657fn compile_exclusive_peers(
658    auras: &[AuraData],
659    bindings: &BufferBindings,
660) -> (
661    Box<[ConcreteAuraDependencyEntry]>,
662    Box<[ConcreteAuraBinding]>,
663) {
664    let mut entries = Vec::with_capacity(bindings.aura_bindings().count());
665    let mut peers = Vec::new();
666
667    for binding in bindings.aura_bindings() {
668        let start = peers.len();
669        // BOUNDS: concrete bindings contain local identities from this finalized aura slice.
670        let group = auras[binding.local.as_usize()]
671            .spell_group
672            .filter(|group| group.rule == SpellGroupRule::Exclusive)
673            .map(|group| group.group_idx);
674
675        if let Some(group) = group {
676            peers.extend(bindings.aura_bindings().filter(|candidate| {
677                candidate.local != binding.local
678                    && candidate.key.source() == binding.key.source()
679                    && candidate.key.affected() == binding.key.affected()
680                    // BOUNDS: concrete bindings contain local identities from this finalized aura slice.
681                    && auras[candidate.local.as_usize()]
682                        .spell_group
683                        .is_some_and(|peer| {
684                            peer.rule == SpellGroupRule::Exclusive && peer.group_idx == group
685                        })
686            }));
687        }
688
689        // BOUNDS: start was captured from this vector before only extending it.
690
691        peers[start..].sort_unstable_by_key(|peer| peer.local);
692        entries.push(ConcreteAuraDependencyEntry {
693            exclusive_peers: start..peers.len(),
694        });
695    }
696
697    (entries.into_boxed_slice(), peers.into_boxed_slice())
698}
699
700fn collect_exclusive_spell_dependencies(
701    applied: LocalAuraIdx,
702    aura: &AuraData,
703    auras: &[AuraData],
704    spells: &[SpellData],
705    spell_effects: &[SpellEffectData],
706) -> Vec<UnboundExclusiveSpellDependency> {
707    let Some(group) = aura
708        .spell_group
709        .filter(|group| group.rule == SpellGroupRule::Exclusive)
710        .map(|group| group.group_idx)
711    else {
712        return Vec::new();
713    };
714    let mut dependencies = Vec::new();
715
716    for (index, spell) in spells.iter().enumerate() {
717        let start = spell.followup_effects.start as usize;
718        let len = usize::from(spell.followup_effects.len);
719        let followups = (start..start.saturating_add(len)).filter_map(|effect_index| {
720            match spell_effects.get(effect_index) {
721                Some(SpellEffectData::ApplyAura { aura_local }) => Some(*aura_local),
722                _ => None,
723            }
724        });
725        let mut belongs_to_group = false;
726        let mut enables_applied = false;
727
728        for local in spell.applies_aura.into_iter().chain(followups) {
729            // BOUNDS: applied-aura locals are finalized against this exact aura definition slice.
730            let same_group = auras[local.as_usize()]
731                .spell_group
732                .is_some_and(|candidate| {
733                    candidate.rule == SpellGroupRule::Exclusive && candidate.group_idx == group
734                });
735
736            belongs_to_group |= same_group;
737            enables_applied |= same_group && local == applied;
738        }
739
740        if belongs_to_group {
741            dependencies.push(UnboundExclusiveSpellDependency {
742                local: local_spell(index),
743                enabled: enables_applied,
744            });
745        }
746    }
747
748    dependencies
749}
750
751fn local_spell(index: usize) -> LocalSpellIdx {
752    LocalSpellIdx::new(u8::try_from(index).expect("finalized spell count is validated below 256"))
753}
754
755// #t(fn: rust_cyclomatic_complexity) this exhaustive family classification is the auditable source for selective derived-state dispatch.
756fn classify_consumers(
757    effect: &BuffEffect,
758    config: &ProgramConfig,
759    consumers: &mut AuraDependencies,
760) {
761    use AuraDependencies as Dependency;
762
763    match effect {
764        BuffEffect::SpellGateFromEffect { .. } => *consumers |= Dependency::GATES,
765        BuffEffect::PrimaryStat(..) | BuffEffect::PrimaryStatPercent(..) => {
766            *consumers |= Dependency::PRIMARY_STAT | Dependency::PET_STATS;
767        }
768        BuffEffect::BonusStatPercent { stat, .. } => {
769            let primary_matches = match config.base_stats.primary_attribute {
770                Some(Attribute::Strength) => *stat == StatModifierKind::Strength,
771                Some(Attribute::Agility) => *stat == StatModifierKind::Agility,
772                Some(Attribute::Intellect) => *stat == StatModifierKind::Intellect,
773                _ => false,
774            };
775
776            if primary_matches || matches!(stat, StatModifierKind::Primary | StatModifierKind::All)
777            {
778                *consumers |= Dependency::PRIMARY_STAT | Dependency::PET_STATS;
779            }
780
781            if matches!(stat, StatModifierKind::Stamina | StatModifierKind::All) {
782                *consumers |= Dependency::STAMINA | Dependency::PET_STATS;
783            }
784        }
785        BuffEffect::OverrideSpellPowerByAttackPowerPercent(..)
786        | BuffEffect::OverrideAttackPowerBySpellPowerPercent(..)
787        | BuffEffect::AttackPowerPercent(..) => *consumers |= Dependency::PET_STATS,
788        BuffEffect::RechargeRatePercent(..)
789        | BuffEffect::CooldownRechargeRateFromEffect { .. }
790        | BuffEffect::ChargeRechargeRateCategory { .. }
791        | BuffEffect::CooldownDurationPercent { .. } => {
792            *consumers |= Dependency::RECHARGE_RATE;
793        }
794        BuffEffect::MaxChargesCategory(..) => *consumers |= Dependency::MAXIMUM_CHARGES,
795        BuffEffect::AuraTimeRateFromEffect { .. } => *consumers |= Dependency::AURA_TIME,
796        BuffEffect::Haste(..)
797        | BuffEffect::HasteMult(..)
798        | BuffEffect::HasteMultFromEffect { .. } => {
799            *consumers |= Dependency::PLAYER_SPEED | Dependency::COMPANION_SPEED;
800
801            if config.haste_regen {
802                *consumers |= Dependency::RESOURCE_REGEN;
803            }
804        }
805        BuffEffect::SpellCastSpeed(..) | BuffEffect::AttackSpeed(..) => {
806            *consumers |= Dependency::COMPANION_SPEED;
807
808            if matches!(effect, BuffEffect::AttackSpeed(..)) {
809                *consumers |= Dependency::PLAYER_SPEED;
810            }
811        }
812        BuffEffect::MaxResourceFlat(..) => *consumers |= Dependency::MAXIMUM_RESOURCE,
813        BuffEffect::SpellTargetingFromEffect { property, .. }
814            if *property == wowlab_engine_domain::dbc::ModifierPropertyKind::Range =>
815        {
816            *consumers |= Dependency::TARGET_RANGE;
817        }
818        BuffEffect::RegenPercent(..)
819        | BuffEffect::DynamicRegenPercent(..)
820        | BuffEffect::ResourceRegenPerFiveSeconds(..)
821        | BuffEffect::InterruptRegen
822        | BuffEffect::InterruptedRegenPercent(..)
823        | BuffEffect::PreventResourceRegen(..) => {
824            *consumers |= Dependency::RESOURCE_REGEN | Dependency::PET_ACTIONS;
825        }
826        _ => {}
827    }
828}
829
830// #t(fn: rust_cyclomatic_complexity) exact cost dependency union intentionally keeps every resource, entry, label, class-mask, and additional-power selector visible.
831fn collect_cost_spells(
832    effect: &BuffEffect,
833    spells: &[SpellData],
834    config: &ProgramConfig,
835    target: &mut Vec<LocalSpellIdx>,
836) {
837    match effect {
838        BuffEffect::ResourceCostPercent(_, resource) => {
839            for (index, spell) in spells.iter().enumerate() {
840                if (config.base_stats.resource_type == Some(*resource)
841                    && spell_has_primary_cost(spell, config))
842                    || (config.base_stats.secondary_resource_type == Some(*resource)
843                        && spell_has_secondary_cost(spell, config))
844                {
845                    target.push(local_spell(index));
846                }
847            }
848        }
849        BuffEffect::SpellCostPercent(_, spell_ids, _)
850        | BuffEffect::SpellCostFlat(_, spell_ids, _) => {
851            for (index, spell) in spells.iter().enumerate() {
852                if spell_ids.contains(&spell.spell_id) {
853                    target.push(local_spell(index));
854                }
855            }
856        }
857        BuffEffect::SpellCostFromEffect {
858            source_spell_id,
859            effect_index,
860            cost_entry,
861            ..
862        } => {
863            for (index, spell) in spells.iter().enumerate() {
864                let spell_idx = spell.idx();
865                let matching_entry = config.game_data.cost_entry(spell_idx, false) == *cost_entry
866                    || config.game_data.cost_entry(spell_idx, true) == *cost_entry;
867
868                if matching_entry
869                    && config.game_data.effect_affects_spell(
870                        SpellIdx::from_raw(*source_spell_id),
871                        *effect_index,
872                        spell_idx,
873                    )
874                {
875                    target.push(local_spell(index));
876                }
877            }
878        }
879        BuffEffect::AdditionalPowerCostFromEffect {
880            source_spell_id,
881            effect_index,
882            resource_type,
883            ..
884        } => {
885            let matching_resource = config
886                .base_stats
887                .resource_type
888                .is_some_and(|resource| i32::from(u8::from(resource)) == *resource_type)
889                || config
890                    .base_stats
891                    .secondary_resource_type
892                    .is_some_and(|resource| i32::from(u8::from(resource)) == *resource_type);
893
894            if matching_resource {
895                for (index, spell) in spells.iter().enumerate() {
896                    if config.game_data.effect_affects_spell(
897                        SpellIdx::from_raw(*source_spell_id),
898                        *effect_index,
899                        spell.idx(),
900                    ) {
901                        target.push(local_spell(index));
902                    }
903                }
904            }
905        }
906        _ => {}
907    }
908}
909
910fn collect_recharge_spells(
911    effect: &BuffEffect,
912    spells: &[SpellData],
913    config: &ProgramConfig,
914    target: &mut Vec<LocalSpellIdx>,
915) {
916    match effect {
917        BuffEffect::Haste(..)
918        | BuffEffect::HasteMult(..)
919        | BuffEffect::HasteMultFromEffect { .. } => {
920            target.extend(
921                spells
922                    .iter()
923                    .enumerate()
924                    .filter(|&(_index, spell)| {
925                        spell.cooldown.has_cooldown && spell.cooldown.cooldown_hasted
926                    })
927                    .map(|(index, _spell)| local_spell(index)),
928            );
929        }
930        BuffEffect::RechargeRatePercent(_, spell_ids) => {
931            target.extend(
932                spells
933                    .iter()
934                    .enumerate()
935                    .filter(|&(_index, spell)| {
936                        spell.cooldown.has_cooldown && spell_ids.contains(&spell.spell_id)
937                    })
938                    .map(|(index, _spell)| local_spell(index)),
939            );
940        }
941        BuffEffect::CooldownRechargeRateFromEffect {
942            source_spell_id,
943            effect_index,
944            ..
945        }
946        | BuffEffect::CooldownDurationPercent {
947            source_spell_id,
948            effect_index,
949            ..
950        } => {
951            target.extend(
952                spells
953                    .iter()
954                    .enumerate()
955                    .filter(|&(_index, spell)| {
956                        spell.cooldown.has_cooldown
957                            && config.game_data.effect_affects_spell(
958                                SpellIdx::from_raw(*source_spell_id),
959                                *effect_index,
960                                spell.idx(),
961                            )
962                    })
963                    .map(|(index, _spell)| local_spell(index)),
964            );
965        }
966        BuffEffect::ChargeRechargeRateCategory { category, .. } => {
967            target.extend(
968                spells
969                    .iter()
970                    .enumerate()
971                    .filter(|&(_index, spell)| {
972                        spell.cooldown.has_cooldown
973                            && spell
974                                .cooldown
975                                .charge_category
976                                .is_some_and(|value| value.raw() == *category)
977                    })
978                    .map(|(index, _spell)| local_spell(index)),
979            );
980        }
981        _ => {}
982    }
983}
984
985fn collect_maximum_charge_spells(
986    effect: &BuffEffect,
987    spells: &[SpellData],
988    target: &mut Vec<LocalSpellIdx>,
989) {
990    let BuffEffect::MaxChargesCategory(_, category) = effect else {
991        return;
992    };
993
994    target.extend(
995        spells
996            .iter()
997            .enumerate()
998            .filter(|&(_index, spell)| {
999                spell.is_charged()
1000                    && spell
1001                        .cooldown
1002                        .charge_category
1003                        .is_some_and(|value| value.raw() == *category)
1004            })
1005            .map(|(index, _spell)| local_spell(index)),
1006    );
1007}
1008
1009fn collect_gate_spells(effect: &BuffEffect, spells: &[SpellData], target: &mut Vec<LocalSpellIdx>) {
1010    if matches!(effect, BuffEffect::SpellGateFromEffect { .. }) {
1011        target.extend((0..spells.len()).map(local_spell));
1012    }
1013}
1014
1015fn collect_aura_timing(
1016    effect: &BuffEffect,
1017    auras: &[AuraData],
1018    config: &ProgramConfig,
1019    target: &mut Vec<LocalAuraIdx>,
1020) {
1021    let BuffEffect::AuraTimeRateFromEffect {
1022        source_spell_id,
1023        effect_index,
1024        ..
1025    } = effect
1026    else {
1027        return;
1028    };
1029
1030    target.extend(
1031        auras
1032            .iter()
1033            .enumerate()
1034            .filter(|&(_, aura)| {
1035                config.game_data.effect_affects_spell(
1036                    SpellIdx::from_raw(*source_spell_id),
1037                    *effect_index,
1038                    SpellIdx::from_raw(aura.aura_id),
1039                )
1040            })
1041            .map(|(index, _)| local_aura(index)),
1042    );
1043}
1044
1045fn local_aura(index: usize) -> LocalAuraIdx {
1046    LocalAuraIdx::new(u8::try_from(index).expect("finalized aura count is validated below 256"))
1047}
1048
1049fn spell_has_primary_cost(spell: &SpellData, config: &ProgramConfig) -> bool {
1050    // #t(fn: rust_floating_point_eq) finalized cost fields use exact zero as the absent sentinel.
1051    spell.cost.resource_cost != 0.0
1052        || spell.cost.optional_resource_cost != 0.0
1053        || spell.cost.resource_cost_pct.percent != 0.0
1054        || spell.cost.optional_resource_cost_pct.percent != 0.0
1055        || spell.cost.maximum_resource_cost_pct.percent != 0.0
1056        || config.game_data.cost_entry(spell.idx(), false) != 0
1057}
1058
1059fn spell_has_secondary_cost(spell: &SpellData, config: &ProgramConfig) -> bool {
1060    // #t(fn: rust_floating_point_eq) finalized cost fields use exact zero as the absent sentinel.
1061    spell.cost.secondary_resource_cost != 0.0
1062        || spell.cost.secondary_optional_resource_cost != 0.0
1063        || config.game_data.cost_entry(spell.idx(), true) != 0
1064}
1065
1066fn spell_uses_maximum_resource_cost(spell: &SpellData) -> bool {
1067    // #t(fn: rust_floating_point_eq) finalized percentage cost fields use exact zero as the absent sentinel.
1068    [
1069        spell.cost.resource_cost_pct,
1070        spell.cost.optional_resource_cost_pct,
1071        spell.cost.maximum_resource_cost_pct,
1072    ]
1073    .into_iter()
1074    .any(|cost| cost.percent != 0.0 && cost.basis == ResourceCostPercentBasis::Maximum)
1075}
1076
1077fn exact_projection_key(
1078    aura: AuraIdx,
1079    on: AuraOn,
1080    current_target: Option<EnemyIdx>,
1081) -> Option<AuraKey> {
1082    let (source, affected) = match on {
1083        AuraOn::Player => (ActorId::Player, ActorId::Player),
1084        AuraOn::Target => (ActorId::Player, ActorId::Enemy(current_target?)),
1085        AuraOn::Pet => {
1086            let pet = ActorId::Pet(PetIdx::PRIMARY);
1087
1088            (pet, pet)
1089        }
1090    };
1091
1092    Some(AuraKey::new(aura, source, affected, on))
1093}