1use 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#[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#[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#[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#[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}