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
50impl std::fmt::Display for Phase {
51 /// ```
52 /// # use elevator_core::hooks::Phase;
53 /// assert_eq!(format!("{}", Phase::Dispatch), "dispatch");
54 /// assert_eq!(format!("{}", Phase::AdvanceTransient), "advance_transient");
55 /// ```
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 let s = match self {
58 Self::AdvanceTransient => "advance_transient",
59 Self::Dispatch => "dispatch",
60 Self::Movement => "movement",
61 Self::Doors => "doors",
62 Self::Loading => "loading",
63 Self::Reposition => "reposition",
64 Self::AdvanceQueue => "advance_queue",
65 Self::Metrics => "metrics",
66 };
67 f.write_str(s)
68 }
69}
70
71/// Whether a hook fires before or after its phase.
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
73pub(crate) enum When {
74 /// Fires immediately before the phase body runs.
75 Before,
76 /// Fires immediately after the phase body completes.
77 After,
78}
79
80/// A boxed closure that receives mutable world access during a phase hook.
81pub(crate) type PhaseHook = Box<dyn Fn(&mut World) + Send + Sync>;
82
83/// Storage for hooks keyed by `(phase, when, optional group)`.
84///
85/// Collapses what used to be four parallel `HashMap`s — `before`,
86/// `after`, `before_group`, `after_group` — into a single map keyed
87/// on the full hook scope. The four `add_*` / `run_*` methods are
88/// thin wrappers preserved for the `SimulationBuilder` shape.
89#[derive(Default)]
90pub(crate) struct PhaseHooks {
91 /// All registered hooks keyed on `(phase, when, optional group)`.
92 hooks: HashMap<(Phase, When, Option<GroupId>), Vec<PhaseHook>>,
93}
94
95impl PhaseHooks {
96 /// Append `hook` to the bucket identified by `(phase, when, group)`.
97 fn add(&mut self, phase: Phase, when: When, group: Option<GroupId>, hook: PhaseHook) {
98 self.hooks
99 .entry((phase, when, group))
100 .or_default()
101 .push(hook);
102 }
103
104 /// Invoke every hook registered for the given scope, in
105 /// registration order. Returns `true` when at least one hook ran;
106 /// the ungrouped wrappers use this to scope the debug invariant
107 /// check, preserving the original semantic of "only check when a
108 /// hook actually touched the world".
109 fn run(&self, phase: Phase, when: When, group: Option<GroupId>, world: &mut World) -> bool {
110 let Some(hooks) = self.hooks.get(&(phase, when, group)) else {
111 return false;
112 };
113 for hook in hooks {
114 hook(world);
115 }
116 true
117 }
118
119 /// Run all before-hooks for the given phase.
120 pub(crate) fn run_before(&self, phase: Phase, world: &mut World) {
121 if self.run(phase, When::Before, None, world) {
122 Self::debug_check_invariants(phase, world);
123 }
124 }
125
126 /// Run all after-hooks for the given phase.
127 pub(crate) fn run_after(&self, phase: Phase, world: &mut World) {
128 if self.run(phase, When::After, None, world) {
129 Self::debug_check_invariants(phase, world);
130 }
131 }
132
133 /// In debug builds, verify that hooks did not break core invariants.
134 #[cfg(debug_assertions)]
135 fn debug_check_invariants(phase: Phase, world: &World) {
136 for (eid, _, elev) in world.iter_elevators() {
137 for &rider_id in &elev.riders {
138 debug_assert!(
139 world.is_alive(rider_id),
140 "hook after {phase:?}: elevator {eid:?} references dead rider {rider_id:?}"
141 );
142 }
143 }
144 }
145
146 #[cfg(not(debug_assertions))]
147 fn debug_check_invariants(_phase: Phase, _world: &World) {}
148
149 /// Register a hook to run before a phase.
150 pub(crate) fn add_before(&mut self, phase: Phase, hook: PhaseHook) {
151 self.add(phase, When::Before, None, hook);
152 }
153
154 /// Register a hook to run after a phase.
155 pub(crate) fn add_after(&mut self, phase: Phase, hook: PhaseHook) {
156 self.add(phase, When::After, None, hook);
157 }
158
159 /// Run all before-hooks for the given phase and group.
160 pub(crate) fn run_before_group(&self, phase: Phase, group: GroupId, world: &mut World) {
161 self.run(phase, When::Before, Some(group), world);
162 }
163
164 /// Run all after-hooks for the given phase and group.
165 pub(crate) fn run_after_group(&self, phase: Phase, group: GroupId, world: &mut World) {
166 self.run(phase, When::After, Some(group), world);
167 }
168
169 /// Register a hook to run before a phase for a specific group.
170 pub(crate) fn add_before_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
171 self.add(phase, When::Before, Some(group), hook);
172 }
173
174 /// Register a hook to run after a phase for a specific group.
175 pub(crate) fn add_after_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
176 self.add(phase, When::After, Some(group), hook);
177 }
178}