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#[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
36pub 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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))]
491pub 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#[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#[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 #[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}