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();
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 event_count += 1;
118
119 if event_count > MAX_EVENTS {
120 return Err(SimRunError::event_budget_exceeded(event_count));
121 }
122
123 let t = event.timestamp();
124
125 if t.as_millis() > self.encounter_end_ms
126 || (t.as_millis() == self.encounter_end_ms
127 && event.priority() > LAST_ENDPOINT_PRIORITY)
128 {
129 break;
130 }
131
132 self.state.current_time = t;
133 self.handle_event(event, t)?;
134
135 if let Some(termination_time) = self.handler.encounter_termination_time() {
136 completed_at = Some(termination_time);
137 break;
138 }
139 }
140
141 if completed_at.is_none() && self.queue.is_empty() {
142 self.dispatch(|handler, c| handler.on_event_queue_empty(c))?;
143 completed_at = self.handler.encounter_termination_time();
144 }
145
146 self.finalize(
147 completed_at.unwrap_or_else(|| SimTime::from_millis(self.encounter_end_ms)),
148 mode,
149 );
150
151 Ok(())
152 }
153
154 fn push_player_ready(&mut self, t: SimTime) {
156 if let Some(pending) = self.pending_player_ready {
157 if pending <= t {
158 return;
159 }
160 }
161
162 self.pending_player_ready = Some(t);
163 self.queue.push(Event::PlayerReady { t });
164 }
165
166 fn handle_event(&mut self, event: Event, t: SimTime) -> Result<()> {
169 match event {
170 Event::PlayerReady { .. } => self.handle_player_ready_event(t),
171 Event::OffGcdReady { .. } => self.handle_player_ready(t),
172
173 Event::CastStart {
174 t,
175 spell_id,
176 empower_rank,
177 source,
178 target,
179 } => {
180 let cast_ms = self.handler.cast_time_ms(spell_id, empower_rank);
182 let complete_time = SimTime::from_millis(t.as_millis().saturating_add(cast_ms));
183 self.queue.push(Event::CastComplete {
184 t: complete_time,
185 spell_id,
186 empower_rank,
187 source,
188 target,
189 });
190 Ok(())
193 }
194
195 event @ Event::CastComplete { .. } => {
196 self.dispatch(|handler, c| handler.on_cast_complete(event, c))
197 }
198
199 Event::TriggeredSpell {
200 spell_id,
201 source,
202 target,
203 ..
204 } => self.dispatch(|handler, c| {
205 handler.on_triggered_spell(spell_id, source, target, c);
206 }),
207
208 Event::SpellLaunch { impact_id, .. } => {
209 self.dispatch(|handler, c| handler.on_spell_launch(impact_id, c))
210 }
211
212 Event::SpellImpact { impact_id, .. } => {
213 self.dispatch(|handler, c| handler.on_spell_impact(impact_id, c))
214 }
215
216 Event::GuardianAction {
217 guardian_id,
218 guardian_generation,
219 ability_index,
220 ..
221 } => self.dispatch(|handler, c| {
222 handler.on_guardian_action(guardian_id, guardian_generation, ability_index, c);
223 }),
224
225 Event::GuardianExpire {
226 guardian_id,
227 guardian_generation,
228 ..
229 } => self.dispatch(|handler, c| {
230 handler.on_guardian_expire(guardian_id, guardian_generation, c);
231 }),
232
233 Event::PetAction {
234 auto_attack_index, ..
235 } => self.dispatch(|handler, c| handler.on_pet_action(auto_attack_index, c)),
236
237 Event::AuraTick { key, target, .. } => {
238 self.dispatch(|handler, c| handler.on_aura_tick(AuraEventRef { key, target }, c))
239 }
240
241 Event::ChannelTick {
242 spell_id,
243 source,
244 target,
245 generation,
246 ..
247 } => self.dispatch(|handler, c| {
248 handler.on_channel_tick(spell_id, source, target, generation, c);
249 }),
250
251 Event::AutoAttack { source, target, .. } => {
252 self.dispatch(|handler, c| handler.on_auto_attack(source, target, c))
253 }
254
255 Event::EnemyAutoAttack { source, target, .. } => {
256 self.dispatch(|handler, c| handler.on_enemy_auto_attack(source, target, c))
257 }
258
259 Event::CooldownReady { cooldown_key, .. } => {
260 self.dispatch(|handler, c| handler.on_cooldown_ready(cooldown_key, c))
261 }
262
263 Event::AuraExpire { key, target, .. } => {
264 self.dispatch(|handler, c| handler.on_aura_expire(AuraEventRef { key, target }, c))
265 }
266
267 Event::ProcHeartbeat { .. } => self.dispatch(|handler, c| handler.on_proc_heartbeat(c)),
268
269 Event::Death { target, scope, .. } => {
270 self.dispatch(|handler, c| handler.on_death(target, scope, c))
271 }
272
273 Event::EncounterDespawn {
274 scope,
275 target,
276 kind,
277 ..
278 } => self.dispatch(|handler, c| handler.on_encounter_despawn(scope, target, kind, c)),
279
280 Event::GroupComplete { scope, group, .. } => {
281 self.dispatch(|handler, c| handler.on_group_complete(scope, group, c))
282 }
283
284 Event::PullComplete {
285 scope, finalize, ..
286 } => self.dispatch(|handler, c| handler.on_pull_complete(scope, finalize, c)),
287
288 Event::EncounterTerminate { reason, .. } => {
289 self.dispatch(|handler, c| handler.on_encounter_terminate(reason, c))
290 }
291
292 Event::WaveActivate { scope, wave, .. } => {
293 self.dispatch(|handler, c| handler.on_wave_activate(scope, wave, c))
294 }
295
296 Event::EnemyActivate { scope, target, .. } => {
297 self.dispatch(|handler, c| handler.on_enemy_activate(scope, target, c))
298 }
299
300 Event::Retarget { scope, .. } => {
301 self.dispatch(|handler, c| handler.on_retarget(scope, c))
302 }
303
304 Event::EncounterMove {
305 scope,
306 actor,
307 transform,
308 ..
309 } => self.dispatch(|handler, c| handler.on_encounter_move(scope, actor, transform, c)),
310
311 Event::ActorMovement {
312 actor,
313 moving,
314 generation,
315 ..
316 } => self.dispatch(|handler, c| {
317 handler.on_actor_movement(actor, moving, generation, c);
318 }),
319
320 Event::RaidEvent { index, .. } => {
321 self.dispatch(|handler, c| handler.on_raid_event(index, c))
322 }
323
324 Event::RaidAddDespawn {
325 target,
326 activation_generation,
327 encounter_generation,
328 ..
329 } => self.dispatch(|handler, c| {
330 handler.on_raid_add_despawn(target, activation_generation, encounter_generation, c);
331 }),
332
333 Event::ExternalBuff { index, .. } => {
334 self.dispatch(|handler, c| handler.on_external_buff(index, c))
335 }
336
337 Event::HookTimer {
338 timer_id,
339 source,
340 target,
341 ..
342 } => self.dispatch(|handler, c| handler.on_hook_timer(timer_id, source, target, c)),
343 }
344 }
345
346 fn handle_player_ready_event(&mut self, t: SimTime) -> Result<()> {
347 if self.pending_player_ready != Some(t) {
349 return Ok(());
350 }
351
352 self.pending_player_ready = None;
353
354 self.handle_player_ready(t)
355 }
356
357 fn handle_player_ready(&mut self, t: SimTime) -> Result<()> {
358 let action = {
359 let mut c = sim_context(&self.state, self.sink);
360
361 self.handler.on_player_ready(&mut c)
362 };
363
364 self.check_handler_error()?;
365
366 match action {
367 Some(SpecAction::Cast {
368 spell_id,
369 empower_rank,
370 source,
371 target,
372 }) => {
373 self.queue.push(Event::CastStart {
374 t,
375 spell_id,
376 empower_rank,
377 source,
378 target,
379 });
380 }
381 Some(SpecAction::Wait { until_ms }) => {
382 let min_next = t.saturating_add(SimTime::from_millis(1));
384 let clamped = SimTime::from_millis(
385 until_ms
386 .max(min_next)
387 .as_millis()
388 .min(self.encounter_end_ms),
389 );
390
391 self.push_player_ready(clamped);
392 }
393 None => {}
394 }
395
396 flush_handler_events(
397 &mut *self.handler,
398 &mut *self.queue,
399 &mut self.pending_player_ready,
400 );
401
402 Ok(())
403 }
404
405 fn dispatch(&mut self, f: impl FnOnce(&mut dyn SpecHandler, &mut SimContext)) -> Result<()> {
406 {
407 let mut c = sim_context(&self.state, self.sink);
408
409 f(self.handler, &mut c);
410 };
411 self.check_handler_error()?;
412 flush_handler_events(
413 &mut *self.handler,
414 &mut *self.queue,
415 &mut self.pending_player_ready,
416 );
417
418 Ok(())
419 }
420
421 fn check_handler_error(&mut self) -> Result<()> {
422 self.handler
423 .take_runtime_error()
424 .map_or(Ok(()), |source| Err(SimRunError::handler(source)))
425 }
426
427 fn finalize(&mut self, completed_at: SimTime, mode: RunMode) {
428 let total_damage = self.handler.total_damage();
429
430 #[cfg(debug_assertions)]
431 self.debug_assert_damage_telemetry_reconciled(total_damage, completed_at, mode);
432
433 let duration_secs = f64::from(completed_at.as_millis()) / MS_PER_SECOND;
434 let dps = if duration_secs > 0.0 {
435 total_damage / duration_secs
436 } else {
437 0.0
438 };
439
440 match mode {
441 RunMode::Aggregate => {
442 self.telemetry.record_sink_events(self.sink, duration_secs);
443 self.telemetry.record_iteration_for(
444 self.state.seed_prefix,
445 self.state.iteration,
446 dps,
447 );
448 }
449 RunMode::CaptureRepresentative => {
450 self.telemetry
451 .install_representative(dps, self.sink, duration_secs);
452 }
453 }
454 }
455
456 #[cfg(debug_assertions)]
457 fn debug_assert_damage_telemetry_reconciled(
458 &self,
459 combat_total: f64,
460 completed_at: SimTime,
461 mode: RunMode,
462 ) {
463 let Some(reconciliation) = damage_reconciliation(combat_total, self.sink) else {
464 return;
465 };
466
467 debug_assert!(
468 reconciliation.is_within_error_bound(),
469 "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:?}",
470 reconciliation.combat_total,
471 reconciliation.telemetry_total,
472 reconciliation.direct,
473 reconciliation.periodic,
474 reconciliation.pet,
475 reconciliation.absolute_delta,
476 reconciliation.relative_delta,
477 reconciliation.error_bound,
478 reconciliation.event_count,
479 reconciliation.absolute_event_sum,
480 self.state.iteration,
481 self.state.seed_prefix,
482 completed_at.as_millis(),
483 );
484 }
485}
486
487#[derive(Clone, Copy, Debug)]
488#[cfg(debug_assertions)]
489struct DamageReconciliation {
490 combat_total: f64,
491 telemetry_total: f64,
492 direct: f64,
493 periodic: f64,
494 pet: f64,
495 absolute_delta: f64,
496 relative_delta: f64,
497 error_bound: f64,
498 event_count: usize,
499 absolute_event_sum: f64,
500}
501
502#[cfg(debug_assertions)]
503const TELEMETRY_CATEGORY_COMBINE_ADDITIONS: usize = 2;
504
505#[cfg(debug_assertions)]
506impl DamageReconciliation {
507 fn is_within_error_bound(self) -> bool {
508 self.absolute_delta <= self.error_bound
509 }
510}
511
512#[cfg(debug_assertions)]
513fn summation_error_coefficient(additions: usize) -> f64 {
514 let Ok(additions) = u32::try_from(additions) else {
515 return f64::INFINITY;
516 };
517 let scaled_epsilon = f64::from(additions) * f64::EPSILON;
518
519 if scaled_epsilon >= 1.0 {
520 f64::INFINITY
521 } else {
522 scaled_epsilon / (1.0 - scaled_epsilon)
523 }
524}
525
526#[cfg(debug_assertions)]
527fn damage_reconciliation(combat_total: f64, sink: &TelemetrySink) -> Option<DamageReconciliation> {
528 if sink.damage.is_empty() {
531 return None;
532 }
533
534 let (direct, periodic, pet, absolute_event_sum) =
535 sink.damage
536 .iter()
537 .fold((0.0, 0.0, 0.0, 0.0), |mut totals, event| {
538 if event.is_pet {
539 totals.2 += event.amount;
540 } else if event.is_periodic {
541 totals.1 += event.amount;
542 } else {
543 totals.0 += event.amount;
544 }
545
546 totals.3 += event.amount.abs();
547
548 totals
549 });
550 let telemetry_total = direct + periodic + pet;
551 let absolute_delta = (combat_total - telemetry_total).abs();
552 let scale = combat_total.abs().max(telemetry_total.abs());
553 let relative_delta = if scale > 0.0 {
554 absolute_delta / scale
555 } else {
556 0.0
557 };
558 let event_count = sink.damage.len();
559 let combat_error = summation_error_coefficient(event_count);
560 let telemetry_error = summation_error_coefficient(
561 event_count.saturating_add(TELEMETRY_CATEGORY_COMBINE_ADDITIONS),
562 );
563 let error_bound = (combat_error + telemetry_error) * absolute_event_sum;
564
565 Some(DamageReconciliation {
566 combat_total,
567 telemetry_total,
568 direct,
569 periodic,
570 pet,
571 absolute_delta,
572 relative_delta,
573 error_bound,
574 event_count,
575 absolute_event_sum,
576 })
577}
578
579fn sim_context<'a>(state: &'a SimState, sink: &'a mut TelemetrySink) -> SimContext<'a> {
580 SimContext {
581 state,
582 telemetry: sink,
583 }
584}
585
586fn flush_handler_events(
587 handler: &mut dyn SpecHandler,
588 queue: &mut EventQueue,
589 pending_player_ready: &mut Option<SimTime>,
590) {
591 handler.flush_scheduled(&mut |event| {
592 if let Event::PlayerReady { t } = event {
594 if pending_player_ready.is_some_and(|pending| pending <= t) {
595 return;
596 }
597
598 *pending_player_ready = Some(t);
599 }
600
601 queue.push(event);
602 });
603}
604
605#[cfg(test)]
606#[path = "engine/tests.rs"]
607mod tests;