Skip to main content

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/// A boxed closure that receives mutable world access during a phase hook.
72pub(crate) type PhaseHook = Box<dyn Fn(&mut World) + Send + Sync>;
73
74/// Storage for before/after hooks keyed by phase.
75#[derive(Default)]
76pub(crate) struct PhaseHooks {
77    /// Hooks to run before each phase.
78    before: HashMap<Phase, Vec<PhaseHook>>,
79    /// Hooks to run after each phase.
80    after: HashMap<Phase, Vec<PhaseHook>>,
81    /// Hooks to run before a phase for a specific group.
82    before_group: HashMap<(Phase, GroupId), Vec<PhaseHook>>,
83    /// Hooks to run after a phase for a specific group.
84    after_group: HashMap<(Phase, GroupId), Vec<PhaseHook>>,
85}
86
87impl PhaseHooks {
88    /// Run all before-hooks for the given phase.
89    pub(crate) fn run_before(&self, phase: Phase, world: &mut World) {
90        if let Some(hooks) = self.before.get(&phase) {
91            for hook in hooks {
92                hook(world);
93            }
94            Self::debug_check_invariants(phase, world);
95        }
96    }
97
98    /// Run all after-hooks for the given phase.
99    pub(crate) fn run_after(&self, phase: Phase, world: &mut World) {
100        if let Some(hooks) = self.after.get(&phase) {
101            for hook in hooks {
102                hook(world);
103            }
104            Self::debug_check_invariants(phase, world);
105        }
106    }
107
108    /// In debug builds, verify that hooks did not break core invariants.
109    #[cfg(debug_assertions)]
110    fn debug_check_invariants(phase: Phase, world: &World) {
111        for (eid, _, elev) in world.iter_elevators() {
112            for &rider_id in &elev.riders {
113                debug_assert!(
114                    world.is_alive(rider_id),
115                    "hook after {phase:?}: elevator {eid:?} references dead rider {rider_id:?}"
116                );
117            }
118        }
119    }
120
121    #[cfg(not(debug_assertions))]
122    fn debug_check_invariants(_phase: Phase, _world: &World) {}
123
124    /// Register a hook to run before a phase.
125    pub(crate) fn add_before(&mut self, phase: Phase, hook: PhaseHook) {
126        self.before.entry(phase).or_default().push(hook);
127    }
128
129    /// Register a hook to run after a phase.
130    pub(crate) fn add_after(&mut self, phase: Phase, hook: PhaseHook) {
131        self.after.entry(phase).or_default().push(hook);
132    }
133
134    /// Run all before-hooks for the given phase and group.
135    pub(crate) fn run_before_group(&self, phase: Phase, group: GroupId, world: &mut World) {
136        if let Some(hooks) = self.before_group.get(&(phase, group)) {
137            for hook in hooks {
138                hook(world);
139            }
140        }
141    }
142
143    /// Run all after-hooks for the given phase and group.
144    pub(crate) fn run_after_group(&self, phase: Phase, group: GroupId, world: &mut World) {
145        if let Some(hooks) = self.after_group.get(&(phase, group)) {
146            for hook in hooks {
147                hook(world);
148            }
149        }
150    }
151
152    /// Register a hook to run before a phase for a specific group.
153    pub(crate) fn add_before_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
154        self.before_group
155            .entry((phase, group))
156            .or_default()
157            .push(hook);
158    }
159
160    /// Register a hook to run after a phase for a specific group.
161    pub(crate) fn add_after_group(&mut self, phase: Phase, group: GroupId, hook: PhaseHook) {
162        self.after_group
163            .entry((phase, group))
164            .or_default()
165            .push(hook);
166    }
167}