1use serde::{Deserialize, Serialize};
4#[cfg(feature = "wasm")]
5use tsify::Tsify;
6
7pub const SLOT_ALIGNMENT: usize = align_of::<f64>();
9
10pub mod field_offset {
12 pub mod cooldown {
14 pub const READY_AT: usize = 0;
16 pub const CURRENT_CHARGES: usize = 16;
18 pub const MAX_CHARGES: usize = 20;
20 pub const NEXT_CHARGE_AT: usize = 24;
22 pub const RECHARGE_TIME: usize = 32;
24 }
25
26 pub mod aura {
28 pub const EXPIRES_AT: usize = 0;
30 pub const BASE_DURATION: usize = 8;
32 }
33
34 pub mod resource {
36 pub const CURRENT: usize = 0;
38 pub const MAX: usize = 8;
40 pub const REGEN_PER_SEC: usize = 16;
42 }
43
44 pub mod unit {
46 pub const HEALTH: usize = 0;
48 pub const MAX_HEALTH: usize = 8;
50 }
51}
52
53pub const PANDEMIC_REFRESH_FRACTION: f64 = 0.3;
55
56#[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#[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#[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#[non_exhaustive]
86pub enum EvalKind {
87 Direct,
89 TimestampReady,
91 TimestampRemaining,
93 TimestampActive,
95 TimestampElapsed,
97 TimestampInactive,
99 CooldownReady,
101 CooldownFullRecharge,
103 AuraRefreshable,
105 PositiveFloat,
107 ResourceDeficit,
109 ResourcePct,
111 ResourceDeficitPct,
113 ResourceTimeToMax,
115 UnitHealthPct,
117 UnitHealthDeficit,
119 SpellUsable,
121}
122impl EvalKind {
125 #[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#[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#[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
176pub 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 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
238pub const KIND_SHIFT: u32 = 56;
240
241pub const ACTION_ID_SHIFT: u32 = 32;
243
244pub 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#[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#[inline]
288#[must_use]
289pub fn decode_eval_result(packed: u64) -> (u8, u32, u32) {
290 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#[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}