1use 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 RecorderCtx<'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 InterpFrame<'a> {
36 pub(crate) buffer: &'a mut DenseBuffer,
37 pub(crate) now: f64,
38}
39
40struct TraceState<'a> {
41 sink: DecisionTraceTarget,
42 ctx: RecorderCtx<'a>,
43 buffer: *mut DenseBuffer,
44 stack: Vec<Decision>,
45 time_ms: u32,
46}
47
48pub(crate) struct InterpBackend<'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> InterpBackend<'a> {
58 pub(crate) fn new(frame: InterpFrame<'a>) -> Self {
59 let InterpFrame { 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: InterpFrame<'a>,
74 sink: DecisionTraceTarget,
75 ctx: RecorderCtx<'a>,
76 ) -> Self {
77 let InterpFrame { 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 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 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 InterpBackend<'_> {
142 type Bool = bool;
143 type Int = i64;
144 type Float = f64;
145
146 #[inline]
147 fn const_bool(&mut self, v: bool) -> bool {
148 v
149 }
150 #[inline]
151 fn const_i32(&mut self, v: i32) -> i64 {
152 i64::from(v)
153 }
154 #[inline]
155 fn const_i64(&mut self, v: i64) -> i64 {
156 v
157 }
158 #[inline]
159 fn const_f64(&mut self, v: f64) -> f64 {
160 v
161 }
162
163 #[inline]
164 fn load_bool(&mut self, offset: usize) -> bool {
165 bytemuck::pod_read_unaligned::<i32>(&self.bytes()[offset..offset + size_of::<i32>()]) != 0
167 }
168 #[inline]
169 fn load_i32(&mut self, offset: usize) -> i64 {
170 i64::from(bytemuck::pod_read_unaligned::<i32>(
172 &self.bytes()[offset..offset + size_of::<i32>()],
173 ))
174 }
175 #[inline]
176 fn load_i64(&mut self, offset: usize) -> i64 {
177 bytemuck::pod_read_unaligned::<i64>(&self.bytes()[offset..offset + size_of::<i64>()])
179 }
180 #[inline]
181 fn load_f64(&mut self, offset: usize) -> f64 {
182 bytemuck::pod_read_unaligned::<f64>(&self.bytes()[offset..offset + size_of::<f64>()])
184 }
185
186 #[inline]
187 fn store_bool(&mut self, offset: usize, v: bool) {
188 let bytes = i32::from(v).to_ne_bytes();
189 unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
192 .copy_from_slice(&bytes);
193 }
194 #[inline]
195 fn store_i32(&mut self, offset: usize, v: i64) {
196 let bytes = crate::numeric::wrapping_i64_to_i32(v).to_ne_bytes();
197 unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
200 .copy_from_slice(&bytes);
201 }
202 #[inline]
203 fn store_f64(&mut self, offset: usize, v: f64) {
204 let bytes = v.to_ne_bytes();
205 unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
208 .copy_from_slice(&bytes);
209 }
210
211 #[inline]
212 fn now(&mut self) -> f64 {
213 self.now
214 }
215
216 #[inline]
217 fn add_f(&mut self, a: f64, b: f64) -> f64 {
218 a + b
219 }
220 #[inline]
221 fn sub_f(&mut self, a: f64, b: f64) -> f64 {
222 a - b
223 }
224 #[inline]
225 fn mul_f(&mut self, a: f64, b: f64) -> f64 {
226 a * b
227 }
228 #[inline]
229 fn safe_div_f(&mut self, a: f64, b: f64) -> f64 {
230 safe_div(a, b)
231 }
232 #[inline]
233 fn true_mod_f(&mut self, a: f64, b: f64) -> f64 {
234 true_mod(a, b)
235 }
236 #[inline]
237 fn min_f(&mut self, a: f64, b: f64) -> f64 {
238 a.min(b)
239 }
240 #[inline]
241 fn max_f(&mut self, a: f64, b: f64) -> f64 {
242 a.max(b)
243 }
244 #[inline]
245 fn floor_f(&mut self, v: f64) -> f64 {
246 v.floor()
247 }
248 #[inline]
249 fn ceil_f(&mut self, v: f64) -> f64 {
250 v.ceil()
251 }
252 #[inline]
253 fn abs_f(&mut self, v: f64) -> f64 {
254 v.abs()
255 }
256
257 #[inline]
258 fn cmp_gt_f(&mut self, a: f64, b: f64) -> bool {
259 a > b
260 }
261 #[inline]
262 fn cmp_gte_f(&mut self, a: f64, b: f64) -> bool {
263 a >= b
264 }
265 #[inline]
266 fn cmp_lt_f(&mut self, a: f64, b: f64) -> bool {
267 a < b
268 }
269 #[inline]
270 fn cmp_lte_f(&mut self, a: f64, b: f64) -> bool {
271 a <= b
272 }
273 #[inline]
274 fn cmp_eq_f(&mut self, a: f64, b: f64) -> bool {
275 float_eq(a, b)
276 }
277 #[inline]
278 fn cmp_ne_f(&mut self, a: f64, b: f64) -> bool {
279 float_ne(a, b)
280 }
281 #[inline]
282 fn cmp_eq_i(&mut self, a: i64, b: i64) -> bool {
283 a == b
284 }
285 #[inline]
286 fn cmp_ne_i(&mut self, a: i64, b: i64) -> bool {
287 a != b
288 }
289 #[inline]
290 fn cmp_gt_i(&mut self, a: i64, b: i64) -> bool {
291 a > b
292 }
293 #[inline]
294 fn cmp_gte_i(&mut self, a: i64, b: i64) -> bool {
295 a >= b
296 }
297 #[inline]
298 fn cmp_lt_i(&mut self, a: i64, b: i64) -> bool {
299 a < b
300 }
301 #[inline]
302 fn cmp_lte_i(&mut self, a: i64, b: i64) -> bool {
303 a <= b
304 }
305
306 #[inline]
307 fn and_b(&mut self, a: bool, b: bool) -> bool {
308 a && b
309 }
310 #[inline]
311 fn or_b(&mut self, a: bool, b: bool) -> bool {
312 a || b
313 }
314 #[inline]
315 fn not_b(&mut self, a: bool) -> bool {
316 !a
317 }
318
319 #[inline]
320 fn bool_to_float(&mut self, v: bool) -> f64 {
321 if v { 1.0 } else { 0.0 }
322 }
323 #[inline]
324 fn bool_to_int(&mut self, v: bool) -> i64 {
325 i64::from(v)
326 }
327 #[inline]
328 fn int_to_float(&mut self, v: i64) -> f64 {
329 wowlab_types::numeric::i64_to_f64(v)
330 }
331 #[inline]
332 fn int_to_bool(&mut self, v: i64) -> bool {
333 v != 0
334 }
335 #[inline]
336 fn float_to_bool(&mut self, v: f64) -> bool {
337 v != 0.0
338 }
339 #[inline]
340 fn float_to_int(&mut self, v: f64) -> i64 {
341 wowlab_types::numeric::f64_to_i64_saturating_trunc(v)
342 }
343
344 #[inline]
345 fn select_f(&mut self, cond: bool, t: f64, f: f64) -> f64 {
346 if cond { t } else { f }
347 }
348 #[inline]
349 fn select_i(&mut self, cond: bool, t: i64, f: i64) -> i64 {
350 if cond { t } else { f }
351 }
352 #[inline]
353 fn select_b(&mut self, cond: bool, t: bool, f: bool) -> bool {
354 if cond { t } else { f }
355 }
356
357 #[inline]
358 fn return_none_if(&mut self, cond: bool) {
359 if cond {
360 self.capture(EvalResult::NONE);
361 }
362 }
363 #[inline]
364 fn return_cast_if(&mut self, cond: bool, spell_id: u32, empower_rank: u8) {
365 if cond {
366 self.capture(EvalResult::cast(SpellIdx(spell_id), empower_rank));
367 }
368 }
369 #[inline]
370 fn return_wait_if(&mut self, cond: bool, seconds: f32) {
371 if cond {
372 self.capture(EvalResult::wait(seconds));
373 }
374 }
375 #[inline]
376 fn return_pool_if(&mut self, cond: bool, target: f32) {
377 if cond {
378 self.capture(EvalResult::pool(target));
379 }
380 }
381 #[inline]
382 fn return_use_item_if(&mut self, cond: bool, gear_slot: u8, empower_rank: u8) {
383 if cond {
384 self.capture(EvalResult::use_item_raw(gear_slot, empower_rank));
385 }
386 }
387 #[inline]
388 fn return_none(&mut self) {
389 self.capture(EvalResult::NONE);
390 }
391
392 #[inline]
393 fn record_action(&mut self, record: ActionRecord<'_>) {
394 let now_secs = self.now;
395 let Some(state) = self.trace.as_mut() else {
396 return;
397 };
398 let status = record.outcome.status();
399 let rejection_reason = match &record.outcome {
400 ActionOutcome::Rejected { failing } => state.rejection_reason(now_secs, failing),
401 _ => None,
402 };
403 let Some(decision) = state.stack.last_mut() else {
404 return;
405 };
406
407 decision.evaluations.push(ActionEvaluation {
408 list_id: record.list_id.to_string(),
409 action_index: record.action_index,
410 status,
411 rejection_reason,
412 });
413
414 if matches!(record.outcome, ActionOutcome::Fired) {
415 decision.fired_action_index = Some(record.action_index);
416 }
417 }
418
419 #[inline]
420 fn record_action_guarded(
421 &mut self,
422 list_id: &str,
423 action_index: usize,
424 _action: &RotationAction,
425 is_terminator: bool,
426 guard_held: bool,
427 failing: &Condition,
428 ) {
429 let now_secs = self.now;
430 let Some(state) = self.trace.as_mut() else {
431 return;
432 };
433 let already_fired = state
434 .stack
435 .last()
436 .is_some_and(|d| d.fired_action_index.is_some());
437 let (status, rejection_reason) = if already_fired {
438 (EvaluationStatus::NotReached, None)
439 } else if guard_held {
440 if is_terminator {
441 (EvaluationStatus::Fired, None)
442 } else {
443 (EvaluationStatus::Executed, None)
444 }
445 } else {
446 let reason = state.rejection_reason(now_secs, failing);
447
448 (EvaluationStatus::Rejected, reason)
449 };
450 let Some(decision) = state.stack.last_mut() else {
451 return;
452 };
453
454 decision.evaluations.push(ActionEvaluation {
455 list_id: list_id.to_string(),
456 action_index,
457 status,
458 rejection_reason,
459 });
460
461 if status == EvaluationStatus::Fired {
462 decision.fired_action_index = Some(action_index);
463 }
464 }
465
466 #[inline]
467 fn begin_list_recording(&mut self, list_id: &str) {
468 let Some(state) = self.trace.as_mut() else {
469 return;
470 };
471
472 state.stack.push(Decision {
473 time_ms: state.time_ms,
474 list_id: list_id.to_string(),
475 fired_action_index: None,
476 evaluations: Vec::new(),
477 nested: Vec::new(),
478 });
479 }
480
481 #[inline]
482 fn end_list_recording(&mut self) {
483 let Some(state) = self.trace.as_mut() else {
484 return;
485 };
486 let Some(finished) = state.stack.pop() else {
487 return;
488 };
489
490 if let Some(parent) = state.stack.last_mut() {
491 parent.nested.push(finished);
492 } else {
493 state.sink.record(finished);
494 }
495 }
496}