elevator_core/hooks.rs
1//! Lifecycle hooks for injecting custom logic before/after simulation phases.
2//!
3//! # Example
4//!
5//! ```rust
6//! use elevator_core::prelude::*;
7//! use elevator_core::hooks::Phase;
8//!
9//! let mut sim = SimulationBuilder::demo()
10//! .before(Phase::Dispatch, |world| {
11//! // Inspect world state before dispatch runs
12//! let idle_count = world.iter_idle_elevators().count();
13//! let _ = idle_count; // use it
14//! })
15//! .build()
16//! .unwrap();
17//!
18//! sim.step(); // hooks fire during each step
19//! ```
20
21use crate::ids::GroupId;
22use crate::world::World;
23use std::collections::HashMap;
24
25/// Simulation phase identifier for hook registration.
26///
27/// Each variant corresponds to one phase in the tick loop. Hooks registered
28/// for a phase run immediately before or after that phase executes.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30#[non_exhaustive]
31pub enum Phase {
32 /// Advance transient rider states (Boarding→Riding, Exiting→Arrived).
33 AdvanceTransient,
34 /// Assign idle elevators to stops via dispatch strategy.
35 Dispatch,
36 /// Update elevator position and velocity.
37 Movement,
38 /// Tick door finite-state machines.
39 Doors,
40 /// Board and exit riders.
41 Loading,
42 /// Reposition idle elevators for better coverage.
43 Reposition,
44 /// Reconcile elevator phase with its `DestinationQueue` front.
45 AdvanceQueue,
46 /// Aggregate metrics from tick events.
47 Metrics,
48}
49
50/// A boxed closure that receives mutable world access during a phase hook.
51pub(crate) type PhaseHook = Box<dyn Fn(&mut World) + Send + Sync>;
52
53/// Storage for before/after hooks keyed by phase.
54#[derive(Default)]
55pub(crate) struct PhaseHooks {
56 /// Hooks to run before each phase.
57 before: HashMap<Phase, Vec<PhaseHook>>,
58 /// Hooks to run after each phase.
59 after: HashMap<Phase, Vec<PhaseHook>>,
60 /// Hooks to run before a phase for a specific group.
61 before_group: HashMap<(Phase, GroupId), Vec<PhaseHook>>,
62 /// Hooks to run after a phase for a specific group.
63 after_group: HashMap<(Phase, GroupId), Vec<PhaseHook>>,
64}
65
66impl PhaseHooks {
67 /// Run all before-hooks for the given phase.
68 pub(crate) fn run_before(&self, phase: Phase, world: &mut World) {
69 if let Some(hooks) = self.before.get(&phase) {
70 for hook in hooks {
71 hook(world);
72 }
73 Self::debug_check_invariants(phase, world);
74 }
75 }
76
77 /// Run all after-hooks for the given phase.
78 pub(crate) fn run_after(&self, phase: Phase, world: &mut World) {
79 if let Some(hooks) = self.after.get(&phase) {
80 for hook in hooks {
81 hook(world);
82 }
83 Self::debug_check_invariants(phase, world);
84 }
85 }
86
87 /// In debug builds, verify that hooks did not break core invariants.
88 #[cfg(debug_assertions)]
89 fn debug_check_invariants(phase: Phase, world: &World) {
90 for (eid, _, elev) in world.iter_elevators() {
91 for &rider_id in &elev.riders {
92 debug_assert!(
93 world.is_alive(rider_id),
94 "hook after {phase:?}: elevator {eid:?} references dead rider {rider_id:?}"
95 );
96 }
97 }
98 }
99
100 #[cfg(not(debug_assertions))]
101 fn debug_check_invariants(_phase: Phase, _world: &World) {}
102
103 /// Register a hook to run before a phase.
104 pub(crate) fn add_before(&mut self, phase: Phase, hook: PhaseHook) {
105 self.before.entry(phase).or_default().push(hook);
106 }
107
108 /// Register a hook to run after a phase.
109 pub(crate) fn add_after(&mut self, phase: Phase, hook: PhaseHook) {
110 self.after.entry(phase).or_default().push(hook);
111 }
112
113 /// Run all before-hooks for the given phase and group.
114 pub(crate) fn run_before_group(&self, phase: Phase, group: GroupId, world: &mut World) {
115 if let Some(hooks) = self.before_group.get(&(phase, group)) {
116 for hook in hooks {
117 hook(world);
118 }
119 }
120 }
121
122 /// Run all after-hooks for the given phase and group.
123 pub(crate) fn run_after_group(&self, phase: Phase, group: GroupId, world: &mut World) {
124 if let Some(hooks) = self.after_group.get(&(phase, group)) {
125 for hook in hooks {
126 hook(world);
127 }
128 }
129 }
130
131 /// Register a hook to run before a phase for a specific group.
132 pub(crate) fn add_before_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
133 self.before_group
134 .entry((phase, group))
135 .or_default()
136 .push(hook);
137 }
138
139 /// Register a hook to run after a phase for a specific group.
140 pub(crate) fn add_after_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
141 self.after_group
142 .entry((phase, group))
143 .or_default()
144 .push(hook);
145 }
146}