1use wowlab_engine_ports::{
2 AuraEventRef, Event, SimContext, SimRunError, SimState, SpecAction, SpecHandler,
3};
4use wowlab_engine_telemetry::TelemetrySink;
5use wowlab_types::{constants::MS_PER_SECOND, sim::SimTime};
6
7use crate::{Result, queue::EventQueue, telemetry::TelemetryAccumulator};
8
9const MAX_EVENTS: u32 = 500_000;
10const LAST_ENDPOINT_PRIORITY: u8 = 3;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13enum RunMode {
14 Aggregate,
15 CaptureRepresentative,
16}
17
18pub struct SimEngineRefs<'a> {
20 pub telemetry: &'a mut TelemetryAccumulator,
21 pub handler: &'a mut dyn SpecHandler,
22 pub queue: &'a mut EventQueue,
24 pub sink: &'a mut TelemetrySink,
26}
27
28impl std::fmt::Debug for SimEngineRefs<'_> {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("SimEngineRefs")
31 .field("telemetry", &self.telemetry)
32 .field("handler", &"<dyn SpecHandler>")
33 .field("queue", &self.queue)
34 .field("sink", &self.sink)
35 .finish()
36 }
37}
38
39pub struct SimEngine<'a> {
42 queue: &'a mut EventQueue,
43 state: SimState,
44 telemetry: &'a mut TelemetryAccumulator,
45 handler: &'a mut dyn SpecHandler,
46 sink: &'a mut TelemetrySink,
47 encounter_end_ms: u32,
48 pending_player_ready: Option<SimTime>,
50}
51impl std::fmt::Debug for SimEngine<'_> {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("SimEngine")
56 .field("queue", &self.queue)
57 .field("state", &self.state)
58 .field("telemetry", &self.telemetry)
59 .field("handler", &"<dyn SpecHandler>")
60 .field("sink", &self.sink)
61 .field("encounter_end_ms", &self.encounter_end_ms)
62 .finish()
63 }
64}
65
66impl<'a> SimEngine<'a> {
67 #[must_use]
69 pub fn new(state: SimState, refs: SimEngineRefs<'a>) -> Self {
70 let encounter_end_ms = state.encounter_end.as_millis();
71 let SimEngineRefs {
72 telemetry,
73 handler,
74 queue,
75 sink,
76 } = refs;
77
78 Self {
79 queue,
80 state,
81 telemetry,
82 handler,
83 sink,
84 encounter_end_ms,
85 pending_player_ready: None,
86 }
87 }
88
89 pub fn run(&mut self) -> Result<()> {
95 self.run_with_mode(RunMode::Aggregate)
96 }
97
98 pub fn capture_representative(&mut self) -> Result<()> {
104 self.run_with_mode(RunMode::CaptureRepresentative)
105 }
106
107 fn run_with_mode(&mut self, mode: RunMode) -> Result<()> {
108 self.sink.clear_events();
109
110 self.dispatch(|handler, c| handler.on_sim_start(c))?;
111 self.push_player_ready(SimTime::ZERO);
112
113 let mut event_count: u32 = 0;
114 let mut completed_at = None;
115
116 while let Some(event) = self.queue.pop() {
117 wowlab_types::bench_metrics::record_event();
118 event_count += 1;
119
120 if event_count > MAX_EVENTS {
121 return Err(SimRunError::event_budget_exceeded(event_count));
122 }
123
124 let t = SimTime::from_millis(self.queue.current_time_ms());
125
126 if t.as_millis() > self.encounter_end_ms
127 || (t.as_millis() == self.encounter_end_ms
128 && event.priority() > LAST_ENDPOINT_PRIORITY)
129 {
130 break;
131 }
132
133 self.state.current_time = t;
134 self.handle_event(event, t)?;
135
136 if let Some(termination_time) = self.handler.encounter_termination_time() {
137 completed_at = Some(termination_time);
138 break;
139 }
140 }
141
142 if completed_at.is_none() && self.queue.is_empty() {
143 self.dispatch(|handler, c| handler.on_event_queue_empty(c))?;
144 completed_at = self.handler.encounter_termination_time();
145 }
146
147 self.finalize(
148 completed_at.unwrap_or_else(|| SimTime::from_millis(self.encounter_end_ms)),
149 mode,
150 );
151
152 Ok(())
153 }
154
155 fn push_player_ready(&mut self, t: SimTime) {
157 if let Some(pending) = self.pending_player_ready
158 && pending <= t
159 {
160 return;
161 }
162
163 self.pending_player_ready = Some(t);
164 self.queue.push(Event::PlayerReady { t });
165 }
166
167 fn handle_event(&mut self, event: Event, t: SimTime) -> Result<()> {
170 match event {
171 Event::PlayerReady { .. } => self.handle_player_ready_event(t),
172 Event::OffGcdReady { .. } => self.handle_player_ready(t),
173
174 Event::CastStart {
175 spell,
176 empower_rank,
177 source,
178 target,
179 ..
180 } => {
181 let cast_ms = self.handler.cast_time_ms(spell, empower_rank);
183 let complete_time = SimTime::from_millis(t.as_millis().saturating_add(cast_ms));
184 self.queue.push(Event::CastComplete {
185 t: complete_time,
186 spell,
187 empower_rank,
188 source,
189 target,
190 });
191 Ok(())
194 }
195
196 event @ Event::CastComplete { .. } => {
197 self.dispatch(|handler, c| handler.on_cast_complete(event, c))
198 }
199
200 Event::TriggeredSpell {
201 spell,
202 source,
203 target,
204 ..
205 } => self.dispatch(|handler, c| {
206 handler.on_triggered_spell(spell, source, target, c);
207 }),
208
209 Event::SpellLaunch { impact, .. } => {
210 self.dispatch(|handler, c| handler.on_spell_launch(impact, c))
211 }
212
213 Event::SpellImpact { impact, .. } => {
214 self.dispatch(|handler, c| handler.on_spell_impact(impact, c))
215 }
216
217 Event::GuardianAction { guardian, .. } => {
218 self.dispatch(|handler, c| handler.on_guardian_action(guardian, c))
219 }
220
221 Event::GuardianExpire { guardian, .. } => {
222 self.dispatch(|handler, c| handler.on_guardian_expire(guardian, c))
223 }
224
225 Event::PetAction { action, .. } => {
226 self.dispatch(|handler, c| handler.on_pet_action(action, c))
227 }
228
229 Event::AuraTick { aura, .. } => self
230 .dispatch(|handler, c| handler.on_aura_tick(AuraEventRef::from_binding(aura), c)),
231
232 Event::ChannelTick {
233 spell,
234 source,
235 target,
236 generation,
237 ..
238 } => self.dispatch(|handler, c| {
239 handler.on_channel_tick(spell, source, target, generation, c);
240 }),
241
242 Event::AutoAttack { source, target, .. } => {
243 self.dispatch(|handler, c| handler.on_auto_attack(source, target, c))
244 }
245
246 Event::EnemyAutoAttack { source, target, .. } => {
247 self.dispatch(|handler, c| handler.on_enemy_auto_attack(source, target, c))
248 }
249
250 Event::CooldownReady { spell, .. } => {
251 self.dispatch(|handler, c| handler.on_cooldown_ready(spell, c))
252 }
253
254 Event::AuraExpire { aura, .. } => self
255 .dispatch(|handler, c| handler.on_aura_expire(AuraEventRef::from_binding(aura), c)),
256
257 Event::ProcHeartbeat { .. } => self.dispatch(|handler, c| handler.on_proc_heartbeat(c)),
258
259 Event::Death { target, scope, .. } => {
260 self.dispatch(|handler, c| handler.on_death(target, scope, c))
261 }
262
263 Event::EncounterDespawn {
264 scope,
265 target,
266 kind,
267 ..
268 } => self.dispatch(|handler, c| handler.on_encounter_despawn(scope, target, kind, c)),
269
270 Event::GroupComplete { scope, group, .. } => {
271 self.dispatch(|handler, c| handler.on_group_complete(scope, group, c))
272 }
273
274 Event::PullComplete {
275 scope, finalize, ..
276 } => self.dispatch(|handler, c| handler.on_pull_complete(scope, finalize, c)),
277
278 Event::EncounterTerminate { reason, .. } => {
279 self.dispatch(|handler, c| handler.on_encounter_terminate(reason, c))
280 }
281
282 Event::WaveActivate { scope, wave, .. } => {
283 self.dispatch(|handler, c| handler.on_wave_activate(scope, wave, c))
284 }
285
286 Event::EnemyActivate { scope, target, .. } => {
287 self.dispatch(|handler, c| handler.on_enemy_activate(scope, target, c))
288 }
289
290 Event::Retarget { scope, .. } => {
291 self.dispatch(|handler, c| handler.on_retarget(scope, c))
292 }
293
294 Event::EncounterMove {
295 scope,
296 actor,
297 transform,
298 ..
299 } => self.dispatch(|handler, c| handler.on_encounter_move(scope, actor, transform, c)),
300
301 Event::ActorMovement {
302 movement, moving, ..
303 } => self.dispatch(|handler, c| {
304 handler.on_actor_movement(movement, moving, c);
305 }),
306
307 Event::RaidEvent { event, .. } => {
308 self.dispatch(|handler, c| handler.on_raid_event(event, c))
309 }
310
311 Event::RaidAddDespawn { add, .. } => self.dispatch(|handler, c| {
312 handler.on_raid_add_despawn(add, c);
313 }),
314
315 Event::ExternalBuff { buff, .. } => {
316 self.dispatch(|handler, c| handler.on_external_buff(buff, c))
317 }
318
319 Event::HookTimer {
320 timer,
321 source,
322 target,
323 ..
324 } => self.dispatch(|handler, c| handler.on_hook_timer(timer, source, target, c)),
325 }
326 }
327
328 fn handle_player_ready_event(&mut self, t: SimTime) -> Result<()> {
329 if self.pending_player_ready != Some(t) {
331 return Ok(());
332 }
333
334 self.pending_player_ready = None;
335
336 self.handle_player_ready(t)
337 }
338
339 fn handle_player_ready(&mut self, t: SimTime) -> Result<()> {
340 let action = {
341 let mut c = sim_context(&self.state, self.sink);
342
343 self.handler.on_player_ready(&mut c)
344 };
345
346 self.check_handler_error()?;
347
348 match action {
349 Some(SpecAction::Cast {
350 spell,
351 empower_rank,
352 source,
353 target,
354 }) => {
355 self.queue.push(Event::CastStart {
356 t,
357 spell,
358 empower_rank,
359 source,
360 target,
361 });
362 }
363 Some(SpecAction::Wait { until_ms }) => {
364 let min_next = t.saturating_add(SimTime::from_millis(1));
366 let clamped = SimTime::from_millis(
367 until_ms
368 .max(min_next)
369 .as_millis()
370 .min(self.encounter_end_ms),
371 );
372
373 self.push_player_ready(clamped);
374 }
375 None => {}
376 }
377
378 flush_handler_events(
379 &mut *self.handler,
380 &mut *self.queue,
381 &mut self.pending_player_ready,
382 );
383
384 Ok(())
385 }
386
387 fn dispatch(&mut self, f: impl FnOnce(&mut dyn SpecHandler, &mut SimContext)) -> Result<()> {
388 {
389 let mut c = sim_context(&self.state, self.sink);
390
391 f(self.handler, &mut c);
392 };
393 self.check_handler_error()?;
394 flush_handler_events(
395 &mut *self.handler,
396 &mut *self.queue,
397 &mut self.pending_player_ready,
398 );
399
400 Ok(())
401 }
402
403 fn check_handler_error(&mut self) -> Result<()> {
404 self.handler
405 .take_runtime_error()
406 .map_or(Ok(()), |source| Err(SimRunError::handler(source)))
407 }
408
409 fn finalize(&mut self, completed_at: SimTime, mode: RunMode) {
410 let total_damage = self.handler.total_damage();
411
412 #[cfg(debug_assertions)]
413 self.debug_assert_damage_telemetry_reconciled(total_damage, completed_at, mode);
414
415 let duration_secs = f64::from(completed_at.as_millis()) / MS_PER_SECOND;
416 let dps = if duration_secs > 0.0 {
417 total_damage / duration_secs
418 } else {
419 0.0
420 };
421
422 match mode {
423 RunMode::Aggregate => {
424 self.telemetry.record_sink_events(self.sink, duration_secs);
425 self.telemetry.record_iteration_for(
426 self.state.seed_prefix,
427 self.state.iteration,
428 dps,
429 );
430 }
431 RunMode::CaptureRepresentative => {
432 self.telemetry
433 .install_representative(dps, self.sink, duration_secs);
434 }
435 }
436 }
437
438 #[cfg(debug_assertions)]
439 fn debug_assert_damage_telemetry_reconciled(
440 &self,
441 combat_total: f64,
442 completed_at: SimTime,
443 mode: RunMode,
444 ) {
445 let Some(reconciliation) = damage_reconciliation(combat_total, self.sink) else {
446 return;
447 };
448
449 debug_assert!(
450 reconciliation.is_within_error_bound(),
451 "damage telemetry diverged from the authoritative combat total: combat_total={:.17e}, telemetry_total={:.17e}, direct={:.17e}, periodic={:.17e}, pet={:.17e}, absolute_delta={:.17e}, relative_delta={:.17e}, error_bound={:.17e}, event_count={}, absolute_event_sum={:.17e}, iteration={}, seed_prefix={}, completed_at_ms={}, mode={mode:?}",
452 reconciliation.combat_total,
453 reconciliation.telemetry_total,
454 reconciliation.direct,
455 reconciliation.periodic,
456 reconciliation.pet,
457 reconciliation.absolute_delta,
458 reconciliation.relative_delta,
459 reconciliation.error_bound,
460 reconciliation.event_count,
461 reconciliation.absolute_event_sum,
462 self.state.iteration,
463 self.state.seed_prefix,
464 completed_at.as_millis(),
465 );
466 }
467}
468
469#[derive(Clone, Copy, Debug)]
470#[cfg(debug_assertions)]
471struct DamageReconciliation {
472 combat_total: f64,
473 telemetry_total: f64,
474 direct: f64,
475 periodic: f64,
476 pet: f64,
477 absolute_delta: f64,
478 relative_delta: f64,
479 error_bound: f64,
480 event_count: usize,
481 absolute_event_sum: f64,
482}
483
484#[cfg(debug_assertions)]
485const TELEMETRY_CATEGORY_COMBINE_ADDITIONS: usize = 2;
486
487#[cfg(debug_assertions)]
488impl DamageReconciliation {
489 fn is_within_error_bound(self) -> bool {
490 self.absolute_delta <= self.error_bound
491 }
492}
493
494#[cfg(debug_assertions)]
495fn summation_error_coefficient(additions: usize) -> f64 {
496 let Ok(additions) = u32::try_from(additions) else {
497 return f64::INFINITY;
498 };
499 let scaled_epsilon = f64::from(additions) * f64::EPSILON;
500
501 if scaled_epsilon >= 1.0 {
502 f64::INFINITY
503 } else {
504 scaled_epsilon / (1.0 - scaled_epsilon)
505 }
506}
507
508#[cfg(debug_assertions)]
509fn damage_reconciliation(combat_total: f64, sink: &TelemetrySink) -> Option<DamageReconciliation> {
510 if sink.damage.is_empty() {
513 return None;
514 }
515
516 let (direct, periodic, pet, absolute_event_sum) =
517 sink.damage
518 .iter()
519 .fold((0.0, 0.0, 0.0, 0.0), |mut totals, event| {
520 if event.is_pet {
521 totals.2 += event.amount;
522 } else if event.is_periodic {
523 totals.1 += event.amount;
524 } else {
525 totals.0 += event.amount;
526 }
527
528 totals.3 += event.amount.abs();
529
530 totals
531 });
532 let telemetry_total = direct + periodic + pet;
533 let absolute_delta = (combat_total - telemetry_total).abs();
534 let scale = combat_total.abs().max(telemetry_total.abs());
535 let relative_delta = if scale > 0.0 {
536 absolute_delta / scale
537 } else {
538 0.0
539 };
540 let event_count = sink.damage.len();
541 let combat_error = summation_error_coefficient(event_count);
542 let telemetry_error = summation_error_coefficient(
543 event_count.saturating_add(TELEMETRY_CATEGORY_COMBINE_ADDITIONS),
544 );
545 let error_bound = (combat_error + telemetry_error) * absolute_event_sum;
546
547 Some(DamageReconciliation {
548 combat_total,
549 telemetry_total,
550 direct,
551 periodic,
552 pet,
553 absolute_delta,
554 relative_delta,
555 error_bound,
556 event_count,
557 absolute_event_sum,
558 })
559}
560
561fn sim_context<'a>(state: &'a SimState, sink: &'a mut TelemetrySink) -> SimContext<'a> {
562 SimContext {
563 state,
564 telemetry: sink,
565 }
566}
567
568fn flush_handler_events(
569 handler: &mut dyn SpecHandler,
570 queue: &mut EventQueue,
571 pending_player_ready: &mut Option<SimTime>,
572) {
573 handler.flush_scheduled(&mut |event| {
574 if let Event::PlayerReady { t } = event {
576 if pending_player_ready.is_some_and(|pending| pending <= t) {
577 return;
578 }
579
580 *pending_player_ready = Some(t);
581 }
582
583 queue.push(event);
584 });
585}
586
587#[cfg(test)]
588#[path = "engine/tests.rs"]
589mod tests;