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