Skip to main content

wowlab_engine_domain/rotation/
reference_backend.rs

1// #t(file: rust_floating_point_eq) `float_to_bool` is the documented exact-zero check that mirrors the trait coercion contract.
2// #t(file: rust_bool_params) `and_b`/`or_b`/`select_b` are trait primitives whose Bool params are unavoidable.
3
4//! Structural reference side of the [`RotationBackend`] lowering trait.
5
6use std::marker::PhantomData;
7
8use wowlab_types::{
9    constants::MS_PER_SECOND,
10    numeric::{float_eq, float_ne, safe_div, true_mod},
11    sim::{Condition, Rotation, RotationAction, SpellIdx},
12};
13
14use super::{
15    backend::RotationBackend,
16    buffer::{DenseBuffer, DescriptorTable},
17    context::ContextSchema,
18    decision_trace::{
19        ActionEvaluation, ActionOutcome, ActionRecord, Decision, DecisionTraceSink,
20        DecisionTraceTarget, EvaluationStatus,
21    },
22    resolver::SpecResolver,
23    result::EvalResult,
24    trace::{TraceContext, condition_label_with_values},
25};
26
27#[derive(Clone, Copy)]
28pub(crate) struct ReferenceRecorderCtx<'a> {
29    pub(crate) rotation: &'a Rotation,
30    pub(crate) schema: &'a ContextSchema,
31    pub(crate) resolver: &'a SpecResolver,
32    pub(crate) table: &'a DescriptorTable,
33}
34
35pub(crate) struct ReferenceFrame<'a> {
36    pub(crate) buffer: &'a mut DenseBuffer,
37    pub(crate) now: f64,
38}
39
40struct TraceState<'a> {
41    sink: DecisionTraceTarget,
42    ctx: ReferenceRecorderCtx<'a>,
43    buffer: *mut DenseBuffer,
44    stack: Vec<Decision>,
45    time_ms: u32,
46}
47
48pub(crate) struct ReferenceBackend<'a> {
49    buf: *mut u8,
50    len: usize,
51    now: f64,
52    result: Option<EvalResult>,
53    trace: Option<TraceState<'a>>,
54    _phantom: PhantomData<&'a mut DenseBuffer>,
55}
56
57impl<'a> ReferenceBackend<'a> {
58    pub(crate) fn new(frame: ReferenceFrame<'a>) -> Self {
59        let ReferenceFrame { buffer: buf, now } = frame;
60        let len = buf.bytes().len();
61
62        Self {
63            buf: buf.as_mut_ptr(),
64            len,
65            now,
66            result: None,
67            trace: None,
68            _phantom: PhantomData,
69        }
70    }
71
72    pub(crate) fn with_sink(
73        frame: ReferenceFrame<'a>,
74        sink: DecisionTraceTarget,
75        ctx: ReferenceRecorderCtx<'a>,
76    ) -> Self {
77        let ReferenceFrame { buffer: buf, now } = frame;
78        let time_ms = wowlab_types::numeric::f64_to_u32_saturating_round(now * MS_PER_SECOND);
79        let len = buf.bytes().len();
80        let buffer_ptr: *mut DenseBuffer = buf;
81        let data_ptr = buf.as_mut_ptr();
82
83        Self {
84            buf: data_ptr,
85            len,
86            now,
87            result: None,
88            trace: Some(TraceState {
89                sink,
90                ctx,
91                buffer: buffer_ptr,
92                stack: Vec::new(),
93                time_ms,
94            }),
95            _phantom: PhantomData,
96        }
97    }
98
99    pub(crate) fn finish(self) -> EvalResult {
100        if let Some(state) = self.trace {
101            for d in state.stack.into_iter().rev() {
102                state.sink.record(d);
103            }
104        }
105
106        self.result.unwrap_or(EvalResult::NONE)
107    }
108
109    #[inline]
110    fn bytes(&self) -> &[u8] {
111        // SAFETY: exclusive `&mut DenseBuffer` borrow via `PhantomData`; buffer not resized, reads never aliased with stores.
112        unsafe { std::slice::from_raw_parts(self.buf, self.len) }
113    }
114
115    #[inline]
116    fn capture(&mut self, r: EvalResult) {
117        if self.result.is_none() {
118            self.result = Some(r);
119        }
120    }
121}
122
123impl TraceState<'_> {
124    fn rejection_reason(&self, now_secs: f64, failing: &Condition) -> Option<String> {
125        // SAFETY: ReferenceBackend holds the exclusive borrow via PhantomData; recorder runs between lowerer buffer ops, so no concurrent mutation.
126        let buffer = unsafe { &mut *self.buffer };
127        let mut tc = TraceContext {
128            rotation: self.ctx.rotation,
129            schema: self.ctx.schema,
130            resolver: self.ctx.resolver,
131            table: self.ctx.table,
132            buffer,
133            now_secs,
134        };
135        let label = condition_label_with_values(failing, &mut tc);
136
137        if label.is_empty() { None } else { Some(label) }
138    }
139}
140
141impl RotationBackend for ReferenceBackend<'_> {
142    type Bool = bool;
143    type Int = i64;
144    type Float = f64;
145    type AndChain = bool;
146    type OrChain = bool;
147    #[cfg(feature = "rotation-optimizations")]
148    type CastGuard = bool;
149
150    #[inline]
151    fn const_bool(&mut self, v: bool) -> bool {
152        v
153    }
154    #[inline]
155    fn const_i32(&mut self, v: i32) -> i64 {
156        i64::from(v)
157    }
158    #[inline]
159    fn const_i64(&mut self, v: i64) -> i64 {
160        v
161    }
162    #[inline]
163    fn const_f64(&mut self, v: f64) -> f64 {
164        v
165    }
166
167    #[inline]
168    fn load_bool(&mut self, offset: usize) -> bool {
169        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len; bool fields are stored as i32
170        bytemuck::pod_read_unaligned::<i32>(&self.bytes()[offset..offset + size_of::<i32>()]) != 0
171    }
172    #[inline]
173    fn load_i32(&mut self, offset: usize) -> i64 {
174        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len
175        i64::from(bytemuck::pod_read_unaligned::<i32>(
176            &self.bytes()[offset..offset + size_of::<i32>()],
177        ))
178    }
179    #[inline]
180    fn load_f64(&mut self, offset: usize) -> f64 {
181        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len
182        bytemuck::pod_read_unaligned::<f64>(&self.bytes()[offset..offset + size_of::<f64>()])
183    }
184
185    #[inline]
186    fn store_bool(&mut self, offset: usize, v: bool) {
187        if self.result.is_some() {
188            return;
189        }
190
191        let bytes = i32::from(v).to_ne_bytes();
192        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
193
194        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
195            .copy_from_slice(&bytes);
196    }
197    #[inline]
198    fn store_i32(&mut self, offset: usize, v: i64) {
199        if self.result.is_some() {
200            return;
201        }
202
203        let bytes = crate::numeric::wrapping_i64_to_i32(v).to_ne_bytes();
204        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
205
206        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
207            .copy_from_slice(&bytes);
208    }
209    #[inline]
210    fn store_f64(&mut self, offset: usize, v: f64) {
211        if self.result.is_some() {
212            return;
213        }
214
215        let bytes = v.to_ne_bytes();
216        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
217
218        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
219            .copy_from_slice(&bytes);
220    }
221
222    #[inline]
223    fn now(&mut self) -> f64 {
224        self.now
225    }
226
227    #[inline]
228    fn add_f(&mut self, a: f64, b: f64) -> f64 {
229        a + b
230    }
231    #[inline]
232    fn sub_f(&mut self, a: f64, b: f64) -> f64 {
233        a - b
234    }
235    #[inline]
236    fn mul_f(&mut self, a: f64, b: f64) -> f64 {
237        a * b
238    }
239    #[inline]
240    fn safe_div_f(&mut self, a: f64, b: f64) -> f64 {
241        safe_div(a, b)
242    }
243    #[inline]
244    fn true_mod_f(&mut self, a: f64, b: f64) -> f64 {
245        true_mod(a, b)
246    }
247    #[inline]
248    fn min_f(&mut self, a: f64, b: f64) -> f64 {
249        a.min(b)
250    }
251    #[inline]
252    fn max_f(&mut self, a: f64, b: f64) -> f64 {
253        a.max(b)
254    }
255    #[inline]
256    fn floor_f(&mut self, v: f64) -> f64 {
257        v.floor()
258    }
259    #[inline]
260    fn ceil_f(&mut self, v: f64) -> f64 {
261        v.ceil()
262    }
263    #[inline]
264    fn abs_f(&mut self, v: f64) -> f64 {
265        v.abs()
266    }
267
268    #[inline]
269    fn cmp_gt_f(&mut self, a: f64, b: f64) -> bool {
270        a > b
271    }
272    #[inline]
273    fn cmp_gte_f(&mut self, a: f64, b: f64) -> bool {
274        a >= b
275    }
276    #[inline]
277    fn cmp_lt_f(&mut self, a: f64, b: f64) -> bool {
278        a < b
279    }
280    #[inline]
281    fn cmp_lte_f(&mut self, a: f64, b: f64) -> bool {
282        a <= b
283    }
284    #[inline]
285    fn cmp_eq_f(&mut self, a: f64, b: f64) -> bool {
286        float_eq(a, b)
287    }
288    #[inline]
289    fn cmp_ne_f(&mut self, a: f64, b: f64) -> bool {
290        float_ne(a, b)
291    }
292    #[inline]
293    fn cmp_eq_i(&mut self, a: i64, b: i64) -> bool {
294        a == b
295    }
296    #[inline]
297    fn cmp_ne_i(&mut self, a: i64, b: i64) -> bool {
298        a != b
299    }
300    #[inline]
301    fn cmp_gt_i(&mut self, a: i64, b: i64) -> bool {
302        a > b
303    }
304    #[inline]
305    fn cmp_gte_i(&mut self, a: i64, b: i64) -> bool {
306        a >= b
307    }
308    #[inline]
309    fn cmp_lt_i(&mut self, a: i64, b: i64) -> bool {
310        a < b
311    }
312    #[inline]
313    fn cmp_lte_i(&mut self, a: i64, b: i64) -> bool {
314        a <= b
315    }
316
317    #[inline]
318    fn and_b(&mut self, a: bool, b: bool) -> bool {
319        a && b
320    }
321    #[inline]
322    fn or_b(&mut self, a: bool, b: bool) -> bool {
323        a || b
324    }
325    #[inline]
326    fn not_b(&mut self, a: bool) -> bool {
327        !a
328    }
329
330    #[inline]
331    fn begin_and(&mut self, first: bool) -> Self::AndChain {
332        first
333    }
334
335    #[inline]
336    fn push_and(&mut self, chain: &mut Self::AndChain, value: bool) {
337        *chain = *chain && value;
338    }
339
340    #[inline]
341    fn finish_and(&mut self, chain: Self::AndChain) -> bool {
342        chain
343    }
344
345    #[inline]
346    fn begin_or(&mut self, first: bool) -> Self::OrChain {
347        first
348    }
349
350    #[inline]
351    fn push_or(&mut self, chain: &mut Self::OrChain, value: bool) {
352        *chain = *chain || value;
353    }
354
355    #[inline]
356    fn finish_or(&mut self, chain: Self::OrChain) -> bool {
357        chain
358    }
359
360    #[cfg(feature = "rotation-optimizations")]
361    #[inline]
362    fn begin_cast_guard(&mut self, first: bool) -> Self::CastGuard {
363        first
364    }
365
366    #[cfg(feature = "rotation-optimizations")]
367    #[inline]
368    fn push_cast_guard(&mut self, guard: &mut Self::CastGuard, value: bool) {
369        *guard = *guard && value;
370    }
371
372    #[cfg(feature = "rotation-optimizations")]
373    #[inline]
374    fn finish_cast_guard(
375        &mut self,
376        guard: Self::CastGuard,
377        spell_id: u32,
378        empower_rank: u8,
379    ) -> bool {
380        self.return_cast_if(guard, spell_id, empower_rank);
381
382        guard
383    }
384
385    #[inline]
386    fn bool_to_float(&mut self, v: bool) -> f64 {
387        if v { 1.0 } else { 0.0 }
388    }
389    #[inline]
390    fn bool_to_int(&mut self, v: bool) -> i64 {
391        i64::from(v)
392    }
393    #[inline]
394    fn int_to_float(&mut self, v: i64) -> f64 {
395        wowlab_types::numeric::i64_to_f64(v)
396    }
397    #[inline]
398    fn int_to_bool(&mut self, v: i64) -> bool {
399        v != 0
400    }
401    #[inline]
402    fn float_to_bool(&mut self, v: f64) -> bool {
403        v != 0.0
404    }
405    #[inline]
406    fn float_to_int(&mut self, v: f64) -> i64 {
407        wowlab_types::numeric::f64_to_i64_saturating_trunc(v)
408    }
409
410    #[inline]
411    fn select_f(&mut self, cond: bool, t: f64, f: f64) -> f64 {
412        if cond { t } else { f }
413    }
414    #[inline]
415    fn select_i(&mut self, cond: bool, t: i64, f: i64) -> i64 {
416        if cond { t } else { f }
417    }
418    #[inline]
419    fn select_b(&mut self, cond: bool, t: bool, f: bool) -> bool {
420        if cond { t } else { f }
421    }
422
423    #[inline]
424    fn return_cast_if(&mut self, cond: bool, spell_id: u32, empower_rank: u8) {
425        if cond {
426            self.capture(EvalResult::cast(SpellIdx(spell_id), empower_rank));
427        }
428    }
429    #[inline]
430    fn return_wait_if(&mut self, cond: bool, seconds: f32) {
431        if cond {
432            self.capture(EvalResult::wait(seconds));
433        }
434    }
435    #[inline]
436    fn return_pool_if(&mut self, cond: bool, target: f32) {
437        if cond {
438            self.capture(EvalResult::pool(target));
439        }
440    }
441    #[inline]
442    fn return_use_item_if(&mut self, cond: bool, gear_slot: u8, empower_rank: u8) {
443        if cond {
444            self.capture(EvalResult::use_item_raw(gear_slot, empower_rank));
445        }
446    }
447    #[inline]
448    fn return_none(&mut self) {
449        self.capture(EvalResult::NONE);
450    }
451
452    #[inline]
453    fn record_action(&mut self, record: ActionRecord<'_>) {
454        let now_secs = self.now;
455        let Some(state) = self.trace.as_mut() else {
456            return;
457        };
458        let status = record.outcome.status();
459        let rejection_reason = match &record.outcome {
460            ActionOutcome::Rejected { failing } => state.rejection_reason(now_secs, failing),
461            _ => None,
462        };
463        let Some(decision) = state.stack.last_mut() else {
464            return;
465        };
466
467        decision.evaluations.push(ActionEvaluation {
468            list_id: record.list_id.to_string(),
469            action_index: record.action_index,
470            status,
471            rejection_reason,
472        });
473
474        if matches!(record.outcome, ActionOutcome::Fired) {
475            decision.fired_action_index = Some(record.action_index);
476        }
477    }
478
479    #[inline]
480    fn record_action_guarded(
481        &mut self,
482        list_id: &str,
483        action_index: usize,
484        _action: &RotationAction,
485        is_terminator: bool,
486        guard_held: bool,
487        failing: &Condition,
488    ) {
489        let now_secs = self.now;
490        let Some(state) = self.trace.as_mut() else {
491            return;
492        };
493        let already_fired = state
494            .stack
495            .last()
496            .is_some_and(|d| d.fired_action_index.is_some());
497        let (status, rejection_reason) = if already_fired {
498            (EvaluationStatus::NotReached, None)
499        } else if guard_held {
500            if is_terminator {
501                (EvaluationStatus::Fired, None)
502            } else {
503                (EvaluationStatus::Executed, None)
504            }
505        } else {
506            let reason = state.rejection_reason(now_secs, failing);
507
508            (EvaluationStatus::Rejected, reason)
509        };
510        let Some(decision) = state.stack.last_mut() else {
511            return;
512        };
513
514        decision.evaluations.push(ActionEvaluation {
515            list_id: list_id.to_string(),
516            action_index,
517            status,
518            rejection_reason,
519        });
520
521        if status == EvaluationStatus::Fired {
522            decision.fired_action_index = Some(action_index);
523        }
524    }
525
526    #[inline]
527    fn begin_list_recording(&mut self, list_id: &str) {
528        let Some(state) = self.trace.as_mut() else {
529            return;
530        };
531
532        state.stack.push(Decision {
533            time_ms: state.time_ms,
534            list_id: list_id.to_string(),
535            fired_action_index: None,
536            evaluations: Vec::new(),
537            nested: Vec::new(),
538        });
539    }
540
541    #[inline]
542    fn end_list_recording(&mut self) {
543        let Some(state) = self.trace.as_mut() else {
544            return;
545        };
546        let Some(finished) = state.stack.pop() else {
547            return;
548        };
549
550        if let Some(parent) = state.stack.last_mut() {
551            parent.nested.push(finished);
552        } else {
553            state.sink.record(finished);
554        }
555    }
556}