Skip to main content

wowlab_engine_combat/state/runtime/
dense_pools.rs

1// #t(file: rust_unchecked_indexing) Finalized local and concrete-aura ordinals index pools resized from the same immutable program.
2// #t(file: rust_vec_string_field) Expiry vectors are mutable retained-capacity runtime queues, not immutable strings or slices.
3// #t(file: rust_magic_numbers) Spell-gate bit masks encode the finite public SpellGate variants.
4// #t(file: rust_lossy_cast) The finalized local-spell universe is bounded to the u8 index domain before runtime pool construction.
5
6use wowlab_engine_domain::pool::{SpellGate, SpellGateUpdate};
7use wowlab_types::sim::SimTime;
8
9use crate::state::LocalSpellIdx;
10
11fn local_spell_from_state_index(index: usize) -> LocalSpellIdx {
12    LocalSpellIdx::new(u8::try_from(index).expect("spell-state index fits in the local identity"))
13}
14
15#[derive(Debug, Default)]
16#[cfg(feature = "rotation-optimizations")]
17struct DirtyPoolKeys {
18    words: Box<[u64]>,
19    summary: Box<[u64]>,
20}
21
22#[cfg(feature = "rotation-optimizations")]
23impl DirtyPoolKeys {
24    fn resize(&mut self, len: usize) {
25        let word_count = len.div_ceil(u64::BITS as usize);
26
27        self.words = vec![0; word_count].into_boxed_slice();
28        self.summary = vec![0; word_count.div_ceil(u64::BITS as usize)].into_boxed_slice();
29    }
30
31    fn mark(&mut self, key: usize) {
32        let word_index = key / u64::BITS as usize;
33        let word = self
34            .words
35            .get_mut(word_index)
36            .expect("dense pool key belongs to its finalized dirty bitset");
37
38        *word |= 1_u64 << key % u64::BITS as usize;
39
40        let summary_word = self
41            .summary
42            .get_mut(word_index / u64::BITS as usize)
43            .expect("dense pool dirty word belongs to its summary bitset");
44
45        *summary_word |= 1_u64 << word_index % u64::BITS as usize;
46    }
47
48    fn pop_first(&mut self) -> Option<usize> {
49        let (summary_index, summary) = self
50            .summary
51            .iter()
52            .copied()
53            .enumerate()
54            .find(|(_, word)| *word != 0)?;
55        let word_index = summary_index * u64::BITS as usize + summary.trailing_zeros() as usize;
56        let word = self
57            .words
58            .get(word_index)
59            .copied()
60            .filter(|word| *word != 0)
61            .expect("dense pool dirty summary points to a nonempty word");
62        let key = word_index * u64::BITS as usize + word.trailing_zeros() as usize;
63        let bit = 1_u64 << key % u64::BITS as usize;
64        let word = self
65            .words
66            .get_mut(word_index)
67            .expect("dense pool dirty key belongs to its finalized bitset");
68
69        *word &= !bit;
70
71        if *word == 0 {
72            let summary_word = self
73                .summary
74                .get_mut(summary_index)
75                .expect("dense pool dirty word belongs to its summary bitset");
76
77            *summary_word &= !(1_u64 << word_index % u64::BITS as usize);
78        }
79
80        Some(key)
81    }
82}
83
84#[derive(Debug, Default)]
85pub(crate) struct DenseDamagePool {
86    values: Vec<f64>,
87    #[cfg(feature = "rotation-optimizations")]
88    dirty: DirtyPoolKeys,
89}
90
91// #t(rust_unchecked_indexing) finalized concrete-aura ordinals index exactly resized pools.
92impl DenseDamagePool {
93    pub(crate) fn resize(&mut self, len: usize) {
94        self.values.resize(len, 0.0);
95        #[cfg(feature = "rotation-optimizations")]
96        self.dirty.resize(len);
97    }
98    pub(crate) fn add(&mut self, key: usize, amount: f64) -> f64 {
99        #[cfg(feature = "rotation-optimizations")]
100        self.dirty.mark(key);
101        self.values[key] += amount;
102
103        self.values[key]
104    }
105    pub(crate) fn remaining(&self, key: usize) -> f64 {
106        self.values.get(key).copied().unwrap_or(0.0)
107    }
108    pub(crate) fn take(&mut self, key: usize) -> f64 {
109        std::mem::take(&mut self.values[key])
110    }
111    pub(crate) fn take_tick(&mut self, key: usize, ticks: f64) -> f64 {
112        let amount = self.values[key] / ticks.max(1.0);
113
114        self.values[key] = (self.values[key] - amount).max(0.0);
115
116        amount
117    }
118    pub(crate) fn remove(&mut self, key: usize) -> f64 {
119        self.take(key)
120    }
121    pub(crate) fn clear(&mut self) {
122        #[cfg(feature = "rotation-optimizations")]
123        while let Some(key) = self.dirty.pop_first() {
124            self.values[key] = 0.0;
125        }
126
127        #[cfg(not(feature = "rotation-optimizations"))]
128        self.values.fill(0.0);
129    }
130}
131
132#[derive(Debug, Default)]
133// #t(rust_similar_structs) Dense damage and rolling multipliers intentionally remain domain-distinct pools with different reset values and mutation APIs.
134pub(crate) struct DenseRollingTickPool {
135    values: Vec<f64>,
136    #[cfg(feature = "rotation-optimizations")]
137    dirty: DirtyPoolKeys,
138}
139
140// #t(rust_unchecked_indexing) finalized concrete-aura ordinals index exactly resized pools.
141impl DenseRollingTickPool {
142    pub(crate) fn resize(&mut self, len: usize) {
143        self.values.resize(len, 1.0);
144        #[cfg(feature = "rotation-optimizations")]
145        self.dirty.resize(len);
146    }
147    pub(crate) fn mult(&self, key: usize) -> f64 {
148        self.values.get(key).copied().unwrap_or(1.0)
149    }
150    pub(crate) fn combine(&mut self, key: usize, old: f64, base: f64, new: f64, mult: f64) {
151        if new > 0.0 {
152            #[cfg(feature = "rotation-optimizations")]
153            self.dirty.mark(key);
154            self.values[key] = (old.max(0.0) * self.values[key] + base * mult) / new;
155        }
156    }
157    pub(crate) fn remove(&mut self, key: usize) {
158        self.values[key] = 1.0;
159    }
160    pub(crate) fn clear(&mut self) {
161        #[cfg(feature = "rotation-optimizations")]
162        while let Some(key) = self.dirty.pop_first() {
163            self.values[key] = 1.0;
164        }
165
166        #[cfg(not(feature = "rotation-optimizations"))]
167        self.values.fill(1.0);
168    }
169}
170
171// #t(rust_vec_string_field) inner vectors retain warmed deadline capacity across reset by design.
172#[derive(Debug, Default)]
173pub(crate) struct DenseExpiryPool {
174    values: Vec<Vec<SimTime>>,
175    #[cfg(feature = "rotation-optimizations")]
176    dirty: DirtyPoolKeys,
177}
178
179// #t(rust_unchecked_indexing) finalized concrete-aura ordinals index exactly resized pools.
180impl DenseExpiryPool {
181    pub(crate) fn resize(&mut self, len: usize) {
182        self.values.resize_with(len, Vec::new);
183        #[cfg(feature = "rotation-optimizations")]
184        self.dirty.resize(len);
185    }
186    pub(crate) fn push_capped(
187        &mut self,
188        key: usize,
189        expiry: SimTime,
190        capacity: usize,
191    ) -> (usize, bool) {
192        #[cfg(feature = "rotation-optimizations")]
193        self.dirty.mark(key);
194        let deadlines = &mut self.values[key];
195        let fresh = deadlines.is_empty();
196
197        if deadlines.len() >= capacity.max(1) {
198            deadlines.remove(0);
199        }
200
201        let index = deadlines.partition_point(|deadline| *deadline <= expiry);
202
203        deadlines.insert(index, expiry);
204
205        (deadlines.len(), fresh)
206    }
207    pub(crate) fn expire_due(&mut self, key: usize, now: SimTime) -> Option<(usize, usize)> {
208        let deadlines = self.values.get_mut(key)?;
209        let due = deadlines.partition_point(|deadline| *deadline <= now);
210
211        deadlines.drain(..due);
212
213        Some((due, deadlines.len()))
214    }
215    pub(crate) fn remove_oldest(&mut self, key: usize) -> bool {
216        self.values.get_mut(key).is_some_and(|v| {
217            !v.is_empty() && {
218                v.remove(0);
219
220                true
221            }
222        })
223    }
224    pub(crate) fn is_empty(&self, key: usize) -> bool {
225        self.values.get(key).is_none_or(Vec::is_empty)
226    }
227    #[cfg(test)]
228    pub(crate) fn len(&self, key: usize) -> usize {
229        self.values.get(key).map_or(0, Vec::len)
230    }
231    pub(crate) fn remove(&mut self, key: usize) {
232        self.values[key].clear();
233    }
234    pub(crate) fn clear(&mut self) {
235        #[cfg(feature = "rotation-optimizations")]
236        while let Some(key) = self.dirty.pop_first() {
237            self.values[key].clear();
238        }
239
240        #[cfg(not(feature = "rotation-optimizations"))]
241        for deadlines in &mut self.values {
242            deadlines.clear();
243        }
244    }
245}
246
247#[derive(Debug, Default)]
248pub(crate) struct DenseAuraCooldownPool {
249    values: Vec<SimTime>,
250    #[cfg(feature = "rotation-optimizations")]
251    dirty: DirtyPoolKeys,
252}
253
254// #t(rust_unchecked_indexing) finalized concrete-aura ordinals index exactly resized pools.
255impl DenseAuraCooldownPool {
256    pub(crate) fn resize(&mut self, len: usize) {
257        self.values.resize(len, SimTime::ZERO);
258        #[cfg(feature = "rotation-optimizations")]
259        self.dirty.resize(len);
260    }
261    pub(crate) fn is_ready(&self, key: usize, now: SimTime) -> bool {
262        self.values[key] <= now
263    }
264    pub(crate) fn start_for(&mut self, key: usize, now: SimTime, duration: SimTime) -> SimTime {
265        #[cfg(feature = "rotation-optimizations")]
266        self.dirty.mark(key);
267        self.values[key] = now.saturating_add(duration);
268
269        self.values[key]
270    }
271    pub(crate) fn clear(&mut self) {
272        #[cfg(feature = "rotation-optimizations")]
273        while let Some(key) = self.dirty.pop_first() {
274            self.values[key] = SimTime::ZERO;
275        }
276
277        #[cfg(not(feature = "rotation-optimizations"))]
278        self.values.fill(SimTime::ZERO);
279    }
280}
281
282#[derive(Clone, Copy, Debug, Default)]
283struct GateState {
284    base: bool,
285    blockers: u8,
286}
287
288#[derive(Debug, Default)]
289pub(crate) struct DenseSpellGatePool {
290    state: Vec<Option<GateState>>,
291    exceptions: Vec<bool>,
292}
293
294// #t(rust_unchecked_indexing) finalized local-spell ordinals index the exact resized spell table.
295// #t(rust_magic_numbers) bits encode the finite gate reasons and one future fail-closed bit.
296// #t(rust_lossy_cast) finalization bounds spell count to the u8 local identity range.
297impl DenseSpellGatePool {
298    pub(crate) fn resize(&mut self, len: usize) {
299        self.state.resize(len, None);
300        self.exceptions.resize(len, false);
301    }
302    pub(crate) fn set(
303        &mut self,
304        key: LocalSpellIdx,
305        gate: SpellGate,
306        update: SpellGateUpdate,
307    ) -> bool {
308        let state = self.state[key.as_usize()].get_or_insert(GateState {
309            base: update.base_enabled,
310            blockers: 0,
311        });
312        let bit = match gate {
313            SpellGate::ReactionDelay => 1,
314            SpellGate::TargetHealth => 2,
315            SpellGate::ActiveAuraRestriction => 4,
316            _ => 8,
317        };
318
319        if update.available {
320            state.blockers &= !bit;
321        } else {
322            state.blockers |= bit;
323        }
324
325        state.base && state.blockers == 0
326    }
327    pub(crate) fn set_aura_state_exception(&mut self, key: LocalSpellIdx, active: bool) {
328        self.exceptions[key.as_usize()] = active;
329    }
330    pub(crate) fn ignores_aura_state(&self, key: LocalSpellIdx) -> bool {
331        self.exceptions[key.as_usize()]
332    }
333    pub(crate) fn drain_reset(&mut self) -> impl Iterator<Item = (LocalSpellIdx, bool)> + '_ {
334        self.exceptions.fill(false);
335
336        self.state.iter_mut().enumerate().filter_map(|(i, slot)| {
337            slot.take()
338                .map(|state| (local_spell_from_state_index(i), state.base))
339        })
340    }
341}
342
343#[cfg(all(test, feature = "rotation-optimizations"))]
344mod tests {
345    use googletest::prelude::*;
346
347    use super::*;
348
349    const FINALIZED_AURA_COUNT: usize = 65_536;
350
351    #[gtest]
352    fn sparse_aura_pool_reset_clears_only_warmed_dirty_keys_without_allocating() -> Result<()> {
353        let mut damage = DenseDamagePool::default();
354        let mut rolling = DenseRollingTickPool::default();
355        let mut expiries = DenseExpiryPool::default();
356        let mut cooldown = DenseAuraCooldownPool::default();
357
358        damage.resize(FINALIZED_AURA_COUNT);
359        rolling.resize(FINALIZED_AURA_COUNT);
360        expiries.resize(FINALIZED_AURA_COUNT);
361        cooldown.resize(FINALIZED_AURA_COUNT);
362
363        expiries.push_capped(42, SimTime::from_millis(1), 2);
364        expiries.clear();
365
366        let (_, allocations) = crate::test_alloc::allocations_during(|| {
367            damage.add(7, 12.0);
368            damage.add(42, 8.0);
369            rolling.combine(42, 1.0, 3.0, 4.0, 2.0);
370            expiries.push_capped(42, SimTime::from_millis(5), 2);
371            cooldown.start_for(42, SimTime::from_millis(10), SimTime::from_millis(20));
372
373            damage.clear();
374            rolling.clear();
375            expiries.clear();
376            cooldown.clear();
377        });
378
379        verify_that!((allocations.count, allocations.bytes), eq((0, 0)))?;
380        verify_true!(damage.dirty.summary.iter().all(|word| *word == 0))?;
381        verify_that!(damage.remaining(7), eq(0.0))?;
382        verify_that!(damage.remaining(42), eq(0.0))?;
383        verify_that!(rolling.mult(42), eq(1.0))?;
384        verify_true!(expiries.is_empty(42))?;
385
386        verify_true!(cooldown.is_ready(42, SimTime::ZERO))
387    }
388}