Skip to main content

elevator_core/sim/
substep.rs

1//! Sub-tick stepping, phase hooks, event drainage.
2//!
3//! Part of the [`super::Simulation`] API surface; extracted from the
4//! monolithic `sim.rs` for readability. See the parent module for the
5//! overarching essential-API summary.
6
7use crate::dispatch::DispatchStrategy;
8use crate::events::EventBus;
9use crate::hooks::Phase;
10use crate::ids::GroupId;
11use crate::metrics::Metrics;
12use crate::systems::PhaseContext;
13use std::collections::BTreeMap;
14
15impl super::Simulation {
16    // ── Sub-stepping ────────────────────────────────────────────────
17
18    /// Get the dispatch strategies map (for advanced sub-stepping).
19    #[must_use]
20    pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
21        &self.dispatchers
22    }
23
24    /// Get the dispatch strategies map mutably (for advanced sub-stepping).
25    pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
26        &mut self.dispatchers
27    }
28
29    /// Get a mutable reference to the event bus.
30    pub const fn events_mut(&mut self) -> &mut EventBus {
31        &mut self.events
32    }
33
34    /// Get a mutable reference to the metrics.
35    pub const fn metrics_mut(&mut self) -> &mut Metrics {
36        &mut self.metrics
37    }
38
39    /// Build the `PhaseContext` for the current tick.
40    #[must_use]
41    pub const fn phase_context(&self) -> PhaseContext {
42        PhaseContext {
43            tick: self.tick,
44            dt: self.dt,
45        }
46    }
47
48    /// Run only the `advance_transient` phase (with hooks).
49    ///
50    /// # Phase ordering
51    ///
52    /// When calling individual phase methods instead of [`step()`](Self::step),
53    /// phases **must** be called in this order each tick:
54    ///
55    /// 1. `run_advance_transient`
56    /// 2. `run_dispatch`
57    /// 3. `run_reposition`
58    /// 4. `run_advance_queue`
59    /// 5. `run_movement`
60    /// 6. `run_doors`
61    /// 7. `run_loading`
62    /// 8. `run_metrics`
63    ///
64    /// Out-of-order execution may cause riders to board with closed doors,
65    /// elevators to move before dispatch, or transient states to persist
66    /// across tick boundaries.
67    pub fn run_advance_transient(&mut self) {
68        self.hooks
69            .run_before(Phase::AdvanceTransient, &mut self.world);
70        for group in &self.groups {
71            self.hooks
72                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
73        }
74        let ctx = self.phase_context();
75        crate::systems::advance_transient::run(
76            &mut self.world,
77            &mut self.events,
78            &ctx,
79            &mut self.rider_index,
80        );
81        for group in &self.groups {
82            self.hooks
83                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
84        }
85        self.hooks
86            .run_after(Phase::AdvanceTransient, &mut self.world);
87    }
88
89    /// Run only the dispatch phase (with hooks).
90    pub fn run_dispatch(&mut self) {
91        self.hooks.run_before(Phase::Dispatch, &mut self.world);
92        for group in &self.groups {
93            self.hooks
94                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
95        }
96        let ctx = self.phase_context();
97        crate::systems::dispatch::run(
98            &mut self.world,
99            &mut self.events,
100            &ctx,
101            &self.groups,
102            &mut self.dispatchers,
103            &self.rider_index,
104        );
105        for group in &self.groups {
106            self.hooks
107                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
108        }
109        self.hooks.run_after(Phase::Dispatch, &mut self.world);
110    }
111
112    /// Run only the movement phase (with hooks).
113    pub fn run_movement(&mut self) {
114        self.hooks.run_before(Phase::Movement, &mut self.world);
115        for group in &self.groups {
116            self.hooks
117                .run_before_group(Phase::Movement, group.id(), &mut self.world);
118        }
119        let ctx = self.phase_context();
120        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
121        crate::systems::movement::run(
122            &mut self.world,
123            &mut self.events,
124            &ctx,
125            &self.elevator_ids_buf,
126            &mut self.metrics,
127        );
128        for group in &self.groups {
129            self.hooks
130                .run_after_group(Phase::Movement, group.id(), &mut self.world);
131        }
132        self.hooks.run_after(Phase::Movement, &mut self.world);
133    }
134
135    /// Run only the doors phase (with hooks).
136    pub fn run_doors(&mut self) {
137        self.hooks.run_before(Phase::Doors, &mut self.world);
138        for group in &self.groups {
139            self.hooks
140                .run_before_group(Phase::Doors, group.id(), &mut self.world);
141        }
142        let ctx = self.phase_context();
143        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
144        crate::systems::doors::run(
145            &mut self.world,
146            &mut self.events,
147            &ctx,
148            &self.elevator_ids_buf,
149        );
150        for group in &self.groups {
151            self.hooks
152                .run_after_group(Phase::Doors, group.id(), &mut self.world);
153        }
154        self.hooks.run_after(Phase::Doors, &mut self.world);
155    }
156
157    /// Run only the loading phase (with hooks).
158    pub fn run_loading(&mut self) {
159        self.hooks.run_before(Phase::Loading, &mut self.world);
160        for group in &self.groups {
161            self.hooks
162                .run_before_group(Phase::Loading, group.id(), &mut self.world);
163        }
164        let ctx = self.phase_context();
165        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
166        crate::systems::loading::run(
167            &mut self.world,
168            &mut self.events,
169            &ctx,
170            &self.elevator_ids_buf,
171            &mut self.rider_index,
172        );
173        for group in &self.groups {
174            self.hooks
175                .run_after_group(Phase::Loading, group.id(), &mut self.world);
176        }
177        self.hooks.run_after(Phase::Loading, &mut self.world);
178    }
179
180    /// Run only the advance-queue phase (with hooks).
181    ///
182    /// Reconciles each elevator's phase/target with the front of its
183    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
184    /// between Reposition and Movement.
185    pub fn run_advance_queue(&mut self) {
186        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
187        for group in &self.groups {
188            self.hooks
189                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
190        }
191        let ctx = self.phase_context();
192        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
193        crate::systems::advance_queue::run(
194            &mut self.world,
195            &mut self.events,
196            &ctx,
197            &self.elevator_ids_buf,
198        );
199        for group in &self.groups {
200            self.hooks
201                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
202        }
203        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
204    }
205
206    /// Run only the reposition phase (with hooks).
207    ///
208    /// Global before/after hooks always fire even when no
209    /// [`RepositionStrategy`](crate::dispatch::RepositionStrategy) is
210    /// configured. Per-group hooks only fire for groups that have a
211    /// repositioner — this differs from other phases where per-group hooks
212    /// fire unconditionally.
213    pub fn run_reposition(&mut self) {
214        self.hooks.run_before(Phase::Reposition, &mut self.world);
215        if !self.repositioners.is_empty() {
216            // Only run per-group hooks for groups that have a repositioner.
217            for group in &self.groups {
218                if self.repositioners.contains_key(&group.id()) {
219                    self.hooks
220                        .run_before_group(Phase::Reposition, group.id(), &mut self.world);
221                }
222            }
223            let ctx = self.phase_context();
224            crate::systems::reposition::run(
225                &mut self.world,
226                &mut self.events,
227                &ctx,
228                &self.groups,
229                &mut self.repositioners,
230                &mut self.reposition_buf,
231            );
232            for group in &self.groups {
233                if self.repositioners.contains_key(&group.id()) {
234                    self.hooks
235                        .run_after_group(Phase::Reposition, group.id(), &mut self.world);
236                }
237            }
238        }
239        self.hooks.run_after(Phase::Reposition, &mut self.world);
240    }
241
242    /// Run the energy system (no hooks — inline phase).
243    #[cfg(feature = "energy")]
244    fn run_energy(&mut self) {
245        let ctx = self.phase_context();
246        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
247        crate::systems::energy::run(
248            &mut self.world,
249            &mut self.events,
250            &ctx,
251            &self.elevator_ids_buf,
252        );
253    }
254
255    /// Run only the metrics phase (with hooks).
256    pub fn run_metrics(&mut self) {
257        self.hooks.run_before(Phase::Metrics, &mut self.world);
258        for group in &self.groups {
259            self.hooks
260                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
261        }
262        let ctx = self.phase_context();
263        crate::systems::metrics::run(
264            &mut self.world,
265            &self.events,
266            &mut self.metrics,
267            &ctx,
268            &self.groups,
269        );
270        for group in &self.groups {
271            self.hooks
272                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
273        }
274        self.hooks.run_after(Phase::Metrics, &mut self.world);
275    }
276
277    // Phase-hook registration lives in `sim/construction.rs`.
278
279    /// Increment the tick counter and flush events to the output buffer.
280    ///
281    /// Call after running all desired phases. Events emitted during this tick
282    /// are moved to the output buffer and available via `drain_events()`.
283    pub fn advance_tick(&mut self) {
284        self.pending_output.extend(self.events.drain());
285        self.tick += 1;
286    }
287
288    /// Advance the simulation by one tick.
289    ///
290    /// Events from this tick are buffered internally and available via
291    /// `drain_events()`. The metrics system only processes events from
292    /// the current tick, regardless of whether the consumer drains them.
293    ///
294    /// ```
295    /// use elevator_core::prelude::*;
296    ///
297    /// let mut sim = SimulationBuilder::demo().build().unwrap();
298    /// sim.step();
299    /// assert_eq!(sim.current_tick(), 1);
300    /// ```
301    pub fn step(&mut self) {
302        self.world.snapshot_prev_positions();
303        self.run_advance_transient();
304        self.run_dispatch();
305        self.run_reposition();
306        self.run_advance_queue();
307        self.run_movement();
308        self.run_doors();
309        self.run_loading();
310        #[cfg(feature = "energy")]
311        self.run_energy();
312        self.run_metrics();
313        self.advance_tick();
314    }
315}