Skip to main content

wowlab_types/types/sim/
idx.rs

1use derive_more::{Debug, Display, From, Into};
2use nohash_hasher::BuildNoHashHasher;
3use serde::{Deserialize, Serialize};
4
5use super::encounter::ActorId;
6
7pub type IntMap<K, V> = std::collections::HashMap<K, V, BuildNoHashHasher<K>>;
8
9pub type IntSet<K> = std::collections::HashSet<K, BuildNoHashHasher<K>>;
10
11/// Fast deterministic hasher for trusted in-process keys.
12#[derive(Clone, Default)]
13pub struct FastBuildHasher(foldhash::fast::FixedState);
14
15impl FastBuildHasher {
16    #[must_use]
17    pub fn new() -> Self {
18        Self::default()
19    }
20}
21
22impl std::fmt::Debug for FastBuildHasher {
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.write_str("FastBuildHasher")
25    }
26}
27
28impl std::hash::BuildHasher for FastBuildHasher {
29    type Hasher = <foldhash::fast::FixedState as std::hash::BuildHasher>::Hasher;
30
31    fn build_hasher(&self) -> Self::Hasher {
32        self.0.build_hasher()
33    }
34}
35
36/// Marker for keys compatible with the integer identity hasher.
37pub trait IsEnabled: nohash_hasher::IsEnabled {}
38
39impl<T> IsEnabled for T where T: nohash_hasher::IsEnabled {}
40
41pub type FastMap<K, V> = std::collections::HashMap<K, V, FastBuildHasher>;
42
43pub type FastSet<K> = std::collections::HashSet<K, FastBuildHasher>;
44
45macro_rules! define_u32_idx {
46    (
47        $(#[doc = $doc:literal])*
48        $name:ident, debug_prefix = $debug_prefix:literal
49    ) => {
50        $(#[doc = $doc])*
51        #[expect(
52            clippy::unsafe_derive_deserialize,
53            reason = "transparent integer newtypes accept every underlying bit pattern"
54        )]
55        #[derive(
56            Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash,
57            Serialize, Deserialize, Display, From, Into,
58        )]
59        #[display("{_0}")]
60        #[serde(transparent)]
61        #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
62        #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
63        #[repr(transparent)]
64        pub struct $name(pub u32);
65
66        impl $name {
67            #[doc = concat!(stringify!($name), " is `#[repr(transparent)]` over `u32`.")]
68            #[inline]
69            pub fn cast_slice(slice: &[u32]) -> &[Self] {
70                // SAFETY: repr(transparent) over u32
71                unsafe { &*(std::ptr::from_ref(slice) as *const [Self]) }
72            }
73
74            #[inline]
75            pub fn cast_slice_mut(slice: &mut [u32]) -> &mut [Self] {
76                // SAFETY: repr(transparent) over u32
77                unsafe { &mut *(std::ptr::from_mut(slice) as *mut [Self]) }
78            }
79
80            #[inline]
81            pub const fn from_raw(id: u32) -> Self {
82                Self(id)
83            }
84
85            #[inline]
86            pub fn try_from_usize(idx: usize) -> Option<Self> {
87                u32::try_from(idx).ok().map(Self)
88            }
89
90            #[inline]
91            pub const fn as_usize(self) -> usize {
92                self.0 as usize
93            }
94
95            #[inline]
96            pub const fn as_u32(self) -> u32 {
97                self.0
98            }
99        }
100
101        impl std::fmt::Debug for $name {
102            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103                write!(f, concat!(stringify!($name), "({})"), self.0)
104            }
105        }
106
107        impl nohash_hasher::IsEnabled for $name {}
108
109        impl From<$name> for u64 {
110            #[inline]
111            fn from(idx: $name) -> u64 {
112                u64::from(idx.0)
113            }
114        }
115    };
116}
117
118macro_rules! define_u16_idx {
119    (
120        $(#[doc = $doc:literal])*
121        $name:ident, max = $max:expr
122    ) => {
123        $(#[doc = $doc])*
124        #[expect(
125            clippy::unsafe_derive_deserialize,
126            // #t(rust_duplicate_strings) Rust attributes require a literal reason at the declaration they govern.
127            reason = "transparent integer newtypes accept every underlying bit pattern"
128        )]
129        #[derive(
130            Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash,
131            Serialize, Deserialize, Display, From, Into,
132        )]
133        #[display("{_0}")]
134        #[serde(transparent)]
135        #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
136        #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
137        #[repr(transparent)]
138        pub struct $name(pub u16);
139
140        impl $name {
141            pub const MAX: usize = $max;
142
143            #[doc = concat!(stringify!($name), " is `#[repr(transparent)]` over `u16`.")]
144            #[inline]
145            pub fn cast_slice(slice: &[u16]) -> &[Self] {
146                // SAFETY: repr(transparent) over u16
147                unsafe { &*(std::ptr::from_ref(slice) as *const [Self]) }
148            }
149
150            #[inline]
151            pub fn cast_slice_mut(slice: &mut [u16]) -> &mut [Self] {
152                // SAFETY: repr(transparent) over u16
153                unsafe { &mut *(std::ptr::from_mut(slice) as *mut [Self]) }
154            }
155
156            #[inline]
157            pub const fn from_raw(idx: u16) -> Self {
158                Self(idx)
159            }
160
161            #[inline]
162            pub fn try_from_usize(idx: usize) -> Option<Self> {
163                (idx < Self::MAX)
164                    .then(|| u16::try_from(idx).ok())
165                    .flatten()
166                    .map(Self)
167            }
168
169            #[inline]
170            pub const fn as_usize(self) -> usize {
171                self.0 as usize
172            }
173
174            #[inline]
175            pub const fn as_u16(self) -> u16 {
176                self.0
177            }
178
179            pub fn iter(count: usize) -> impl Iterator<Item = $name> {
180                (0..count.min(Self::MAX)).filter_map($name::try_from_usize)
181            }
182        }
183    };
184}
185
186macro_rules! define_u8_idx {
187    (
188        $(#[doc = $doc:literal])*
189        $name:ident, max = $max:expr
190    ) => {
191        $(#[doc = $doc])*
192        #[expect(
193            clippy::unsafe_derive_deserialize,
194            // #t(rust_duplicate_strings) Rust attributes require a literal reason at the declaration they govern.
195            reason = "transparent integer newtypes accept every underlying bit pattern"
196        )]
197        #[derive(
198            Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash,
199            Serialize, Deserialize, Debug, Display, From, Into,
200        )]
201        #[debug("{}({_0})", stringify!($name))]
202        #[display("{_0}")]
203        #[serde(transparent)]
204        #[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
205        #[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
206        #[repr(transparent)]
207        pub struct $name(pub u8);
208
209        impl $name {
210            pub const MAX: usize = $max;
211
212            #[doc = concat!(stringify!($name), " is `#[repr(transparent)]` over `u8`.")]
213            #[inline]
214            pub fn cast_slice(slice: &[u8]) -> &[Self] {
215                // SAFETY: repr(transparent) over u8
216                unsafe { &*(std::ptr::from_ref(slice) as *const [Self]) }
217            }
218
219            #[inline]
220            pub fn cast_slice_mut(slice: &mut [u8]) -> &mut [Self] {
221                // SAFETY: repr(transparent) over u8
222                unsafe { &mut *(std::ptr::from_mut(slice) as *mut [Self]) }
223            }
224
225            #[inline]
226            pub const fn from_raw(idx: u8) -> Self {
227                Self(idx)
228            }
229
230            #[inline]
231            pub fn try_from_usize(idx: usize) -> Option<Self> {
232                (idx < Self::MAX)
233                    .then(|| u8::try_from(idx).ok())
234                    .flatten()
235                    .map(Self)
236            }
237
238            #[inline]
239            pub const fn as_usize(self) -> usize {
240                self.0 as usize
241            }
242
243            #[inline]
244            pub const fn as_u8(self) -> u8 {
245                self.0
246            }
247        }
248    };
249}
250
251define_u32_idx! {
252    /// A type-safe spell identifier (`WoW` spell ID).
253    SpellIdx, debug_prefix = "SpellIdx({_0})"
254}
255
256impl SpellIdx {
257    #[inline]
258    #[must_use]
259    pub const fn is_valid(self) -> bool {
260        self.0 != 0
261    }
262}
263
264define_u32_idx! {
265    /// A type-safe aura identifier (`WoW` spell ID for auras).
266    AuraIdx, debug_prefix = "AuraIdx({_0})"
267}
268
269impl AuraIdx {
270    #[inline]
271    #[must_use]
272    pub const fn is_valid(self) -> bool {
273        self.0 != 0
274    }
275}
276
277define_u32_idx! {
278    ProcIdx, debug_prefix = "ProcIdx({_0})"
279}
280
281define_u16_idx! {
282    /// A type-safe unit index: 0 player, 1..N pets, N+1.. enemies.
283    UnitIdx, max = u16::MAX as usize
284}
285
286impl UnitIdx {
287    pub const PLAYER: UnitIdx = UnitIdx(0);
288
289    #[inline]
290    #[must_use]
291    pub fn from_usize_saturating(idx: usize) -> Self {
292        Self(u16::try_from(idx.min(Self::MAX)).unwrap_or(u16::MAX))
293    }
294
295    #[inline]
296    #[must_use]
297    pub const fn is_player(self) -> bool {
298        self.0 == 0
299    }
300}
301
302impl std::fmt::Debug for UnitIdx {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        if self.0 == 0 {
305            write!(f, "UnitIdx(PLAYER)")
306        } else {
307            write!(f, "UnitIdx({})", self.0)
308        }
309    }
310}
311
312define_u16_idx! {
313    TargetIdx, max = u16::MAX as usize
314}
315
316impl TargetIdx {
317    pub const PRIMARY: TargetIdx = TargetIdx(0);
318
319    #[inline]
320    #[must_use]
321    pub fn from_usize_saturating(idx: usize) -> Self {
322        Self(u16::try_from(idx.min(Self::MAX)).unwrap_or(u16::MAX))
323    }
324
325    #[inline]
326    #[must_use]
327    pub const fn is_primary(self) -> bool {
328        self.0 == 0
329    }
330}
331
332impl std::fmt::Debug for TargetIdx {
333    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334        if self.0 == 0 {
335            write!(f, "TargetIdx(PRIMARY)")
336        } else {
337            write!(f, "TargetIdx({})", self.0)
338        }
339    }
340}
341
342define_u16_idx! {
343    PetIdx, max = 256
344}
345
346impl PetIdx {
347    pub const PRIMARY: PetIdx = PetIdx(0);
348
349    /// Maps a guardian arena slot after the reserved persistent-primary-pet identity.
350    #[inline]
351    #[must_use]
352    pub fn from_guardian_slot(slot: usize) -> Option<Self> {
353        slot.checked_add(Self::PRIMARY.as_usize() + 1)
354            .and_then(Self::try_from_usize)
355    }
356
357    #[inline]
358    #[must_use]
359    pub const fn is_primary(self) -> bool {
360        self.0 == 0
361    }
362
363    #[inline]
364    #[must_use]
365    pub const fn to_unit_idx(&self) -> UnitIdx {
366        UnitIdx(self.0 + 1)
367    }
368}
369
370impl std::fmt::Debug for PetIdx {
371    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
372        if self.0 == 0 {
373            write!(f, "PetIdx(PRIMARY)")
374        } else {
375            write!(f, "PetIdx({})", self.0)
376        }
377    }
378}
379
380define_u16_idx! {
381    EnemyIdx, max = 256
382}
383
384impl EnemyIdx {
385    pub const PRIMARY: EnemyIdx = EnemyIdx(0);
386
387    #[inline]
388    #[must_use]
389    pub const fn is_primary(self) -> bool {
390        self.0 == 0
391    }
392
393    #[inline]
394    #[must_use]
395    pub const fn to_target_idx(&self) -> TargetIdx {
396        TargetIdx(self.0)
397    }
398}
399
400impl std::fmt::Debug for EnemyIdx {
401    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        if self.0 == 0 {
403            write!(f, "EnemyIdx(PRIMARY)")
404        } else {
405            write!(f, "EnemyIdx({})", self.0)
406        }
407    }
408}
409
410define_u16_idx! {
411    /// Stable identifier for an encounter enemy group.
412    GroupId, max = u16::MAX as usize
413}
414
415impl std::fmt::Debug for GroupId {
416    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
417        write!(f, "GroupId({})", self.0)
418    }
419}
420
421define_u16_idx! {
422    /// Stable identifier for an encounter activation wave.
423    WaveId, max = u16::MAX as usize
424}
425
426impl std::fmt::Debug for WaveId {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        write!(f, "WaveId({})", self.0)
429    }
430}
431
432define_u16_idx! {
433    /// Stable identifier for an encounter pull.
434    PullId, max = u16::MAX as usize
435}
436
437impl std::fmt::Debug for PullId {
438    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
439        write!(f, "PullId({})", self.0)
440    }
441}
442
443define_u32_idx! {
444    SnapshotIdx, debug_prefix = "SnapshotIdx({_0})"
445}
446
447impl SnapshotIdx {
448    pub const INVALID: SnapshotIdx = SnapshotIdx(0);
449
450    #[inline]
451    #[must_use]
452    pub const fn is_valid(self) -> bool {
453        self.0 != 0
454    }
455
456    #[inline]
457    #[must_use]
458    pub const fn next(self) -> Self {
459        Self(self.0.wrapping_add(1))
460    }
461}
462
463define_u8_idx! {
464    ResourceIdx, max = 32
465}
466
467impl ResourceIdx {
468    pub const MANA: ResourceIdx = ResourceIdx(0);
469    pub const RAGE: ResourceIdx = ResourceIdx(1);
470    pub const FOCUS: ResourceIdx = ResourceIdx(2);
471    pub const ENERGY: ResourceIdx = ResourceIdx(3);
472    pub const COMBO_POINTS: ResourceIdx = ResourceIdx(4);
473    pub const RUNES: ResourceIdx = ResourceIdx(5);
474    pub const RUNIC_POWER: ResourceIdx = ResourceIdx(6);
475    pub const SOUL_SHARDS: ResourceIdx = ResourceIdx(7);
476    pub const ASTRAL_POWER: ResourceIdx = ResourceIdx(8);
477    pub const HOLY_POWER: ResourceIdx = ResourceIdx(9);
478    pub const MAELSTROM: ResourceIdx = ResourceIdx(10);
479    pub const CHI: ResourceIdx = ResourceIdx(11);
480    pub const INSANITY: ResourceIdx = ResourceIdx(12);
481    pub const FURY: ResourceIdx = ResourceIdx(13);
482    pub const PAIN: ResourceIdx = ResourceIdx(14);
483    pub const ESSENCE: ResourceIdx = ResourceIdx(15);
484}
485
486#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
487#[serde(rename_all = "snake_case")]
488#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
489#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491// #t(rust_non_exhaustive_on_public) WoW aura targets are a fixed set (Player, Target, Pet)
492pub enum AuraOn {
493    #[default]
494    Player,
495    Target,
496    Pet,
497}
498
499impl std::fmt::Display for AuraOn {
500    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501        match self {
502            Self::Player => f.write_str("player"),
503            Self::Target => f.write_str("target"),
504            Self::Pet => f.write_str("pet"),
505        }
506    }
507}
508
509/// Collision-free identity for one aura instance.
510#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
511pub struct AuraKey {
512    aura: AuraIdx,
513    source: ActorId,
514    affected: ActorId,
515    on: AuraOn,
516}
517
518/// Dense-buffer identity for an APL aura category projection.
519#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
520pub struct AuraProjectionKey {
521    aura: AuraIdx,
522    on: AuraOn,
523}
524
525impl AuraProjectionKey {
526    #[inline]
527    #[must_use]
528    pub const fn new(aura: AuraIdx, on: AuraOn) -> Self {
529        Self { aura, on }
530    }
531
532    #[inline]
533    #[must_use]
534    pub const fn aura(self) -> AuraIdx {
535        self.aura
536    }
537
538    #[inline]
539    #[must_use]
540    pub const fn on(self) -> AuraOn {
541        self.on
542    }
543}
544
545impl AuraKey {
546    #[inline]
547    // #t(fn: rust_ctor_param_count) the four fields are the irreducible exact aura identity
548    #[must_use]
549    pub const fn new(aura: AuraIdx, source: ActorId, affected: ActorId, on: AuraOn) -> Self {
550        Self {
551            aura,
552            source,
553            affected,
554            on,
555        }
556    }
557
558    #[inline]
559    #[must_use]
560    pub const fn aura(self) -> AuraIdx {
561        self.aura
562    }
563
564    #[inline]
565    #[must_use]
566    pub const fn source(self) -> ActorId {
567        self.source
568    }
569
570    #[inline]
571    #[must_use]
572    pub const fn affected(self) -> ActorId {
573        self.affected
574    }
575
576    #[inline]
577    #[must_use]
578    pub const fn on(self) -> AuraOn {
579        self.on
580    }
581}
582
583#[cfg(test)]
584mod tests {
585    use googletest::prelude::*;
586    use rstest::rstest;
587
588    use super::*;
589
590    #[gtest]
591    #[rstest]
592    #[case::spell_at_u32_max(u32::MAX as usize, Some(SpellIdx(u32::MAX)))]
593    #[case::spell_over_u32_max(u32::MAX as usize + 1, None)]
594    fn spell_try_from_usize(
595        #[case] input: usize,
596        #[case] expected: Option<SpellIdx>,
597    ) -> Result<()> {
598        verify_that!(SpellIdx::try_from_usize(input), eq(expected))
599    }
600
601    #[gtest]
602    #[rstest]
603    #[case::unit_below_max(65534, Some(UnitIdx(65534)))]
604    #[case::unit_at_max_rejected(65535, None)]
605    fn unit_try_from_usize(#[case] input: usize, #[case] expected: Option<UnitIdx>) -> Result<()> {
606        verify_that!(UnitIdx::try_from_usize(input), eq(expected))
607    }
608
609    #[gtest]
610    #[rstest]
611    #[case::resource_below_max(31, Some(ResourceIdx(31)))]
612    #[case::resource_at_max_rejected(32, None)]
613    fn resource_try_from_usize(
614        #[case] input: usize,
615        #[case] expected: Option<ResourceIdx>,
616    ) -> Result<()> {
617        verify_that!(ResourceIdx::try_from_usize(input), eq(expected))
618    }
619
620    #[gtest]
621    fn from_usize_saturating_clamps() -> Result<()> {
622        verify_that!(UnitIdx::from_usize_saturating(10), eq(UnitIdx(10)))?;
623        verify_that!(
624            UnitIdx::from_usize_saturating(usize::MAX),
625            eq(UnitIdx(u16::MAX))
626        )?;
627        verify_that!(TargetIdx::from_usize_saturating(10), eq(TargetIdx(10)))?;
628
629        verify_that!(
630            TargetIdx::from_usize_saturating(usize::MAX),
631            eq(TargetIdx(u16::MAX))
632        )
633    }
634
635    #[gtest]
636    fn snapshot_next_wraps() -> Result<()> {
637        verify_that!(SnapshotIdx(1).next(), eq(SnapshotIdx(2)))?;
638
639        verify_that!(SnapshotIdx(u32::MAX).next(), eq(SnapshotIdx(0)))
640    }
641
642    #[gtest]
643    fn validity_and_role_predicates() -> Result<()> {
644        verify_that!(SpellIdx(0).is_valid(), eq(false))?;
645        verify_that!(SpellIdx(1).is_valid(), eq(true))?;
646        verify_that!(UnitIdx::PLAYER.is_player(), eq(true))?;
647        verify_that!(UnitIdx(1).is_player(), eq(false))?;
648        verify_that!(TargetIdx::PRIMARY.is_primary(), eq(true))?;
649        verify_that!(TargetIdx(1).is_primary(), eq(false))?;
650        verify_that!(PetIdx::PRIMARY.is_primary(), eq(true))?;
651        verify_that!(PetIdx(1).is_primary(), eq(false))?;
652        verify_that!(EnemyIdx::PRIMARY.is_primary(), eq(true))?;
653        verify_that!(EnemyIdx(1).is_primary(), eq(false))?;
654        verify_that!(SnapshotIdx::INVALID.is_valid(), eq(false))?;
655
656        verify_that!(SnapshotIdx(1).is_valid(), eq(true))
657    }
658
659    #[gtest]
660    fn pet_to_unit_idx_offset() -> Result<()> {
661        verify_that!(PetIdx(0).to_unit_idx(), eq(UnitIdx(1)))?;
662
663        verify_that!(PetIdx(3).to_unit_idx(), eq(UnitIdx(4)))
664    }
665
666    #[gtest]
667    fn guardian_slots_follow_the_reserved_primary_pet_identity() -> Result<()> {
668        verify_that!(PetIdx::from_guardian_slot(0), eq(Some(PetIdx(1))))?;
669        verify_that!(PetIdx::from_guardian_slot(254), eq(Some(PetIdx(255))))?;
670
671        verify_that!(PetIdx::from_guardian_slot(255), eq(None))
672    }
673
674    #[gtest]
675    fn enemy_to_target_idx_identity() -> Result<()> {
676        verify_that!(EnemyIdx(2).to_target_idx(), eq(TargetIdx(2)))?;
677
678        verify_that!(EnemyIdx(0).to_target_idx(), eq(TargetIdx(0)))
679    }
680
681    #[gtest]
682    #[rstest]
683    #[case::unit_player(format!("{:?}", UnitIdx(0)), "UnitIdx(PLAYER)")]
684    #[case::unit_other(format!("{:?}", UnitIdx(2)), "UnitIdx(2)")]
685    #[case::target_primary(format!("{:?}", TargetIdx(0)), "TargetIdx(PRIMARY)")]
686    #[case::target_other(format!("{:?}", TargetIdx(1)), "TargetIdx(1)")]
687    #[case::pet_primary(format!("{:?}", PetIdx(0)), "PetIdx(PRIMARY)")]
688    #[case::pet_other(format!("{:?}", PetIdx(5)), "PetIdx(5)")]
689    #[case::enemy_primary(format!("{:?}", EnemyIdx(0)), "EnemyIdx(PRIMARY)")]
690    #[case::enemy_other(format!("{:?}", EnemyIdx(3)), "EnemyIdx(3)")]
691    fn custom_debug_strings(#[case] actual: String, #[case] expected: &str) -> Result<()> {
692        verify_that!(actual, eq(expected))
693    }
694
695    #[gtest]
696    #[rstest]
697    #[case::player(AuraOn::Player, "player")]
698    #[case::target(AuraOn::Target, "target")]
699    #[case::pet(AuraOn::Pet, "pet")]
700    fn aura_on_display(#[case] on: AuraOn, #[case] expected: &str) -> Result<()> {
701        verify_that!(format!("{on}"), eq(expected))
702    }
703
704    #[gtest]
705    #[rstest]
706    #[case::player(AuraOn::Player)]
707    #[case::target(AuraOn::Target)]
708    #[case::pet(AuraOn::Pet)]
709    fn aura_key_roundtrip(#[case] on: AuraOn) -> Result<()> {
710        let k = AuraProjectionKey::new(AuraIdx(4242), on);
711
712        verify_that!(k.aura(), eq(AuraIdx(4242)))?;
713
714        verify_that!(k.on(), eq(on))
715    }
716
717    #[gtest]
718    #[rstest]
719    #[case::aura_zero(AuraIdx(0), AuraOn::Player)]
720    #[case::aura_max(AuraIdx(u32::MAX), AuraOn::Pet)]
721    fn aura_key_bit_isolation(#[case] aura: AuraIdx, #[case] on: AuraOn) -> Result<()> {
722        let k = AuraProjectionKey::new(aura, on);
723
724        verify_that!(k.aura(), eq(aura))?;
725
726        verify_that!(k.on(), eq(on))
727    }
728
729    #[gtest]
730    fn category_projection_is_not_an_exact_primary_target_key() -> Result<()> {
731        let aura = AuraIdx(4242);
732        let projection = AuraProjectionKey::new(aura, AuraOn::Target);
733        let exact = AuraKey::new(
734            aura,
735            ActorId::Player,
736            ActorId::Enemy(EnemyIdx::PRIMARY),
737            AuraOn::Target,
738        );
739
740        verify_that!(projection.aura(), eq(exact.aura()))?;
741
742        verify_that!(projection.on(), eq(exact.on()))
743    }
744}