Skip to main content

wowlab_engine_domain/rotation/buffer/
layout.rs

1// #t(file: rust_duplicate_strings) Rust attributes require the repeated literal reason at every field they govern.
2
3//! Frozen dense-buffer layout and construction-validated slot handles.
4
5use std::{fmt::Write as _, sync::Arc};
6
7use wowlab_types::{
8    combat::ResourceType,
9    sim::{AuraKey, AuraProjectionKey, FastSet, SpellIdx},
10};
11
12use super::{
13    AuraSlot, BufferOffsets, ByteOffset, CooldownSlot, DefaultValue, EquipmentSlot, HeroTreeSlot,
14    HistorySlot, ItemSlot, ResourceSlot, SetBonusSlot, SpellSlot, StandaloneDefault, SwingSlot,
15    TalentSlot, UnitSlot, WeaponImbueSlot,
16};
17
18fn valid_slot<T>(offset: ByteOffset, byte_len: usize) -> bool {
19    offset.as_usize().is_multiple_of(align_of::<T>())
20        && offset
21            .as_usize()
22            .checked_add(size_of::<T>())
23            .is_some_and(|end| end <= byte_len)
24}
25
26macro_rules! slot_handle {
27    ($name:ident) => {
28        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
29        #[must_use]
30        pub struct $name(ByteOffset);
31
32        impl $name {
33            pub(crate) const fn offset(self) -> ByteOffset {
34                self.0
35            }
36        }
37    };
38}
39
40slot_handle!(SpellSlotHandle);
41slot_handle!(AuraSlotHandle);
42slot_handle!(AuraProjectionSlotHandle);
43slot_handle!(CooldownSlotHandle);
44slot_handle!(HistorySlotHandle);
45slot_handle!(ResourceSlotHandle);
46slot_handle!(UnitSlotHandle);
47slot_handle!(TalentSlotHandle);
48slot_handle!(HeroTreeSlotHandle);
49slot_handle!(ItemSlotHandle);
50slot_handle!(SwingSlotHandle);
51slot_handle!(EquipmentSlotHandle);
52slot_handle!(SetBonusSlotHandle);
53slot_handle!(WeaponImbueSlotHandle);
54
55/// Deterministic identity of one complete frozen buffer layout.
56#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
57#[must_use]
58pub struct LayoutFingerprint(u64);
59
60impl LayoutFingerprint {
61    #[must_use]
62    pub const fn as_u64(&self) -> u64 {
63        self.0
64    }
65}
66
67/// Additional runtime identities that must exist independently of rotation reads.
68#[derive(Clone, Debug, Default)]
69pub struct BufferRequirements {
70    pub(crate) cooldowns: Vec<SpellIdx>,
71    cooldown_set: FastSet<SpellIdx>,
72    pub(crate) auras: Vec<AuraKey>,
73    aura_set: FastSet<AuraKey>,
74    pub(crate) resources: Vec<ResourceType>,
75    resource_set: FastSet<ResourceType>,
76    pub(crate) spells: Vec<SpellIdx>,
77    spell_set: FastSet<SpellIdx>,
78    pub(crate) talents: Vec<Box<str>>,
79    pub(crate) hero_trees: Vec<Box<str>>,
80    pub(crate) items: Vec<Box<str>>,
81    pub(crate) swings: Vec<Box<str>>,
82    pub(crate) history: Vec<SpellIdx>,
83    history_set: FastSet<SpellIdx>,
84    pub(crate) equipment: Vec<Box<str>>,
85    pub(crate) set_bonuses: Vec<Box<str>>,
86    pub(crate) units: Vec<Box<str>>,
87    pub(crate) weapon_imbues: Vec<Box<str>>,
88}
89
90macro_rules! add_copy_requirement {
91    ($name:ident, $field:ident, $set:ident, $Key:ty) => {
92        pub fn $name(&mut self, key: $Key) {
93            if self.$set.insert(key) {
94                self.$field.push(key);
95            }
96        }
97    };
98}
99
100macro_rules! add_named_requirement {
101    ($name:ident, $field:ident) => {
102        pub fn $name(&mut self, key: impl Into<Box<str>>) {
103            let key = key.into();
104
105            if !self.$field.contains(&key) {
106                self.$field.push(key);
107            }
108        }
109    };
110}
111
112impl BufferRequirements {
113    #[must_use]
114    pub fn new() -> Self {
115        Self {
116            cooldowns: Vec::new(),
117            cooldown_set: FastSet::default(),
118            auras: Vec::new(),
119            aura_set: FastSet::default(),
120            resources: Vec::new(),
121            resource_set: FastSet::default(),
122            spells: Vec::new(),
123            spell_set: FastSet::default(),
124            talents: Vec::new(),
125            hero_trees: Vec::new(),
126            items: Vec::new(),
127            swings: Vec::new(),
128            history: Vec::new(),
129            history_set: FastSet::default(),
130            equipment: Vec::new(),
131            set_bonuses: Vec::new(),
132            units: Vec::new(),
133            weapon_imbues: Vec::new(),
134        }
135    }
136
137    #[must_use]
138    pub fn aura_keys(&self) -> &[AuraKey] {
139        &self.auras
140    }
141    #[must_use]
142    pub fn resource_keys(&self) -> &[ResourceType] {
143        &self.resources
144    }
145    add_copy_requirement!(cooldown, cooldowns, cooldown_set, SpellIdx);
146    add_copy_requirement!(aura, auras, aura_set, AuraKey);
147    add_copy_requirement!(resource, resources, resource_set, ResourceType);
148    add_copy_requirement!(spell, spells, spell_set, SpellIdx);
149    add_copy_requirement!(history, history, history_set, SpellIdx);
150    add_named_requirement!(talent, talents);
151    add_named_requirement!(hero_tree, hero_trees);
152    add_named_requirement!(item, items);
153    add_named_requirement!(swing, swings);
154    add_named_requirement!(equipment, equipment);
155    add_named_requirement!(set_bonus, set_bonuses);
156
157    add_named_requirement!(unit, units);
158
159    add_named_requirement!(weapon_imbue, weapon_imbues);
160}
161
162/// Immutable slot layout shared by a combat program and its runtime buffer.
163#[derive(Debug)]
164pub(crate) struct FrozenBufferLayout {
165    pub(crate) offsets: BufferOffsets,
166    byte_len: usize,
167    standalone_defaults: Box<[(ByteOffset, &'static DefaultValue)]>,
168    bindings: FrozenBufferBindings,
169    history: Box<[HistorySlotHandle]>,
170    fingerprint: LayoutFingerprint,
171}
172
173/// Construction-validated bindings with stable traversal and no keyed lookup.
174#[derive(Debug)]
175pub(crate) struct FrozenBufferBindings {
176    #[cfg_attr(
177        not(test),
178        expect(
179            dead_code,
180            reason = "retained for complete schema construction and layout tests"
181        )
182    )]
183    concrete_auras: Box<[(AuraKey, AuraSlotHandle)]>,
184    aura_projections: Box<[(AuraProjectionKey, AuraProjectionSlotHandle)]>,
185    spells: Box<[(SpellIdx, SpellSlotHandle)]>,
186    cooldowns: Box<[(SpellIdx, CooldownSlotHandle)]>,
187    #[cfg_attr(
188        not(test),
189        expect(
190            dead_code,
191            reason = "retained for complete schema construction and layout tests"
192        )
193    )]
194    history: Box<[(SpellIdx, HistorySlotHandle)]>,
195    resources: Box<[(ResourceType, ResourceSlotHandle)]>,
196    #[cfg_attr(
197        not(test),
198        expect(
199            dead_code,
200            reason = "retained for complete schema construction and layout tests"
201        )
202    )]
203    hero_trees: Box<[(Box<str>, HeroTreeSlotHandle)]>,
204    items: Box<[(Box<str>, ItemSlotHandle)]>,
205    swings: Box<[(Box<str>, SwingSlotHandle)]>,
206    #[cfg_attr(
207        not(test),
208        expect(
209            dead_code,
210            reason = "retained for complete schema construction and layout tests"
211        )
212    )]
213    equipment: Box<[(Box<str>, EquipmentSlotHandle)]>,
214    #[cfg_attr(
215        not(test),
216        expect(
217            dead_code,
218            reason = "retained for complete schema construction and layout tests"
219        )
220    )]
221    set_bonuses: Box<[(Box<str>, SetBonusSlotHandle)]>,
222    units: Box<[(Box<str>, UnitSlotHandle)]>,
223    weapon_imbues: Box<[(Box<str>, WeaponImbueSlotHandle)]>,
224}
225
226macro_rules! copy_bindings {
227    ($name:ident, $Key:ty, $Handle:ty, $field:ident) => {
228        pub(crate) fn $name(&self) -> impl ExactSizeIterator<Item = ($Key, $Handle)> + '_ {
229            self.$field.iter().copied()
230        }
231    };
232}
233
234macro_rules! named_bindings {
235    ($name:ident, $Handle:ty, $field:ident) => {
236        pub(crate) fn $name(&self) -> impl ExactSizeIterator<Item = (&str, $Handle)> + '_ {
237            self.$field
238                .iter()
239                .map(|(key, handle)| (key.as_ref(), *handle))
240        }
241    };
242}
243
244impl FrozenBufferBindings {
245    #[cfg(test)]
246    copy_bindings!(auras, AuraKey, AuraSlotHandle, concrete_auras);
247    copy_bindings!(
248        aura_projections,
249        AuraProjectionKey,
250        AuraProjectionSlotHandle,
251        aura_projections
252    );
253    copy_bindings!(spells, SpellIdx, SpellSlotHandle, spells);
254    copy_bindings!(cooldowns, SpellIdx, CooldownSlotHandle, cooldowns);
255    #[cfg(test)]
256    copy_bindings!(history, SpellIdx, HistorySlotHandle, history);
257    copy_bindings!(resources, ResourceType, ResourceSlotHandle, resources);
258    #[cfg(test)]
259    named_bindings!(hero_trees, HeroTreeSlotHandle, hero_trees);
260    named_bindings!(items, ItemSlotHandle, items);
261    named_bindings!(swings, SwingSlotHandle, swings);
262    #[cfg(test)]
263    named_bindings!(equipment, EquipmentSlotHandle, equipment);
264    #[cfg(test)]
265    named_bindings!(set_bonuses, SetBonusSlotHandle, set_bonuses);
266    named_bindings!(units, UnitSlotHandle, units);
267    named_bindings!(weapon_imbues, WeaponImbueSlotHandle, weapon_imbues);
268}
269
270impl FrozenBufferLayout {
271    #[must_use]
272    pub(crate) const fn byte_len(&self) -> usize {
273        self.byte_len
274    }
275
276    pub(crate) const fn fingerprint(&self) -> LayoutFingerprint {
277        self.fingerprint
278    }
279
280    pub(crate) fn history_handles(&self) -> &[HistorySlotHandle] {
281        &self.history
282    }
283
284    pub(crate) const fn bindings(&self) -> &FrozenBufferBindings {
285        &self.bindings
286    }
287
288    pub(crate) fn spell_handle(&self, key: SpellIdx) -> Option<SpellSlotHandle> {
289        wowlab_types::bench_metrics::record_keyed_lookup();
290
291        self.offsets
292            .slots
293            .spells
294            .get(&key)
295            .copied()
296            .filter(|&offset| valid_slot::<SpellSlot>(offset, self.byte_len))
297            .map(SpellSlotHandle)
298    }
299
300    pub(crate) fn aura_handle(&self, key: AuraKey) -> Option<AuraSlotHandle> {
301        wowlab_types::bench_metrics::record_keyed_lookup();
302
303        self.offsets
304            .slots
305            .auras
306            .get(&key)
307            .copied()
308            .filter(|&offset| valid_slot::<AuraSlot>(offset, self.byte_len))
309            .map(AuraSlotHandle)
310    }
311
312    pub(crate) fn cooldown_handle(&self, key: SpellIdx) -> Option<CooldownSlotHandle> {
313        wowlab_types::bench_metrics::record_keyed_lookup();
314
315        self.offsets
316            .slots
317            .cooldowns
318            .get(&key)
319            .copied()
320            .filter(|&offset| valid_slot::<CooldownSlot>(offset, self.byte_len))
321            .map(CooldownSlotHandle)
322    }
323
324    pub(crate) fn history_handle(&self, key: SpellIdx) -> Option<HistorySlotHandle> {
325        wowlab_types::bench_metrics::record_keyed_lookup();
326
327        self.offsets
328            .slots
329            .history
330            .get(&key)
331            .copied()
332            .filter(|&offset| valid_slot::<HistorySlot>(offset, self.byte_len))
333            .map(HistorySlotHandle)
334    }
335
336    pub(crate) fn resource_handle(&self, key: ResourceType) -> Option<ResourceSlotHandle> {
337        wowlab_types::bench_metrics::record_keyed_lookup();
338
339        self.offsets
340            .slots
341            .resources
342            .get(&key)
343            .copied()
344            .filter(|&offset| valid_slot::<ResourceSlot>(offset, self.byte_len))
345            .map(ResourceSlotHandle)
346    }
347
348    pub(crate) fn unit_handle(&self, key: &str) -> Option<UnitSlotHandle> {
349        wowlab_types::bench_metrics::record_keyed_lookup();
350
351        self.offsets
352            .slots
353            .units
354            .get(key)
355            .copied()
356            .filter(|&offset| valid_slot::<UnitSlot>(offset, self.byte_len))
357            .map(UnitSlotHandle)
358    }
359
360    pub(crate) fn talent_handle(&self, key: &str) -> Option<TalentSlotHandle> {
361        wowlab_types::bench_metrics::record_keyed_lookup();
362
363        self.offsets
364            .slots
365            .talents
366            .get(key)
367            .copied()
368            .filter(|&offset| valid_slot::<TalentSlot>(offset, self.byte_len))
369            .map(TalentSlotHandle)
370    }
371
372    pub(crate) fn freeze(offsets: BufferOffsets, byte_len: usize) -> Arc<Self> {
373        let mut standalone_defaults = Vec::with_capacity(offsets.standalone.len());
374
375        for (&descriptor_id, &offset) in &offsets.standalone {
376            if let Some(default) = inventory::iter::<StandaloneDefault>
377                .into_iter()
378                .find(|default| (default.matches)(descriptor_id))
379            {
380                standalone_defaults.push((offset, &default.value));
381            }
382        }
383
384        let mut concrete_auras: Vec<_> = offsets
385            .slots
386            .auras
387            .iter()
388            .filter(|&(_, &offset)| valid_slot::<AuraSlot>(offset, byte_len))
389            .map(|(&key, &offset)| (key, AuraSlotHandle(offset)))
390            .collect();
391
392        concrete_auras.sort_unstable_by_key(|(_, handle)| *handle);
393
394        let mut aura_projections: Vec<_> = offsets
395            .slots
396            .aura_projections
397            .iter()
398            .filter(|&(_, &offset)| valid_slot::<AuraSlot>(offset, byte_len))
399            .map(|(&key, &offset)| (key, AuraProjectionSlotHandle(offset)))
400            .collect();
401
402        aura_projections.sort_unstable_by_key(|(_, handle)| *handle);
403
404        let mut spells: Vec<_> = offsets
405            .slots
406            .spells
407            .iter()
408            .filter(|&(_, &offset)| valid_slot::<SpellSlot>(offset, byte_len))
409            .map(|(&key, &offset)| (key, SpellSlotHandle(offset)))
410            .collect();
411
412        spells.sort_unstable_by_key(|(_, handle)| *handle);
413
414        let mut cooldowns: Vec<_> = offsets
415            .slots
416            .cooldowns
417            .iter()
418            .filter(|&(_, &offset)| valid_slot::<CooldownSlot>(offset, byte_len))
419            .map(|(&key, &offset)| (key, CooldownSlotHandle(offset)))
420            .collect();
421
422        cooldowns.sort_unstable_by_key(|(_, handle)| *handle);
423
424        let mut history_bindings: Vec<_> = offsets
425            .slots
426            .history
427            .iter()
428            .filter(|&(_, &offset)| valid_slot::<HistorySlot>(offset, byte_len))
429            .map(|(&key, &offset)| (key, HistorySlotHandle(offset)))
430            .collect();
431
432        history_bindings.sort_unstable_by_key(|(_, handle)| *handle);
433        let history = history_bindings
434            .iter()
435            .map(|(_, handle)| *handle)
436            .collect::<Vec<_>>();
437        let mut resources: Vec<_> = offsets
438            .slots
439            .resources
440            .iter()
441            .filter(|&(_, &offset)| valid_slot::<ResourceSlot>(offset, byte_len))
442            .map(|(&key, &offset)| (key, ResourceSlotHandle(offset)))
443            .collect();
444
445        resources.sort_unstable_by_key(|(_, handle)| *handle);
446
447        let mut units: Vec<_> = offsets
448            .slots
449            .units
450            .iter()
451            .filter(|&(_, &offset)| valid_slot::<UnitSlot>(offset, byte_len))
452            .map(|(key, &offset)| (Box::<str>::from(key.as_str()), UnitSlotHandle(offset)))
453            .collect();
454
455        units.sort_unstable_by_key(|(_, handle)| *handle);
456
457        macro_rules! collect_named {
458            ($map:expr, $Slot:ty, $Handle:ident) => {{
459                let mut entries: Vec<_> = $map
460                    .iter()
461                    .filter(|&(_, &offset)| valid_slot::<$Slot>(offset, byte_len))
462                    .map(|(key, &offset)| (Box::<str>::from(key.as_str()), $Handle(offset)))
463                    .collect();
464
465                entries.sort_unstable_by_key(|(_, handle)| *handle);
466                entries.into_boxed_slice()
467            }};
468        }
469
470        let hero_trees =
471            collect_named!(&offsets.slots.hero_trees, HeroTreeSlot, HeroTreeSlotHandle);
472        let items = collect_named!(&offsets.slots.items, ItemSlot, ItemSlotHandle);
473        let swings = collect_named!(&offsets.slots.swings, SwingSlot, SwingSlotHandle);
474        let equipment =
475            collect_named!(&offsets.slots.equipment, EquipmentSlot, EquipmentSlotHandle);
476        let set_bonuses =
477            collect_named!(&offsets.slots.set_bonuses, SetBonusSlot, SetBonusSlotHandle);
478        let weapon_imbues = collect_named!(
479            &offsets.slots.weapon_imbues,
480            WeaponImbueSlot,
481            WeaponImbueSlotHandle
482        );
483
484        let fingerprint = fingerprint(&offsets, byte_len);
485
486        Arc::new(Self {
487            offsets,
488            byte_len,
489            standalone_defaults: standalone_defaults.into_boxed_slice(),
490            bindings: FrozenBufferBindings {
491                concrete_auras: concrete_auras.into_boxed_slice(),
492                aura_projections: aura_projections.into_boxed_slice(),
493                spells: spells.into_boxed_slice(),
494                cooldowns: cooldowns.into_boxed_slice(),
495                history: history_bindings.into_boxed_slice(),
496                resources: resources.into_boxed_slice(),
497                hero_trees,
498                items,
499                swings,
500                equipment,
501                set_bonuses,
502                units: units.into_boxed_slice(),
503                weapon_imbues,
504            },
505            history: history.into_boxed_slice(),
506            fingerprint,
507        })
508    }
509
510    pub(crate) fn standalone_defaults(&self) -> &[(ByteOffset, &'static DefaultValue)] {
511        &self.standalone_defaults
512    }
513}
514
515fn fingerprint(offsets: &BufferOffsets, byte_len: usize) -> LayoutFingerprint {
516    const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
517    const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
518
519    let mut entries = Vec::new();
520
521    macro_rules! add_entries {
522        ($tag:literal, $map:expr) => {
523            for (key, offset) in $map {
524                let mut entry = String::new();
525                let _ = write!(&mut entry, "{}:{:?}:{}", $tag, key, offset.as_usize());
526                entries.push(entry);
527            }
528        };
529    }
530
531    add_entries!("cooldown", &offsets.slots.cooldowns);
532    add_entries!("aura", &offsets.slots.auras);
533    add_entries!("projection", &offsets.slots.aura_projections);
534    add_entries!("resource", &offsets.slots.resources);
535    add_entries!("spell", &offsets.slots.spells);
536    add_entries!("talent", &offsets.slots.talents);
537    add_entries!("hero_tree", &offsets.slots.hero_trees);
538    add_entries!("item", &offsets.slots.items);
539    add_entries!("swing", &offsets.slots.swings);
540    add_entries!("history", &offsets.slots.history);
541    add_entries!("equipment", &offsets.slots.equipment);
542    add_entries!("set_bonus", &offsets.slots.set_bonuses);
543    add_entries!("unit", &offsets.slots.units);
544    add_entries!("weapon_imbue", &offsets.slots.weapon_imbues);
545    add_entries!("standalone", &offsets.standalone);
546    entries.sort_unstable();
547
548    let mut hash = FNV_OFFSET_BASIS;
549    let mut feed = |bytes: &[u8]| {
550        for byte in bytes {
551            hash ^= u64::from(*byte);
552            hash = hash.wrapping_mul(FNV_PRIME);
553        }
554    };
555
556    feed(&byte_len.to_le_bytes());
557    feed(&offsets.player.as_usize().to_le_bytes());
558    feed(&offsets.combat.as_usize().to_le_bytes());
559    feed(&offsets.pet.as_usize().to_le_bytes());
560
561    for entry in entries {
562        feed(entry.as_bytes());
563        feed(&[0]);
564    }
565
566    LayoutFingerprint(hash)
567}
568
569#[cfg(test)]
570mod tests {
571    use googletest::prelude::*;
572    use wowlab_types::sim::{ActorId, AuraIdx, AuraOn};
573
574    use super::*;
575    use crate::rotation::buffer::SlotMaps;
576
577    fn base_offsets(slots: SlotMaps<ByteOffset>) -> BufferOffsets {
578        BufferOffsets {
579            slots,
580            player: ByteOffset::new(0),
581            combat: ByteOffset::new(0),
582            pet: ByteOffset::new(0),
583            standalone: wowlab_types::sim::FastMap::default(),
584        }
585    }
586
587    #[gtest]
588    fn every_slot_kind_binds_only_after_bounds_validation() -> Result<()> {
589        #[cfg(feature = "bench-metrics")]
590        wowlab_types::bench_metrics::reset();
591
592        let spell = SpellIdx::from_raw(7);
593        let aura = AuraKey::new(AuraIdx(8), ActorId::Player, ActorId::Player, AuraOn::Player);
594        let projection = AuraProjectionKey::new(AuraIdx(8), AuraOn::Player);
595        let mut slots = SlotMaps::default();
596        let mut offset = 0;
597
598        macro_rules! insert {
599            ($map:ident, $key:expr, $Slot:ty) => {{
600                slots.$map.insert($key, ByteOffset::new(offset));
601                offset += size_of::<$Slot>();
602            }};
603        }
604
605        insert!(spells, spell, SpellSlot);
606        insert!(auras, aura, AuraSlot);
607        insert!(aura_projections, projection, AuraSlot);
608        insert!(cooldowns, spell, CooldownSlot);
609        insert!(history, spell, HistorySlot);
610        insert!(resources, ResourceType::Mana, ResourceSlot);
611        insert!(units, "target".to_string(), UnitSlot);
612        insert!(talents, "talent".to_string(), TalentSlot);
613        insert!(hero_trees, "hero".to_string(), HeroTreeSlot);
614        insert!(items, "item".to_string(), ItemSlot);
615        insert!(swings, "swing".to_string(), SwingSlot);
616        insert!(equipment, "equipment".to_string(), EquipmentSlot);
617        insert!(set_bonuses, "set".to_string(), SetBonusSlot);
618        insert!(weapon_imbues, "imbue".to_string(), WeaponImbueSlot);
619
620        let layout = FrozenBufferLayout::freeze(base_offsets(slots), offset);
621
622        verify_that!(layout.spell_handle(spell), some(anything()))?;
623        verify_that!(layout.aura_handle(aura), some(anything()))?;
624        verify_true!(
625            layout
626                .bindings()
627                .aura_projections()
628                .any(|(key, _)| key == projection)
629        )?;
630        verify_that!(layout.cooldown_handle(spell), some(anything()))?;
631        verify_true!(layout.bindings().history().any(|(key, _)| key == spell))?;
632        verify_that!(layout.resource_handle(ResourceType::Mana), some(anything()))?;
633        verify_that!(layout.unit_handle("target"), some(anything()))?;
634        verify_that!(layout.talent_handle("talent"), some(anything()))?;
635        verify_true!(layout.bindings().hero_trees().any(|(key, _)| key == "hero"))?;
636        verify_true!(layout.bindings().items().any(|(key, _)| key == "item"))?;
637        verify_true!(layout.bindings().swings().any(|(key, _)| key == "swing"))?;
638        verify_true!(
639            layout
640                .bindings()
641                .equipment()
642                .any(|(key, _)| key == "equipment")
643        )?;
644        verify_true!(layout.bindings().set_bonuses().any(|(key, _)| key == "set"))?;
645
646        verify_true!(
647            layout
648                .bindings()
649                .weapon_imbues()
650                .any(|(key, _)| key == "imbue")
651        )?;
652
653        #[cfg(feature = "bench-metrics")]
654        verify_that!(wowlab_types::bench_metrics::snapshot().keyed_lookups, eq(6))?;
655
656        Ok(())
657    }
658
659    #[gtest]
660    fn wrong_kind_misaligned_and_out_of_bounds_bindings_are_rejected() -> Result<()> {
661        let wrong_kind = SpellIdx::from_raw(9);
662        let misaligned = SpellIdx::from_raw(10);
663        let out_of_bounds = SpellIdx::from_raw(11);
664        let mut slots = SlotMaps::default();
665
666        slots.cooldowns.insert(wrong_kind, ByteOffset::new(0));
667        slots.spells.insert(misaligned, ByteOffset::new(1));
668        slots.spells.insert(out_of_bounds, ByteOffset::new(64));
669        let layout = FrozenBufferLayout::freeze(base_offsets(slots), 64);
670
671        verify_that!(layout.spell_handle(wrong_kind), none())?;
672        verify_that!(layout.cooldown_handle(wrong_kind), some(anything()))?;
673        verify_that!(layout.spell_handle(misaligned), none())?;
674
675        verify_that!(layout.spell_handle(out_of_bounds), none())
676    }
677
678    #[gtest]
679    fn concrete_projection_and_history_arrays_are_stable_by_offset() -> Result<()> {
680        let first = AuraKey::new(
681            AuraIdx(20),
682            ActorId::Player,
683            ActorId::Player,
684            AuraOn::Player,
685        );
686        let second = AuraKey::new(
687            AuraIdx(21),
688            ActorId::Player,
689            ActorId::Player,
690            AuraOn::Player,
691        );
692        let first_projection = AuraProjectionKey::new(AuraIdx(20), AuraOn::Player);
693        let second_projection = AuraProjectionKey::new(AuraIdx(21), AuraOn::Player);
694        let mut slots = SlotMaps::default();
695
696        slots.auras.insert(first, ByteOffset::new(AuraSlot::SIZE));
697        slots.auras.insert(second, ByteOffset::new(0));
698        slots
699            .aura_projections
700            .insert(first_projection, ByteOffset::new(AuraSlot::SIZE * 3));
701        slots
702            .aura_projections
703            .insert(second_projection, ByteOffset::new(AuraSlot::SIZE * 2));
704        slots.history.insert(
705            SpellIdx::from_raw(1),
706            ByteOffset::new(AuraSlot::SIZE * 4 + HistorySlot::SIZE),
707        );
708        slots
709            .history
710            .insert(SpellIdx::from_raw(2), ByteOffset::new(AuraSlot::SIZE * 4));
711        let byte_len = AuraSlot::SIZE * 4 + HistorySlot::SIZE * 2;
712        let layout = FrozenBufferLayout::freeze(base_offsets(slots), byte_len);
713        let aura_offsets: Vec<_> = layout
714            .bindings()
715            .auras()
716            .map(|(_, handle)| handle.offset().as_usize())
717            .collect();
718        let projection_offsets: Vec<_> = layout
719            .bindings()
720            .aura_projections()
721            .map(|(_, handle)| handle.offset().as_usize())
722            .collect();
723        let history_offsets: Vec<_> = layout
724            .history_handles()
725            .iter()
726            .map(|handle| handle.offset().as_usize())
727            .collect();
728
729        verify_that!(aura_offsets, eq(&vec![0, AuraSlot::SIZE]))?;
730        verify_that!(
731            projection_offsets,
732            eq(&vec![AuraSlot::SIZE * 2, AuraSlot::SIZE * 3])
733        )?;
734
735        verify_that!(
736            history_offsets,
737            eq(&vec![
738                AuraSlot::SIZE * 4,
739                AuraSlot::SIZE * 4 + HistorySlot::SIZE
740            ])
741        )
742    }
743}