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.tick_in_progress = true;
69        self.sync_world_tick();
70        self.hooks
71            .run_before(Phase::AdvanceTransient, &mut self.world);
72        for group in &self.groups {
73            self.hooks
74                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
75        }
76        let ctx = self.phase_context();
77        crate::systems::advance_transient::run(
78            &mut self.world,
79            &mut self.events,
80            &ctx,
81            &mut self.rider_index,
82        );
83        for group in &self.groups {
84            self.hooks
85                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
86        }
87        self.hooks
88            .run_after(Phase::AdvanceTransient, &mut self.world);
89    }
90
91    /// Run only the dispatch phase (with hooks).
92    pub fn run_dispatch(&mut self) {
93        self.sync_world_tick();
94        self.hooks.run_before(Phase::Dispatch, &mut self.world);
95        for group in &self.groups {
96            self.hooks
97                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
98        }
99        let ctx = self.phase_context();
100        crate::systems::dispatch::run(
101            &mut self.world,
102            &mut self.events,
103            &ctx,
104            &self.groups,
105            &mut self.dispatchers,
106            &self.rider_index,
107        );
108        for group in &self.groups {
109            self.hooks
110                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
111        }
112        self.hooks.run_after(Phase::Dispatch, &mut self.world);
113    }
114
115    /// Run only the movement phase (with hooks).
116    pub fn run_movement(&mut self) {
117        self.hooks.run_before(Phase::Movement, &mut self.world);
118        for group in &self.groups {
119            self.hooks
120                .run_before_group(Phase::Movement, group.id(), &mut self.world);
121        }
122        let ctx = self.phase_context();
123        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
124        crate::systems::movement::run(
125            &mut self.world,
126            &mut self.events,
127            &ctx,
128            &self.elevator_ids_buf,
129            &mut self.metrics,
130        );
131        for group in &self.groups {
132            self.hooks
133                .run_after_group(Phase::Movement, group.id(), &mut self.world);
134        }
135        self.hooks.run_after(Phase::Movement, &mut self.world);
136    }
137
138    /// Run only the doors phase (with hooks).
139    pub fn run_doors(&mut self) {
140        self.hooks.run_before(Phase::Doors, &mut self.world);
141        for group in &self.groups {
142            self.hooks
143                .run_before_group(Phase::Doors, group.id(), &mut self.world);
144        }
145        let ctx = self.phase_context();
146        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
147        crate::systems::doors::run(
148            &mut self.world,
149            &mut self.events,
150            &ctx,
151            &self.elevator_ids_buf,
152        );
153        for group in &self.groups {
154            self.hooks
155                .run_after_group(Phase::Doors, group.id(), &mut self.world);
156        }
157        self.hooks.run_after(Phase::Doors, &mut self.world);
158    }
159
160    /// Run only the loading phase (with hooks).
161    pub fn run_loading(&mut self) {
162        self.hooks.run_before(Phase::Loading, &mut self.world);
163        for group in &self.groups {
164            self.hooks
165                .run_before_group(Phase::Loading, group.id(), &mut self.world);
166        }
167        let ctx = self.phase_context();
168        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
169        crate::systems::loading::run(
170            &mut self.world,
171            &mut self.events,
172            &ctx,
173            &self.elevator_ids_buf,
174            &mut self.rider_index,
175        );
176        for group in &self.groups {
177            self.hooks
178                .run_after_group(Phase::Loading, group.id(), &mut self.world);
179        }
180        self.hooks.run_after(Phase::Loading, &mut self.world);
181    }
182
183    /// Run only the advance-queue phase (with hooks).
184    ///
185    /// Reconciles each elevator's phase/target with the front of its
186    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
187    /// between Reposition and Movement.
188    pub fn run_advance_queue(&mut self) {
189        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
190        for group in &self.groups {
191            self.hooks
192                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
193        }
194        let ctx = self.phase_context();
195        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
196        crate::systems::advance_queue::run(
197            &mut self.world,
198            &mut self.events,
199            &ctx,
200            &self.elevator_ids_buf,
201        );
202        for group in &self.groups {
203            self.hooks
204                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
205        }
206        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
207    }
208
209    /// Run only the reposition phase (with hooks).
210    ///
211    /// Global before/after hooks always fire even when no
212    /// [`RepositionStrategy`](crate::dispatch::RepositionStrategy) is
213    /// configured. Per-group hooks only fire for groups that have a
214    /// repositioner — this differs from other phases where per-group hooks
215    /// fire unconditionally.
216    pub fn run_reposition(&mut self) {
217        self.sync_world_tick();
218        self.hooks.run_before(Phase::Reposition, &mut self.world);
219        if !self.repositioners.is_empty() {
220            // Only run per-group hooks for groups that have a repositioner.
221            for group in &self.groups {
222                if self.repositioners.contains_key(&group.id()) {
223                    self.hooks
224                        .run_before_group(Phase::Reposition, group.id(), &mut self.world);
225                }
226            }
227            let ctx = self.phase_context();
228            crate::systems::reposition::run(
229                &mut self.world,
230                &mut self.events,
231                &ctx,
232                &self.groups,
233                &mut self.repositioners,
234                &mut self.reposition_buf,
235            );
236            for group in &self.groups {
237                if self.repositioners.contains_key(&group.id()) {
238                    self.hooks
239                        .run_after_group(Phase::Reposition, group.id(), &mut self.world);
240                }
241            }
242        }
243        self.hooks.run_after(Phase::Reposition, &mut self.world);
244    }
245
246    /// Run the energy system (no hooks — inline phase).
247    #[cfg(feature = "energy")]
248    fn run_energy(&mut self) {
249        let ctx = self.phase_context();
250        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
251        crate::systems::energy::run(
252            &mut self.world,
253            &mut self.events,
254            &ctx,
255            &self.elevator_ids_buf,
256        );
257    }
258
259    /// Run only the metrics phase (with hooks).
260    pub fn run_metrics(&mut self) {
261        self.hooks.run_before(Phase::Metrics, &mut self.world);
262        for group in &self.groups {
263            self.hooks
264                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
265        }
266        let ctx = self.phase_context();
267        crate::systems::metrics::run(
268            &mut self.world,
269            &self.events,
270            &mut self.metrics,
271            &ctx,
272            &self.groups,
273        );
274        for group in &self.groups {
275            self.hooks
276                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
277        }
278        self.hooks.run_after(Phase::Metrics, &mut self.world);
279    }
280
281    // Phase-hook registration lives in `sim/construction.rs`.
282
283    /// Increment the tick counter and flush events to the output buffer.
284    ///
285    /// Call after running all desired phases. Events emitted during this tick
286    /// are moved to the output buffer and available via `drain_events()`.
287    pub fn advance_tick(&mut self) {
288        self.pending_output.extend(self.events.drain());
289        self.tick += 1;
290        self.tick_in_progress = false;
291        // Keep the `CurrentTick` world resource in lockstep after the tick
292        // counter advances; substep consumers driving phases manually
293        // will see the fresh value on their next call.
294        self.sync_world_tick();
295        // Drop arrival-log entries older than the configured retention.
296        // Unbounded growth would turn `arrivals_in_window` into an O(n)
297        // per-stop per-tick scan.
298        let retention = self
299            .world
300            .resource::<crate::arrival_log::ArrivalLogRetention>()
301            .copied()
302            .unwrap_or_default()
303            .0;
304        let cutoff = self.tick.saturating_sub(retention);
305        if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
306            log.prune_before(cutoff);
307        }
308    }
309
310    /// Mirror `self.tick` into the `CurrentTick` world resource so
311    /// phases that only have `&World` (reposition strategies, custom
312    /// consumers) can compute rolling-window queries without plumbing
313    /// `PhaseContext`. Called from `step()` and `advance_tick()` so
314    /// manual-phase callers stay in sync too.
315    fn sync_world_tick(&mut self) {
316        if let Some(ct) = self.world.resource_mut::<crate::arrival_log::CurrentTick>() {
317            ct.0 = self.tick;
318        }
319    }
320
321    /// Advance the simulation by one tick.
322    ///
323    /// Events from this tick are buffered internally and available via
324    /// `drain_events()`. The metrics system only processes events from
325    /// the current tick, regardless of whether the consumer drains them.
326    ///
327    /// ```
328    /// use elevator_core::prelude::*;
329    ///
330    /// let mut sim = SimulationBuilder::demo().build().unwrap();
331    /// sim.step();
332    /// assert_eq!(sim.current_tick(), 1);
333    /// ```
334    pub fn step(&mut self) {
335        self.sync_world_tick();
336        self.world.snapshot_prev_positions();
337        self.run_advance_transient();
338        self.run_dispatch();
339        self.run_reposition();
340        self.run_advance_queue();
341        self.run_movement();
342        self.run_doors();
343        self.run_loading();
344        #[cfg(feature = "energy")]
345        self.run_energy();
346        self.run_metrics();
347        self.advance_tick();
348    }
349}