Skip to main content

wowlab_engine_domain/rotation/
result.rs

1//! Rotation evaluation result type shared by JIT and interpreter backends.
2
3use static_assertions::const_assert_eq;
4use wowlab_types::{game::GearSlot, sim::SpellIdx};
5
6/// Decoded form of the JIT's packed-`u64` return value.
7// docref:start rotation-compiler-eval-result
8#[derive(Clone, Copy, Debug, PartialEq)]
9#[repr(C)]
10pub struct EvalResult {
11    pub kind: u8,
12    /// One-based empower rank for cast and item-use actions; zero when omitted.
13    pub empower_rank: u8,
14    /// Spell ID for `KIND_CAST`, or gear-slot repr for `KIND_USE_ITEM`.
15    pub spell_id: u32,
16    /// Wait seconds for `KIND_WAIT`, pool target for `KIND_POOL`.
17    pub wait_time: f32,
18}
19// docref:end rotation-compiler-eval-result
20
21pub(super) const KIND_NONE: u8 = 0;
22pub(super) const KIND_CAST: u8 = 1;
23pub(super) const KIND_WAIT: u8 = 2;
24pub(super) const KIND_POOL: u8 = 3;
25pub(super) const KIND_USE_ITEM: u8 = 4;
26
27const_assert_eq!(size_of::<EvalResult>(), 12);
28
29impl EvalResult {
30    pub const NONE: Self = Self {
31        kind: KIND_NONE,
32        empower_rank: 0,
33        spell_id: 0,
34        wait_time: 0.0,
35    };
36
37    #[must_use]
38    pub fn cast(spell: SpellIdx, empower_rank: u8) -> Self {
39        Self {
40            kind: KIND_CAST,
41            empower_rank,
42            spell_id: spell.0,
43            wait_time: 0.0,
44        }
45    }
46
47    #[must_use]
48    pub fn wait(seconds: f32) -> Self {
49        Self {
50            kind: KIND_WAIT,
51            empower_rank: 0,
52            spell_id: 0,
53            wait_time: seconds,
54        }
55    }
56
57    #[must_use]
58    pub fn pool(target: f32) -> Self {
59        Self {
60            kind: KIND_POOL,
61            empower_rank: 0,
62            spell_id: 0,
63            wait_time: target,
64        }
65    }
66
67    #[must_use]
68    pub fn use_item(slot: GearSlot, empower_rank: u8) -> Self {
69        // #t(rust_lossy_cast) GearSlot is repr(u8); the discriminant cast is exact, not lossy
70        Self::use_item_raw(slot as u8, empower_rank)
71    }
72
73    /// Use-item result from a raw [`GearSlot`] repr (the form the lowerer threads through).
74    #[must_use]
75    pub fn use_item_raw(slot_repr: u8, empower_rank: u8) -> Self {
76        Self {
77            kind: KIND_USE_ITEM,
78            empower_rank,
79            spell_id: u32::from(slot_repr),
80            wait_time: 0.0,
81        }
82    }
83
84    #[must_use]
85    pub fn is_none(&self) -> bool {
86        self.kind == KIND_NONE
87    }
88
89    #[must_use]
90    pub fn is_cast(&self) -> bool {
91        self.kind == KIND_CAST
92    }
93
94    #[must_use]
95    pub fn is_wait(&self) -> bool {
96        self.kind == KIND_WAIT
97    }
98
99    #[must_use]
100    pub fn is_pool(&self) -> bool {
101        self.kind == KIND_POOL
102    }
103
104    #[must_use]
105    pub fn is_use_item(&self) -> bool {
106        self.kind == KIND_USE_ITEM
107    }
108
109    #[must_use]
110    pub fn item_slot(&self) -> Option<GearSlot> {
111        u8::try_from(self.spell_id)
112            .ok()
113            .and_then(|slot| GearSlot::try_from(slot).ok())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use googletest::prelude::*;
120
121    use super::*;
122
123    #[gtest]
124    fn use_item_creates_correct_result() {
125        let result = EvalResult::use_item(GearSlot::Trinket1, 3);
126
127        expect_that!(
128            result,
129            matches_pattern!(EvalResult {
130                kind: eq(KIND_USE_ITEM),
131                empower_rank: eq(3),
132                spell_id: eq(GearSlot::Trinket1 as u32),
133                wait_time: near(0.0, f32::EPSILON),
134            })
135        );
136    }
137
138    #[gtest]
139    fn is_use_item_true_for_item_results() {
140        let item = EvalResult::use_item(GearSlot::Trinket2, 0);
141
142        expect_that!(item.is_use_item(), is_true());
143    }
144
145    #[gtest]
146    fn is_use_item_false_for_other_kinds() {
147        let cast = EvalResult::cast(SpellIdx(100), 0);
148
149        expect_that!(!cast.is_use_item(), is_true());
150
151        let wait = EvalResult::wait(1.0);
152
153        expect_that!(!wait.is_use_item(), is_true());
154
155        let pool = EvalResult::pool(50.0);
156
157        expect_that!(!pool.is_use_item(), is_true());
158
159        expect_that!(!EvalResult::NONE.is_use_item(), is_true());
160    }
161
162    #[gtest]
163    fn item_slot_returns_correct_gear_slot() {
164        let result = EvalResult::use_item(GearSlot::Trinket1, 0);
165
166        expect_that!(result.item_slot(), eq(Some(GearSlot::Trinket1)));
167
168        let result2 = EvalResult::use_item(GearSlot::Trinket2, 0);
169
170        expect_that!(result2.item_slot(), eq(Some(GearSlot::Trinket2)));
171    }
172
173    #[gtest]
174    fn item_slot_returns_none_for_invalid_value() {
175        let result = EvalResult {
176            kind: KIND_USE_ITEM,
177            empower_rank: 0,
178            spell_id: 255,
179            wait_time: 0.0,
180        };
181
182        expect_that!(result.item_slot(), eq(None));
183    }
184}