Skip to main content

wowlab_engine_combat/state/runtime/
buff_cache.rs

1//! Actor-scoped static buff-total caches.
2
3use std::cell::Cell;
4
5use wowlab_types::sim::{ActorId, PetIdx};
6
7#[cfg(feature = "rotation-optimizations")]
8use crate::state::buff_effect::DAMAGE_SCHOOL_COUNT;
9use crate::state::{BuffTotals, buff_effect::PlayerScalingBuffs};
10
11/// Compact static buff projection consumed by outgoing-damage setup.
12#[derive(Clone, Copy, Debug, PartialEq)]
13#[cfg(feature = "rotation-optimizations")]
14pub(crate) struct DamageBuffProjection {
15    pub(crate) attack_power: f64,
16    pub(crate) spell_power: f64,
17    pub(crate) attack_power_pct: f64,
18    pub(crate) crit_pct: f64,
19    pub(crate) mastery: f64,
20    pub(crate) vers_pct: f64,
21    pub(crate) damage_mult: f64,
22    pub(crate) pet_damage_mult: f64,
23    pub(crate) auto_attack_damage_mult: f64,
24    pub(crate) auto_attack_crit_pct: f64,
25    pub(crate) school_damage_mult: [f64; DAMAGE_SCHOOL_COUNT],
26}
27
28#[cfg(feature = "rotation-optimizations")]
29impl Default for DamageBuffProjection {
30    fn default() -> Self {
31        Self {
32            attack_power: 0.0,
33            spell_power: 0.0,
34            attack_power_pct: 0.0,
35            crit_pct: 0.0,
36            mastery: 0.0,
37            vers_pct: 0.0,
38            damage_mult: 1.0,
39            pet_damage_mult: 1.0,
40            auto_attack_damage_mult: 1.0,
41            auto_attack_crit_pct: 0.0,
42            school_damage_mult: [1.0; DAMAGE_SCHOOL_COUNT],
43        }
44    }
45}
46
47#[derive(Clone, Copy, Debug, Default)]
48struct BuffCacheEntry {
49    valid: bool,
50    revision: u64,
51    owner_revision: u64,
52    totals: BuffTotals,
53}
54
55#[derive(Debug)]
56struct ActorCaches<T>
57where
58    T: Copy,
59{
60    player: Cell<T>,
61    external: Cell<T>,
62    pets: Box<[Cell<T>]>,
63    enemies: Box<[Cell<T>]>,
64}
65
66impl<T> ActorCaches<T>
67where
68    T: Copy,
69{
70    fn new(enemy_count: usize, initial: T) -> Self {
71        Self {
72            player: Cell::new(initial),
73            external: Cell::new(initial),
74            pets: vec![Cell::new(initial); PetIdx::MAX].into_boxed_slice(),
75            enemies: vec![Cell::new(initial); enemy_count].into_boxed_slice(),
76        }
77    }
78
79    fn get(&self, actor: ActorId) -> Option<&Cell<T>> {
80        match actor {
81            ActorId::Player => Some(&self.player),
82            ActorId::External => Some(&self.external),
83            ActorId::Pet(pet) => self.pets.get(pet.as_usize()),
84            ActorId::Enemy(enemy) => self.enemies.get(enemy.as_usize()),
85        }
86    }
87
88    fn clear(&self, initial: T) {
89        self.player.set(initial);
90        self.external.set(initial);
91
92        for cache in &self.pets {
93            cache.set(initial);
94        }
95
96        for cache in &self.enemies {
97            cache.set(initial);
98        }
99    }
100}
101
102#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
103#[cfg(feature = "rotation-optimizations")]
104pub(crate) struct DamageProjectionKey {
105    revision: u64,
106    owner_revision: u64,
107    #[cfg(any(test, feature = "test-support"))]
108    sheet_attack_power_bits: u64,
109    #[cfg(any(test, feature = "test-support"))]
110    sheet_spell_power_bits: u64,
111    #[cfg(any(test, feature = "test-support"))]
112    sheet_crit_bits: u64,
113    #[cfg(any(test, feature = "test-support"))]
114    sheet_mastery_bits: u64,
115    #[cfg(any(test, feature = "test-support"))]
116    sheet_vers_bits: u64,
117}
118
119#[derive(Clone, Copy, Debug)]
120#[cfg(all(
121    feature = "rotation-optimizations",
122    any(test, feature = "test-support")
123))]
124pub(crate) struct DamageProjectionSheetStats {
125    pub(crate) attack_power: f64,
126    pub(crate) spell_power: f64,
127    pub(crate) crit: f64,
128    pub(crate) mastery: f64,
129    pub(crate) versatility: f64,
130}
131
132#[cfg(feature = "rotation-optimizations")]
133impl DamageProjectionKey {
134    #[cfg(any(test, feature = "test-support"))]
135    pub(crate) fn new(
136        revision: u64,
137        owner_revision: u64,
138        sheet: DamageProjectionSheetStats,
139    ) -> Self {
140        Self {
141            revision,
142            owner_revision,
143            sheet_attack_power_bits: sheet.attack_power.to_bits(),
144            sheet_spell_power_bits: sheet.spell_power.to_bits(),
145            sheet_crit_bits: sheet.crit.to_bits(),
146            sheet_mastery_bits: sheet.mastery.to_bits(),
147            sheet_vers_bits: sheet.versatility.to_bits(),
148        }
149    }
150
151    #[cfg(not(any(test, feature = "test-support")))]
152    pub(crate) const fn new(revision: u64, owner_revision: u64) -> Self {
153        Self {
154            revision,
155            owner_revision,
156        }
157    }
158}
159
160#[derive(Clone, Copy, Debug, Default)]
161#[cfg(feature = "rotation-optimizations")]
162struct DamageProjectionCacheEntry {
163    valid: bool,
164    key: DamageProjectionKey,
165    projection: DamageBuffProjection,
166}
167
168/// Separate raw-actor and inherited-owner-filtered cache sets.
169#[derive(Debug)]
170pub(in crate::state) struct BuffTotalsCaches {
171    actor: ActorCaches<BuffCacheEntry>,
172    actor_only: ActorCaches<BuffCacheEntry>,
173    #[cfg(feature = "rotation-optimizations")]
174    damage: ActorCaches<DamageProjectionCacheEntry>,
175    player_scaling: Cell<PlayerScalingCacheEntry>,
176}
177
178#[derive(Clone, Copy, Debug, Default)]
179struct PlayerScalingCacheEntry {
180    valid: bool,
181    revision: u64,
182    buffs: PlayerScalingBuffs,
183}
184
185impl BuffTotalsCaches {
186    pub(super) fn new(enemy_count: usize) -> Self {
187        Self {
188            actor: ActorCaches::new(enemy_count, BuffCacheEntry::default()),
189            actor_only: ActorCaches::new(enemy_count, BuffCacheEntry::default()),
190            #[cfg(feature = "rotation-optimizations")]
191            damage: ActorCaches::new(enemy_count, DamageProjectionCacheEntry::default()),
192            player_scaling: Cell::new(PlayerScalingCacheEntry::default()),
193        }
194    }
195
196    pub(super) fn actor(&self, actor: ActorId, revision: u64) -> Option<BuffTotals> {
197        let cached = self.actor.get(actor)?.get();
198
199        (cached.valid && cached.revision == revision).then_some(cached.totals)
200    }
201
202    pub(super) fn store_actor(&self, actor: ActorId, revision: u64, totals: &BuffTotals) {
203        if let Some(cache) = self.actor.get(actor) {
204            cache.set(BuffCacheEntry {
205                valid: true,
206                revision,
207                owner_revision: 0,
208                totals: *totals,
209            });
210        }
211    }
212
213    pub(super) fn actor_only(
214        &self,
215        actor: ActorId,
216        revision: u64,
217        owner_revision: u64,
218    ) -> Option<BuffTotals> {
219        let cached = self.actor_only.get(actor)?.get();
220
221        (cached.valid && cached.revision == revision && cached.owner_revision == owner_revision)
222            .then_some(cached.totals)
223    }
224
225    pub(super) fn store_actor_only(
226        &self,
227        actor: ActorId,
228        revision: u64,
229        owner_revision: u64,
230        totals: &BuffTotals,
231    ) {
232        if let Some(cache) = self.actor_only.get(actor) {
233            cache.set(BuffCacheEntry {
234                valid: true,
235                revision,
236                owner_revision,
237                totals: *totals,
238            });
239        }
240    }
241
242    pub(super) fn clear(&self) {
243        self.actor.clear(BuffCacheEntry::default());
244        self.actor_only.clear(BuffCacheEntry::default());
245        #[cfg(feature = "rotation-optimizations")]
246        self.damage.clear(DamageProjectionCacheEntry::default());
247    }
248
249    pub(super) fn player_scaling(&self, revision: u64) -> Option<PlayerScalingBuffs> {
250        let cached = self.player_scaling.get();
251
252        (cached.valid && cached.revision == revision).then_some(cached.buffs)
253    }
254
255    pub(super) fn store_player_scaling(&self, revision: u64, buffs: PlayerScalingBuffs) {
256        self.player_scaling.set(PlayerScalingCacheEntry {
257            valid: true,
258            revision,
259            buffs,
260        });
261    }
262
263    pub(super) fn clear_player_scaling(&self) {
264        self.player_scaling.set(PlayerScalingCacheEntry::default());
265    }
266
267    #[cfg(feature = "rotation-optimizations")]
268    fn damage(&self, actor: ActorId, key: DamageProjectionKey) -> Option<DamageBuffProjection> {
269        let cached = self.damage.get(actor)?.get();
270
271        (cached.valid && cached.key == key).then_some(cached.projection)
272    }
273
274    #[cfg(feature = "rotation-optimizations")]
275    fn store_damage(
276        &self,
277        actor: ActorId,
278        key: DamageProjectionKey,
279        projection: DamageBuffProjection,
280    ) {
281        if let Some(cache) = self.damage.get(actor) {
282            cache.set(DamageProjectionCacheEntry {
283                valid: true,
284                key,
285                projection,
286            });
287        }
288    }
289}
290
291#[cfg(feature = "rotation-optimizations")]
292impl super::CombatInstance {
293    pub(crate) fn cached_damage_buff_projection(
294        &self,
295        actor: ActorId,
296        key: DamageProjectionKey,
297    ) -> Option<DamageBuffProjection> {
298        self.buff_totals_caches.damage(actor, key)
299    }
300
301    pub(crate) fn cache_damage_buff_projection(
302        &self,
303        actor: ActorId,
304        key: DamageProjectionKey,
305        projection: DamageBuffProjection,
306    ) {
307        self.buff_totals_caches.store_damage(actor, key, projection);
308    }
309}