Skip to main content

wowlab_engine_combat/state/runtime/
projection.rs

1//! Typed invalidation for encounter fields observed by rotations and scheduled work.
2
3use wowlab_types::sim::SimTime;
4
5use super::CombatInstance;
6use crate::state::program::AuraProjectionDependencyIdx;
7
8bitflags::bitflags! {
9    /// Focused projection families dirtied by combat-state mutations.
10    #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
11    pub(crate) struct ProjectionDirty: u8 {
12        const TARGET_HEALTH = 1 << 0;
13        const TARGET_IDENTITY_RANGE_TRAVEL = 1 << 1;
14        const POPULATION = 1 << 2;
15    }
16}
17
18/// Exact, coalescing aura-projection dirtiness with finalization-bounded storage.
19#[derive(Debug)]
20pub(super) struct AuraProjectionDirty {
21    membership: Box<[u64]>,
22    pending: Box<[AuraProjectionDependencyIdx]>,
23    len: usize,
24}
25
26impl AuraProjectionDirty {
27    pub(super) fn new(capacity: usize) -> Self {
28        let word_count = capacity.div_ceil(u64::BITS as usize);
29
30        Self {
31            membership: vec![0; word_count].into_boxed_slice(),
32            pending: vec![AuraProjectionDependencyIdx::default(); capacity].into_boxed_slice(),
33            len: 0,
34        }
35    }
36
37    fn mark(&mut self, index: AuraProjectionDependencyIdx) {
38        let raw = index.as_usize();
39
40        debug_assert!(
41            raw < self.pending.len(),
42            "aura projection dependency {raw} exceeds finalized dirty capacity {}",
43            self.pending.len()
44        );
45
46        if raw >= self.pending.len() {
47            return;
48        }
49
50        let word_index = raw / u64::BITS as usize;
51        let bit = 1_u64 << (raw % u64::BITS as usize);
52        // BOUNDS: raw was checked against pending capacity, and membership is its ceiling bitset.
53        let word = &mut self.membership[word_index];
54
55        if *word & bit != 0 {
56            return;
57        }
58
59        *word |= bit;
60        // BOUNDS: membership and pending capacity are derived from the same projection count.
61        self.pending[self.len] = index;
62        self.len += 1;
63    }
64
65    fn pop(&mut self) -> Option<AuraProjectionDependencyIdx> {
66        self.len = self.len.checked_sub(1)?;
67        // BOUNDS: len only counts entries stored in this finalized pending slice.
68        let index = self.pending[self.len];
69        let raw = index.as_usize();
70        // BOUNDS: raw is a finalized dependency index stored only after the capacity check.
71        let word = &mut self.membership[raw / u64::BITS as usize];
72
73        *word &= !(1_u64 << (raw % u64::BITS as usize));
74
75        Some(index)
76    }
77
78    fn clear(&mut self) {
79        while self.pop().is_some() {}
80    }
81
82    const fn is_empty(&self) -> bool {
83        self.len == 0
84    }
85}
86
87impl CombatInstance {
88    pub(crate) fn mark_projection_dirty(&mut self, dirty: ProjectionDirty, at: SimTime) {
89        self.projection_dirty |= dirty;
90        self.projection_dirty_at = self.projection_dirty_at.max(at);
91    }
92
93    pub(crate) fn take_projection_dirty(&mut self) -> Option<(ProjectionDirty, SimTime)> {
94        if self.projection_dirty.is_empty() {
95            return None;
96        }
97
98        let dirty = std::mem::take(&mut self.projection_dirty);
99        let at = self.projection_dirty_at;
100
101        self.projection_dirty_at = SimTime::ZERO;
102
103        Some((dirty, at))
104    }
105
106    pub(crate) fn mark_aura_projection_dirty(&mut self, index: AuraProjectionDependencyIdx) {
107        self.aura_projection_dirty.mark(index);
108    }
109
110    pub(crate) fn pop_aura_projection_dirty(&mut self) -> Option<AuraProjectionDependencyIdx> {
111        self.aura_projection_dirty.pop()
112    }
113
114    pub(crate) const fn aura_projections_are_dirty(&self) -> bool {
115        !self.aura_projection_dirty.is_empty()
116    }
117
118    pub(crate) fn clear_aura_projection_dirty(&mut self) {
119        self.aura_projection_dirty.clear();
120    }
121
122    pub(crate) fn clear_projection_dirty(&mut self) {
123        self.projection_dirty = ProjectionDirty::empty();
124        self.projection_dirty_at = SimTime::ZERO;
125        self.aura_projection_dirty.clear();
126    }
127
128    #[cfg(test)]
129    pub(crate) fn record_projection_flush(&mut self) {
130        self.projection_flush_count += 1;
131    }
132
133    #[cfg(test)]
134    pub(crate) const fn projection_flush_count(&self) -> u32 {
135        self.projection_flush_count
136    }
137
138    #[cfg(test)]
139    pub(crate) fn reset_projection_flush_count(&mut self) {
140        self.projection_flush_count = 0;
141    }
142}