Skip to main content

wowlab_buffer_contract/
lib.rs

1//! Stable metadata and memory-layout contracts for the rotation engine.
2
3use serde::{Deserialize, Serialize};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6
7/// Required slot alignment, in bytes; `assert_repr_c_layout` rejects any field with a stricter alignment.
8pub const SLOT_ALIGNMENT: usize = align_of::<f64>();
9
10/// Pinned byte offsets for fields read by multi-field [`EvalKind`] variants.
11pub mod field_offset {
12    /// Offsets into the keyed cooldown slot.
13    pub mod cooldown {
14        /// Offset of the next ready timestamp, in seconds.
15        pub const READY_AT: usize = 0;
16        /// Offset of the current integer charge count.
17        pub const CURRENT_CHARGES: usize = 16;
18        /// Offset of the maximum integer charge count.
19        pub const MAX_CHARGES: usize = 20;
20        /// Offset of the next charge timestamp, in seconds.
21        pub const NEXT_CHARGE_AT: usize = 24;
22        /// Offset of the recharge duration, in seconds.
23        pub const RECHARGE_TIME: usize = 32;
24    }
25
26    /// Offsets into the keyed aura slot.
27    pub mod aura {
28        /// Offset of the expiration timestamp, in seconds.
29        pub const EXPIRES_AT: usize = 0;
30        /// Offset of the aura's base duration, in seconds.
31        pub const BASE_DURATION: usize = 8;
32    }
33
34    /// Offsets into the keyed resource slot.
35    pub mod resource {
36        /// Offset of the current resource amount.
37        pub const CURRENT: usize = 0;
38        /// Offset of the maximum resource amount.
39        pub const MAX: usize = 8;
40        /// Offset of passive resource regeneration per second.
41        pub const REGEN_PER_SEC: usize = 16;
42    }
43
44    /// Offsets into the keyed unit slot.
45    pub mod unit {
46        /// Offset of the unit's current health.
47        pub const HEALTH: usize = 0;
48        /// Offset of the unit's maximum health.
49        pub const MAX_HEALTH: usize = 8;
50    }
51}
52
53/// Pandemic refresh fraction; an aura is refreshable once `remaining < fraction * base_duration` (`WoW` 30% window).
54pub const PANDEMIC_REFRESH_FRACTION: f64 = 0.3;
55
56/// Primitive value representation stored in a buffer field.
57#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
58#[serde(rename_all = "snake_case")]
59#[cfg_attr(feature = "wasm", derive(Tsify))]
60#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
61#[non_exhaustive]
62pub enum FieldType {
63    Bool,
64    Int,
65    Float,
66}
67
68/// Whether a slot is a singleton or keyed by spell/aura/resource.
69#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
70#[serde(rename_all = "snake_case")]
71#[cfg_attr(feature = "wasm", derive(Tsify))]
72#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
73#[non_exhaustive]
74pub enum SlotKind {
75    Singleton,
76    Keyed,
77}
78
79/// Operation used to interpret a field from the dense rotation buffer.
80#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
81#[serde(rename_all = "snake_case")]
82#[cfg_attr(feature = "wasm", derive(Tsify))]
83#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
84// docref:start rotation-compiler-eval-kind
85#[non_exhaustive]
86pub enum EvalKind {
87    /// Return the field value without derived evaluation.
88    Direct,
89    /// Return whether a timestamp is at or before the current time.
90    TimestampReady,
91    /// Return the non-negative time remaining until a timestamp.
92    TimestampRemaining,
93    /// Return whether an occupied timestamp has not yet elapsed.
94    TimestampActive,
95    /// Return the non-negative time elapsed since a timestamp.
96    TimestampElapsed,
97    /// Return whether a timestamp is unoccupied or elapsed.
98    TimestampInactive,
99    /// Return whether a cooldown is ready, accounting for charges.
100    CooldownReady,
101    /// Time to full charges: single-charge equals time remaining; multi-charge adds one recharge per still-missing charge.
102    CooldownFullRecharge,
103    /// Refreshable iff `remaining < PANDEMIC_REFRESH_FRACTION * base_duration`.
104    AuraRefreshable,
105    /// Return whether a floating-point value is positive.
106    PositiveFloat,
107    /// Return the difference between the maximum and current resource.
108    ResourceDeficit,
109    /// Return current resource as a percentage of its maximum.
110    ResourcePct,
111    /// Return missing resource as a percentage of its maximum.
112    ResourceDeficitPct,
113    /// Return the time required to regenerate to maximum resource.
114    ResourceTimeToMax,
115    /// Return current health as a percentage of maximum health.
116    UnitHealthPct,
117    /// Return the difference between maximum and current health.
118    UnitHealthDeficit,
119    /// Cross-slot spell usability; extra offsets resolved at JIT compile time via a side-table.
120    SpellUsable,
121}
122// docref:end rotation-compiler-eval-kind
123
124impl EvalKind {
125    /// Returns the derived result type, or `None` when the field type is unchanged.
126    #[must_use]
127    pub const fn result_field_type(self) -> Option<FieldType> {
128        match self {
129            Self::Direct => None,
130            Self::TimestampReady
131            | Self::TimestampActive
132            | Self::TimestampInactive
133            | Self::CooldownReady
134            | Self::AuraRefreshable
135            | Self::PositiveFloat
136            | Self::SpellUsable => Some(FieldType::Bool),
137            Self::TimestampRemaining
138            | Self::TimestampElapsed
139            | Self::CooldownFullRecharge
140            | Self::ResourceDeficit
141            | Self::ResourcePct
142            | Self::ResourceDeficitPct
143            | Self::ResourceTimeToMax
144            | Self::UnitHealthPct
145            | Self::UnitHealthDeficit => Some(FieldType::Float),
146        }
147    }
148}
149
150/// Inventory entry describing one dense-buffer slot domain.
151#[derive(Debug)]
152pub struct SlotDescriptor {
153    pub name: &'static str,
154    pub size: usize,
155    pub kind: SlotKind,
156}
157
158inventory::collect!(SlotDescriptor);
159
160/// Inventory entry describing one expression-readable buffer field.
161#[derive(Debug)]
162pub struct FieldDescriptor {
163    pub domain: &'static str,
164    pub name: &'static str,
165    pub field_type: FieldType,
166    pub eval_kind: EvalKind,
167    pub field_offset: usize,
168    pub slot_size: usize,
169    pub slot_kind: SlotKind,
170    pub description: &'static str,
171    pub key_domain: Option<&'static str>,
172}
173
174inventory::collect!(FieldDescriptor);
175
176/// Asserts that a `repr(C)` layout matches declared `(size, alignment)` pairs.
177///
178/// # Panics
179///
180/// Panics when the actual layout differs from the declared C ABI layout.
181pub const fn assert_repr_c_layout(
182    actual_offsets: &[usize],
183    actual_size: usize,
184    fields: &[(usize, usize)],
185) {
186    assert!(actual_offsets.len() == fields.len(), "field count mismatch");
187
188    let mut offset: usize = 0;
189    let mut max_align: usize = 1;
190    let mut i = 0;
191
192    // #t(block: rust_unchecked_indexing) `i` bounded by `fields.len()` via while condition; both slices asserted same length above
193    while i < fields.len() {
194        let (f_size, f_align) = fields[i];
195
196        assert!(
197            f_align.is_power_of_two(),
198            "field alignment must be a non-zero power of two"
199        );
200
201        let rem = offset % f_align;
202
203        if rem != 0 {
204            offset += f_align - rem;
205        }
206
207        assert!(
208            actual_offsets[i] == offset,
209            "repr(C) offset mismatch — struct layout has diverged from declared fields"
210        );
211
212        offset += f_size;
213
214        if f_align > max_align {
215            max_align = f_align;
216        }
217
218        i += 1;
219    }
220
221    let rem = offset % max_align;
222
223    if rem != 0 {
224        offset += max_align - rem;
225    }
226
227    assert!(
228        actual_size == offset,
229        "repr(C) size mismatch — struct may have trailing fields not listed in define_slot!"
230    );
231
232    assert!(
233        max_align <= SLOT_ALIGNMENT,
234        "slot alignment exceeds SLOT_ALIGNMENT — DenseBuffer storage cannot satisfy it"
235    );
236}
237
238/// Shift for the high 8 `kind` bits in the packed `[kind:8][action_id:24][payload:32]` layout.
239pub const KIND_SHIFT: u32 = 56;
240
241/// Shift for the 24-bit action ID at bits 32..56.
242pub const ACTION_ID_SHIFT: u32 = 32;
243
244/// 24-bit mask for the action ID field.
245pub const ACTION_ID_MASK: u64 = 0x00FF_FFFF;
246
247const PACKED_BITS: u32 = u64::BITS;
248const KIND_BITS: u32 = 8;
249const ACTION_ID_BITS: u32 = 24;
250const PAYLOAD_BITS: u32 = u32::BITS;
251
252const _: () = assert!(
253    KIND_SHIFT + KIND_BITS == PACKED_BITS,
254    "kind field must occupy the top 8 bits of the u64"
255);
256const _: () = assert!(
257    ACTION_ID_SHIFT + ACTION_ID_BITS == KIND_SHIFT,
258    "action_id field must sit directly below the kind field with no gap or overlap"
259);
260const _: () = assert!(
261    ACTION_ID_MASK == (1u64 << ACTION_ID_BITS) - 1,
262    "ACTION_ID_MASK must be exactly ACTION_ID_BITS wide"
263);
264const _: () = assert!(
265    ACTION_ID_SHIFT == PAYLOAD_BITS,
266    "payload occupies the low 32 bits, so action_id must start where payload ends"
267);
268const _: () = assert!(
269    KIND_BITS + ACTION_ID_BITS + PAYLOAD_BITS == PACKED_BITS,
270    "packed fields must tile the u64 exactly with no overlap"
271);
272
273/// Packs an evaluation result into `[kind:8][action_id:24][payload:32]`.
274///
275/// The action kind defines whether the payload stores an empower rank or raw `f32` bits.
276#[inline]
277#[must_use]
278pub fn pack_eval_result(kind: u8, action_id: u32, payload: u32) -> u64 {
279    let kind_bits = u64::from(kind) << KIND_SHIFT;
280    let action_bits = (u64::from(action_id) & ACTION_ID_MASK) << ACTION_ID_SHIFT;
281
282    kind_bits | action_bits | u64::from(payload)
283}
284
285/// Decodes `[kind:8][action_id:24][payload:32]` into its components.
286// docref:start rotation-compiler-decode
287#[inline]
288#[must_use]
289pub fn decode_eval_result(packed: u64) -> (u8, u32, u32) {
290    // #t(block: rust_lossy_cast, rust_magic_numbers) packed u64 bit extraction.
291    let kind = (packed >> KIND_SHIFT) as u8;
292    let action_id = ((packed >> ACTION_ID_SHIFT) & ACTION_ID_MASK) as u32;
293    let bytes = packed.to_le_bytes();
294    let payload = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
295    (kind, action_id, payload)
296}
297// docref:end rotation-compiler-decode
298
299#[cfg(test)]
300mod tests {
301    use googletest::prelude::*;
302
303    use super::*;
304
305    fn verify_layout_panic(
306        actual_offsets: &[usize],
307        actual_size: usize,
308        fields: &[(usize, usize)],
309        expected: &str,
310    ) -> Result<()> {
311        let panic = std::panic::catch_unwind(|| {
312            assert_repr_c_layout(actual_offsets, actual_size, fields);
313        })
314        .err()
315        .or_fail()?;
316        let message = panic
317            .downcast_ref::<&str>()
318            .copied()
319            .or_else(|| panic.downcast_ref::<String>().map(String::as_str))
320            .or_fail()?;
321
322        verify_that!(message, contains_substring(expected))
323    }
324
325    #[gtest]
326    fn pack_decode_preserves_component_bits() -> Result<()> {
327        let cases: &[(u8, u32, u32)] = &[
328            (0, 0, 0.0_f32.to_bits()),
329            (1, 12_345, 1.5_f32.to_bits()),
330            (2, 0, (-0.0_f32).to_bits()),
331            (3, 0, f32::INFINITY.to_bits()),
332            (4, 0xFF_FFFF, 0x7FC0_1234),
333        ];
334
335        for &(kind, action_id, payload) in cases {
336            let packed = pack_eval_result(kind, action_id, payload);
337            let (decoded_kind, decoded_action_id, decoded_payload) = decode_eval_result(packed);
338
339            verify_that!(decoded_kind, eq(kind))?;
340            verify_that!(decoded_action_id, eq(action_id))?;
341            verify_that!(decoded_payload, eq(payload))?;
342        }
343
344        Ok(())
345    }
346
347    #[gtest]
348    fn packed_layout_has_exact_bit_positions() -> Result<()> {
349        let packed = pack_eval_result(0xAB, 0xCD_EF01, 1.0_f32.to_bits());
350
351        verify_that!(packed, eq(0xABCD_EF01_3F80_0000))
352    }
353
354    #[gtest]
355    fn pack_truncates_action_id_to_24_bits() -> Result<()> {
356        let packed = pack_eval_result(7, 0xABCD_EF01, 0.0_f32.to_bits());
357        let (kind, action_id, payload) = decode_eval_result(packed);
358
359        verify_that!(kind, eq(7))?;
360        verify_that!(action_id, eq(0xCD_EF01))?;
361
362        verify_that!(payload, eq(0.0_f32.to_bits()))
363    }
364
365    #[gtest]
366    fn result_field_type_matches_evaluation_semantics() -> Result<()> {
367        verify_that!(EvalKind::Direct.result_field_type(), none())?;
368
369        for eval_kind in [
370            EvalKind::TimestampReady,
371            EvalKind::TimestampActive,
372            EvalKind::TimestampInactive,
373            EvalKind::CooldownReady,
374            EvalKind::AuraRefreshable,
375            EvalKind::PositiveFloat,
376            EvalKind::SpellUsable,
377        ] {
378            verify_that!(eval_kind.result_field_type(), some(eq(FieldType::Bool)))?;
379        }
380
381        for eval_kind in [
382            EvalKind::TimestampRemaining,
383            EvalKind::TimestampElapsed,
384            EvalKind::CooldownFullRecharge,
385            EvalKind::ResourceDeficit,
386            EvalKind::ResourcePct,
387            EvalKind::ResourceDeficitPct,
388            EvalKind::ResourceTimeToMax,
389            EvalKind::UnitHealthPct,
390            EvalKind::UnitHealthDeficit,
391        ] {
392            verify_that!(eval_kind.result_field_type(), some(eq(FieldType::Float)))?;
393        }
394
395        Ok(())
396    }
397
398    #[gtest]
399    fn repr_c_layout_accepts_natural_padding_and_trailing_padding() {
400        assert_repr_c_layout(&[0, 8, 16], 24, &[(4, 4), (8, 8), (4, 4)]);
401    }
402
403    #[gtest]
404    fn repr_c_layout_rejects_field_count_mismatch() -> Result<()> {
405        verify_layout_panic(&[0], 8, &[(8, 8), (8, 8)], "field count mismatch")
406    }
407
408    #[gtest]
409    fn repr_c_layout_rejects_invalid_alignment() -> Result<()> {
410        verify_layout_panic(
411            &[0],
412            8,
413            &[(8, 0)],
414            "field alignment must be a non-zero power of two",
415        )
416    }
417
418    #[gtest]
419    fn repr_c_layout_rejects_offset_drift() -> Result<()> {
420        verify_layout_panic(&[0, 4], 16, &[(4, 4), (8, 8)], "repr(C) offset mismatch")
421    }
422
423    #[gtest]
424    fn repr_c_layout_rejects_size_drift() -> Result<()> {
425        verify_layout_panic(&[0, 8], 12, &[(4, 4), (8, 8)], "repr(C) size mismatch")
426    }
427
428    #[gtest]
429    fn repr_c_layout_rejects_over_aligned_fields() -> Result<()> {
430        verify_layout_panic(
431            &[0],
432            16,
433            &[(16, 16)],
434            "slot alignment exceeds SLOT_ALIGNMENT",
435        )
436    }
437}