Skip to main content

elevator_core/
sim.rs

1//! Top-level simulation runner and tick loop.
2//!
3//! # Essential API
4//!
5//! `Simulation` exposes a large surface, but most users only need the
6//! ~15 methods below, grouped by the order they appear in a typical
7//! game loop.
8//!
9//! ### Construction
10//!
11//! - [`SimulationBuilder::demo()`](crate::builder::SimulationBuilder::demo)
12//!   or [`SimulationBuilder::from_config()`](crate::builder::SimulationBuilder::from_config)
13//!   — fluent entry point; call [`.build()`](crate::builder::SimulationBuilder::build)
14//!   to get a `Simulation`.
15//! - [`Simulation::new()`](crate::sim::Simulation::new) — direct construction from
16//!   `&SimConfig` + a dispatch strategy.
17//!
18//! ### Per-tick driving
19//!
20//! - [`Simulation::step()`](crate::sim::Simulation::step) — run all 8 phases.
21//! - [`Simulation::current_tick()`](crate::sim::Simulation::current_tick) — the
22//!   current tick counter.
23//!
24//! ### Spawning and rerouting riders
25//!
26//! - [`Simulation::spawn_rider_by_stop_id()`](crate::sim::Simulation::spawn_rider_by_stop_id)
27//!   — simple origin/destination/weight spawn.
28//! - [`Simulation::build_rider_by_stop_id()`](crate::sim::Simulation::build_rider_by_stop_id)
29//!   — fluent [`RiderBuilder`](crate::sim::RiderBuilder) for patience, preferences, access
30//!   control, explicit groups, multi-leg routes.
31//! - [`Simulation::reroute()`](crate::sim::Simulation::reroute) — change a waiting
32//!   rider's destination mid-trip.
33//! - [`Simulation::settle_rider()`](crate::sim::Simulation::settle_rider) /
34//!   [`Simulation::despawn_rider()`](crate::sim::Simulation::despawn_rider) —
35//!   terminal-state cleanup for `Arrived`/`Abandoned` riders.
36//!
37//! ### Observability
38//!
39//! - [`Simulation::drain_events()`](crate::sim::Simulation::drain_events) — consume
40//!   the event stream emitted by the last tick.
41//! - [`Simulation::metrics()`](crate::sim::Simulation::metrics) — aggregate
42//!   wait/ride/throughput stats.
43//! - [`Simulation::waiting_at()`](crate::sim::Simulation::waiting_at) /
44//!   [`Simulation::residents_at()`](crate::sim::Simulation::residents_at) — O(1)
45//!   population queries by stop.
46//!
47//! ### Imperative control
48//!
49//! - [`Simulation::push_destination()`](crate::sim::Simulation::push_destination) /
50//!   [`Simulation::push_destination_front()`](crate::sim::Simulation::push_destination_front) /
51//!   [`Simulation::clear_destinations()`](crate::sim::Simulation::clear_destinations)
52//!   — override dispatch by pushing/clearing stops on an elevator's
53//!   [`DestinationQueue`](crate::components::DestinationQueue).
54//!
55//! ### Persistence
56//!
57//! - [`Simulation::snapshot()`](crate::sim::Simulation::snapshot) — capture full
58//!   state as a serializable [`WorldSnapshot`](crate::snapshot::WorldSnapshot).
59//! - [`WorldSnapshot::restore()`](crate::snapshot::WorldSnapshot::restore)
60//!   — rebuild a `Simulation` from a snapshot.
61//!
62//! Everything else (phase-runners, world-level accessors, energy, tag
63//! metrics, topology queries) is available for advanced use but is not
64//! required for the common case.
65
66mod construction;
67mod lifecycle;
68mod topology;
69
70use crate::components::{
71    AccessControl, FloorPosition, Orientation, Patience, Preferences, Rider, RiderPhase, Route,
72};
73use crate::dispatch::{BuiltinReposition, DispatchStrategy, ElevatorGroup, RepositionStrategy};
74use crate::entity::EntityId;
75use crate::error::SimError;
76use crate::events::{Event, EventBus};
77use crate::hooks::{Phase, PhaseHooks};
78use crate::ids::GroupId;
79use crate::metrics::Metrics;
80use crate::rider_index::RiderIndex;
81use crate::stop::StopId;
82use crate::systems::PhaseContext;
83use crate::time::TimeAdapter;
84use crate::topology::TopologyGraph;
85use crate::world::World;
86use std::collections::{BTreeMap, HashMap, HashSet};
87use std::fmt;
88use std::sync::Mutex;
89
90/// Parameters for creating a new elevator at runtime.
91#[derive(Debug, Clone)]
92pub struct ElevatorParams {
93    /// Maximum travel speed (distance/tick).
94    pub max_speed: f64,
95    /// Acceleration rate (distance/tick^2).
96    pub acceleration: f64,
97    /// Deceleration rate (distance/tick^2).
98    pub deceleration: f64,
99    /// Maximum weight the car can carry.
100    pub weight_capacity: f64,
101    /// Ticks for a door open/close transition.
102    pub door_transition_ticks: u32,
103    /// Ticks the door stays fully open.
104    pub door_open_ticks: u32,
105    /// Stop entity IDs this elevator cannot serve (access restriction).
106    pub restricted_stops: HashSet<EntityId>,
107    /// Speed multiplier for Inspection mode (0.0..1.0).
108    pub inspection_speed_factor: f64,
109}
110
111impl Default for ElevatorParams {
112    fn default() -> Self {
113        Self {
114            max_speed: 2.0,
115            acceleration: 1.5,
116            deceleration: 2.0,
117            weight_capacity: 800.0,
118            door_transition_ticks: 5,
119            door_open_ticks: 10,
120            restricted_stops: HashSet::new(),
121            inspection_speed_factor: 0.25,
122        }
123    }
124}
125
126/// Parameters for creating a new line at runtime.
127#[derive(Debug, Clone)]
128pub struct LineParams {
129    /// Human-readable name.
130    pub name: String,
131    /// Dispatch group to add this line to.
132    pub group: GroupId,
133    /// Physical orientation.
134    pub orientation: Orientation,
135    /// Lowest reachable position on the line axis.
136    pub min_position: f64,
137    /// Highest reachable position on the line axis.
138    pub max_position: f64,
139    /// Optional floor-plan position.
140    pub position: Option<FloorPosition>,
141    /// Maximum cars on this line (None = unlimited).
142    pub max_cars: Option<usize>,
143}
144
145impl LineParams {
146    /// Create line parameters with the given name and group, defaulting
147    /// everything else.
148    pub fn new(name: impl Into<String>, group: GroupId) -> Self {
149        Self {
150            name: name.into(),
151            group,
152            orientation: Orientation::default(),
153            min_position: 0.0,
154            max_position: 0.0,
155            position: None,
156            max_cars: None,
157        }
158    }
159}
160
161/// Fluent builder for spawning riders with optional configuration.
162///
163/// Created via [`Simulation::build_rider`] or [`Simulation::build_rider_by_stop_id`].
164///
165/// ```
166/// use elevator_core::prelude::*;
167///
168/// let mut sim = SimulationBuilder::demo().build().unwrap();
169/// let rider = sim.build_rider_by_stop_id(StopId(0), StopId(1))
170///     .unwrap()
171///     .weight(80.0)
172///     .spawn()
173///     .unwrap();
174/// ```
175pub struct RiderBuilder<'a> {
176    /// Mutable reference to the simulation (consumed on spawn).
177    sim: &'a mut Simulation,
178    /// Origin stop entity.
179    origin: EntityId,
180    /// Destination stop entity.
181    destination: EntityId,
182    /// Rider weight (default: 75.0).
183    weight: f64,
184    /// Explicit dispatch group (skips auto-detection).
185    group: Option<GroupId>,
186    /// Explicit multi-leg route.
187    route: Option<Route>,
188    /// Maximum wait ticks before abandoning.
189    patience: Option<u64>,
190    /// Boarding preferences.
191    preferences: Option<Preferences>,
192    /// Per-rider access control.
193    access_control: Option<AccessControl>,
194}
195
196impl RiderBuilder<'_> {
197    /// Set the rider's weight (default: 75.0).
198    #[must_use]
199    pub const fn weight(mut self, weight: f64) -> Self {
200        self.weight = weight;
201        self
202    }
203
204    /// Set the dispatch group explicitly, skipping auto-detection.
205    #[must_use]
206    pub const fn group(mut self, group: GroupId) -> Self {
207        self.group = Some(group);
208        self
209    }
210
211    /// Provide an explicit multi-leg route.
212    #[must_use]
213    pub fn route(mut self, route: Route) -> Self {
214        self.route = Some(route);
215        self
216    }
217
218    /// Set maximum wait ticks before the rider abandons.
219    #[must_use]
220    pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
221        self.patience = Some(max_wait_ticks);
222        self
223    }
224
225    /// Set boarding preferences.
226    #[must_use]
227    pub const fn preferences(mut self, prefs: Preferences) -> Self {
228        self.preferences = Some(prefs);
229        self
230    }
231
232    /// Set per-rider access control (allowed stops).
233    #[must_use]
234    pub fn access_control(mut self, ac: AccessControl) -> Self {
235        self.access_control = Some(ac);
236        self
237    }
238
239    /// Spawn the rider with the configured options.
240    ///
241    /// # Errors
242    ///
243    /// Returns [`SimError::NoRoute`] if no group serves both stops (when auto-detecting).
244    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops (when auto-detecting).
245    /// Returns [`SimError::GroupNotFound`] if an explicit group does not exist.
246    pub fn spawn(self) -> Result<EntityId, SimError> {
247        let route = if let Some(route) = self.route {
248            route
249        } else if let Some(group) = self.group {
250            if !self.sim.groups.iter().any(|g| g.id() == group) {
251                return Err(SimError::GroupNotFound(group));
252            }
253            Route::direct(self.origin, self.destination, group)
254        } else {
255            // Auto-detect group (same logic as spawn_rider).
256            let matching: Vec<GroupId> = self
257                .sim
258                .groups
259                .iter()
260                .filter(|g| {
261                    g.stop_entities().contains(&self.origin)
262                        && g.stop_entities().contains(&self.destination)
263                })
264                .map(ElevatorGroup::id)
265                .collect();
266
267            match matching.len() {
268                0 => {
269                    let origin_groups: Vec<GroupId> = self
270                        .sim
271                        .groups
272                        .iter()
273                        .filter(|g| g.stop_entities().contains(&self.origin))
274                        .map(ElevatorGroup::id)
275                        .collect();
276                    let destination_groups: Vec<GroupId> = self
277                        .sim
278                        .groups
279                        .iter()
280                        .filter(|g| g.stop_entities().contains(&self.destination))
281                        .map(ElevatorGroup::id)
282                        .collect();
283                    return Err(SimError::NoRoute {
284                        origin: self.origin,
285                        destination: self.destination,
286                        origin_groups,
287                        destination_groups,
288                    });
289                }
290                1 => Route::direct(self.origin, self.destination, matching[0]),
291                _ => {
292                    return Err(SimError::AmbiguousRoute {
293                        origin: self.origin,
294                        destination: self.destination,
295                        groups: matching,
296                    });
297                }
298            }
299        };
300
301        let eid = self
302            .sim
303            .spawn_rider_inner(self.origin, self.destination, self.weight, route);
304
305        // Apply optional components.
306        if let Some(max_wait) = self.patience {
307            self.sim.world.set_patience(
308                eid,
309                Patience {
310                    max_wait_ticks: max_wait,
311                    waited_ticks: 0,
312                },
313            );
314        }
315        if let Some(prefs) = self.preferences {
316            self.sim.world.set_preferences(eid, prefs);
317        }
318        if let Some(ac) = self.access_control {
319            self.sim.world.set_access_control(eid, ac);
320        }
321
322        Ok(eid)
323    }
324}
325
326/// The core simulation state, advanced by calling `step()`.
327pub struct Simulation {
328    /// The ECS world containing all entity data.
329    world: World,
330    /// Internal event bus — only holds events from the current tick.
331    events: EventBus,
332    /// Events from completed ticks, available to consumers via `drain_events()`.
333    pending_output: Vec<Event>,
334    /// Current simulation tick.
335    tick: u64,
336    /// Time delta per tick (seconds).
337    dt: f64,
338    /// Elevator groups in this simulation.
339    groups: Vec<ElevatorGroup>,
340    /// Config `StopId` to `EntityId` mapping for spawn helpers.
341    stop_lookup: HashMap<StopId, EntityId>,
342    /// Dispatch strategies keyed by group.
343    dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
344    /// Serializable strategy identifiers (for snapshot).
345    strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
346    /// Reposition strategies keyed by group (optional per group).
347    repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
348    /// Serializable reposition strategy identifiers (for snapshot).
349    reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
350    /// Aggregated metrics.
351    metrics: Metrics,
352    /// Time conversion utility.
353    time: TimeAdapter,
354    /// Lifecycle hooks (before/after each phase).
355    hooks: PhaseHooks,
356    /// Reusable buffer for elevator IDs (avoids per-tick allocation).
357    elevator_ids_buf: Vec<EntityId>,
358    /// Lazy-rebuilt connectivity graph for cross-line topology queries.
359    topo_graph: Mutex<TopologyGraph>,
360    /// Phase-partitioned reverse index for O(1) population queries.
361    rider_index: RiderIndex,
362}
363
364impl Simulation {
365    // ── Accessors ────────────────────────────────────────────────────
366
367    /// Get a shared reference to the world.
368    #[must_use]
369    pub const fn world(&self) -> &World {
370        &self.world
371    }
372
373    /// Get a mutable reference to the world.
374    ///
375    /// Exposed for advanced use cases (manual rider management, custom
376    /// component attachment). Prefer `spawn_rider` / `spawn_rider_by_stop_id`
377    /// for standard operations.
378    pub const fn world_mut(&mut self) -> &mut World {
379        &mut self.world
380    }
381
382    /// Current simulation tick.
383    #[must_use]
384    pub const fn current_tick(&self) -> u64 {
385        self.tick
386    }
387
388    /// Time delta per tick (seconds).
389    #[must_use]
390    pub const fn dt(&self) -> f64 {
391        self.dt
392    }
393
394    /// Get current simulation metrics.
395    #[must_use]
396    pub const fn metrics(&self) -> &Metrics {
397        &self.metrics
398    }
399
400    /// The time adapter for tick↔wall-clock conversion.
401    #[must_use]
402    pub const fn time(&self) -> &TimeAdapter {
403        &self.time
404    }
405
406    /// Get the elevator groups.
407    #[must_use]
408    pub fn groups(&self) -> &[ElevatorGroup] {
409        &self.groups
410    }
411
412    /// Resolve a config `StopId` to its runtime `EntityId`.
413    #[must_use]
414    pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
415        self.stop_lookup.get(&id).copied()
416    }
417
418    /// Get the strategy identifier for a group.
419    #[must_use]
420    pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
421        self.strategy_ids.get(&group)
422    }
423
424    /// Iterate over the stop ID → entity ID mapping.
425    pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
426        self.stop_lookup.iter()
427    }
428
429    /// Peek at events pending for consumer retrieval.
430    #[must_use]
431    pub fn pending_events(&self) -> &[Event] {
432        &self.pending_output
433    }
434
435    // ── Destination queue (imperative dispatch) ────────────────────
436
437    /// Read-only view of an elevator's destination queue (FIFO of target
438    /// stop `EntityId`s).
439    ///
440    /// Returns `None` if `elev` is not an elevator entity. Returns
441    /// `Some(&[])` for elevators with an empty queue.
442    #[must_use]
443    pub fn destination_queue(&self, elev: EntityId) -> Option<&[EntityId]> {
444        self.world
445            .destination_queue(elev)
446            .map(crate::components::DestinationQueue::queue)
447    }
448
449    /// Push a stop onto the back of an elevator's destination queue.
450    ///
451    /// Adjacent duplicates are suppressed: if the last entry already equals
452    /// `stop`, the queue is unchanged and no event is emitted.
453    /// Otherwise emits [`Event::DestinationQueued`].
454    ///
455    /// # Errors
456    ///
457    /// - [`SimError::InvalidState`] if `elev` is not an elevator.
458    /// - [`SimError::InvalidState`] if `stop` is not a stop.
459    pub fn push_destination(&mut self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
460        self.validate_push_targets(elev, stop)?;
461        let appended = self
462            .world
463            .destination_queue_mut(elev)
464            .is_some_and(|q| q.push_back(stop));
465        if appended {
466            self.events.emit(Event::DestinationQueued {
467                elevator: elev,
468                stop,
469                tick: self.tick,
470            });
471        }
472        Ok(())
473    }
474
475    /// Insert a stop at the front of an elevator's destination queue —
476    /// "go here next, before anything else in the queue".
477    ///
478    /// On the next `AdvanceQueue` phase (between Dispatch and Movement),
479    /// the elevator redirects to this new front if it differs from the
480    /// current target.
481    ///
482    /// Adjacent duplicates are suppressed: if the first entry already equals
483    /// `stop`, the queue is unchanged and no event is emitted.
484    ///
485    /// # Errors
486    ///
487    /// - [`SimError::InvalidState`] if `elev` is not an elevator.
488    /// - [`SimError::InvalidState`] if `stop` is not a stop.
489    pub fn push_destination_front(
490        &mut self,
491        elev: EntityId,
492        stop: EntityId,
493    ) -> Result<(), SimError> {
494        self.validate_push_targets(elev, stop)?;
495        let inserted = self
496            .world
497            .destination_queue_mut(elev)
498            .is_some_and(|q| q.push_front(stop));
499        if inserted {
500            self.events.emit(Event::DestinationQueued {
501                elevator: elev,
502                stop,
503                tick: self.tick,
504            });
505        }
506        Ok(())
507    }
508
509    /// Clear an elevator's destination queue.
510    ///
511    /// TODO: clearing does not currently abort an in-flight movement — the
512    /// elevator will finish its current leg and then go idle (since the
513    /// queue is empty). A future change can add a phase transition to
514    /// cancel mid-flight.
515    ///
516    /// # Errors
517    ///
518    /// Returns [`SimError::InvalidState`] if `elev` is not an elevator.
519    pub fn clear_destinations(&mut self, elev: EntityId) -> Result<(), SimError> {
520        if self.world.elevator(elev).is_none() {
521            return Err(SimError::InvalidState {
522                entity: elev,
523                reason: "not an elevator".into(),
524            });
525        }
526        if let Some(q) = self.world.destination_queue_mut(elev) {
527            q.clear();
528        }
529        Ok(())
530    }
531
532    /// Validate that `elev` is an elevator and `stop` is a stop.
533    fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
534        if self.world.elevator(elev).is_none() {
535            return Err(SimError::InvalidState {
536                entity: elev,
537                reason: "not an elevator".into(),
538            });
539        }
540        if self.world.stop(stop).is_none() {
541            return Err(SimError::InvalidState {
542                entity: stop,
543                reason: "not a stop".into(),
544            });
545        }
546        Ok(())
547    }
548
549    // Dispatch & reposition management live in `sim/construction.rs`.
550
551    // ── Tagging ──────────────────────────────────────────────────────
552
553    /// Attach a metric tag to an entity (rider, stop, elevator, etc.).
554    ///
555    /// Tags enable per-tag metric breakdowns. An entity can have multiple tags.
556    /// Riders automatically inherit tags from their origin stop when spawned.
557    pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) {
558        if let Some(tags) = self
559            .world
560            .resource_mut::<crate::tagged_metrics::MetricTags>()
561        {
562            tags.tag(id, tag);
563        }
564    }
565
566    /// Remove a metric tag from an entity.
567    pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
568        if let Some(tags) = self
569            .world
570            .resource_mut::<crate::tagged_metrics::MetricTags>()
571        {
572            tags.untag(id, tag);
573        }
574    }
575
576    /// Query the metric accumulator for a specific tag.
577    #[must_use]
578    pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
579        self.world
580            .resource::<crate::tagged_metrics::MetricTags>()
581            .and_then(|tags| tags.metric(tag))
582    }
583
584    /// List all registered metric tags.
585    pub fn all_tags(&self) -> Vec<&str> {
586        self.world
587            .resource::<crate::tagged_metrics::MetricTags>()
588            .map_or_else(Vec::new, |tags| tags.all_tags().collect())
589    }
590
591    // ── Rider spawning ───────────────────────────────────────────────
592
593    /// Create a rider builder for fluent rider spawning.
594    ///
595    /// ```
596    /// use elevator_core::prelude::*;
597    ///
598    /// let mut sim = SimulationBuilder::demo().build().unwrap();
599    /// let s0 = sim.stop_entity(StopId(0)).unwrap();
600    /// let s1 = sim.stop_entity(StopId(1)).unwrap();
601    /// let rider = sim.build_rider(s0, s1)
602    ///     .weight(80.0)
603    ///     .spawn()
604    ///     .unwrap();
605    /// ```
606    pub const fn build_rider(
607        &mut self,
608        origin: EntityId,
609        destination: EntityId,
610    ) -> RiderBuilder<'_> {
611        RiderBuilder {
612            sim: self,
613            origin,
614            destination,
615            weight: 75.0,
616            group: None,
617            route: None,
618            patience: None,
619            preferences: None,
620            access_control: None,
621        }
622    }
623
624    /// Create a rider builder using config `StopId`s.
625    ///
626    /// # Errors
627    ///
628    /// Returns [`SimError::StopNotFound`] if either stop ID is unknown.
629    ///
630    /// ```
631    /// use elevator_core::prelude::*;
632    ///
633    /// let mut sim = SimulationBuilder::demo().build().unwrap();
634    /// let rider = sim.build_rider_by_stop_id(StopId(0), StopId(1))
635    ///     .unwrap()
636    ///     .weight(80.0)
637    ///     .spawn()
638    ///     .unwrap();
639    /// ```
640    pub fn build_rider_by_stop_id(
641        &mut self,
642        origin: StopId,
643        destination: StopId,
644    ) -> Result<RiderBuilder<'_>, SimError> {
645        let origin_eid = self
646            .stop_lookup
647            .get(&origin)
648            .copied()
649            .ok_or(SimError::StopNotFound(origin))?;
650        let dest_eid = self
651            .stop_lookup
652            .get(&destination)
653            .copied()
654            .ok_or(SimError::StopNotFound(destination))?;
655        Ok(RiderBuilder {
656            sim: self,
657            origin: origin_eid,
658            destination: dest_eid,
659            weight: 75.0,
660            group: None,
661            route: None,
662            patience: None,
663            preferences: None,
664            access_control: None,
665        })
666    }
667
668    /// Spawn a rider at the given origin stop entity, headed to destination stop entity.
669    ///
670    /// Auto-detects the elevator group by finding groups that serve both origin
671    /// and destination stops.
672    ///
673    /// # Errors
674    ///
675    /// Returns [`SimError::NoRoute`] if no group serves both stops.
676    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
677    pub fn spawn_rider(
678        &mut self,
679        origin: EntityId,
680        destination: EntityId,
681        weight: f64,
682    ) -> Result<EntityId, SimError> {
683        let matching: Vec<GroupId> = self
684            .groups
685            .iter()
686            .filter(|g| {
687                g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
688            })
689            .map(ElevatorGroup::id)
690            .collect();
691
692        let group = match matching.len() {
693            0 => {
694                let origin_groups: Vec<GroupId> = self
695                    .groups
696                    .iter()
697                    .filter(|g| g.stop_entities().contains(&origin))
698                    .map(ElevatorGroup::id)
699                    .collect();
700                let destination_groups: Vec<GroupId> = self
701                    .groups
702                    .iter()
703                    .filter(|g| g.stop_entities().contains(&destination))
704                    .map(ElevatorGroup::id)
705                    .collect();
706                return Err(SimError::NoRoute {
707                    origin,
708                    destination,
709                    origin_groups,
710                    destination_groups,
711                });
712            }
713            1 => matching[0],
714            _ => {
715                return Err(SimError::AmbiguousRoute {
716                    origin,
717                    destination,
718                    groups: matching,
719                });
720            }
721        };
722
723        let route = Route::direct(origin, destination, group);
724        Ok(self.spawn_rider_inner(origin, destination, weight, route))
725    }
726
727    /// Spawn a rider with an explicit route.
728    ///
729    /// Same as [`spawn_rider`](Self::spawn_rider) but uses the provided route
730    /// instead of auto-detecting the group.
731    ///
732    /// # Errors
733    ///
734    /// Returns [`SimError::EntityNotFound`] if origin does not exist.
735    /// Returns [`SimError::InvalidState`] if origin doesn't match the route's
736    /// first leg `from`.
737    pub fn spawn_rider_with_route(
738        &mut self,
739        origin: EntityId,
740        destination: EntityId,
741        weight: f64,
742        route: Route,
743    ) -> Result<EntityId, SimError> {
744        if self.world.stop(origin).is_none() {
745            return Err(SimError::EntityNotFound(origin));
746        }
747        if let Some(leg) = route.current()
748            && leg.from != origin
749        {
750            return Err(SimError::InvalidState {
751                entity: origin,
752                reason: format!(
753                    "origin {origin:?} does not match route first leg from {:?}",
754                    leg.from
755                ),
756            });
757        }
758        Ok(self.spawn_rider_inner(origin, destination, weight, route))
759    }
760
761    /// Internal helper: spawn a rider entity with the given route.
762    fn spawn_rider_inner(
763        &mut self,
764        origin: EntityId,
765        destination: EntityId,
766        weight: f64,
767        route: Route,
768    ) -> EntityId {
769        let eid = self.world.spawn();
770        self.world.set_rider(
771            eid,
772            Rider {
773                weight,
774                phase: RiderPhase::Waiting,
775                current_stop: Some(origin),
776                spawn_tick: self.tick,
777                board_tick: None,
778            },
779        );
780        self.world.set_route(eid, route);
781        self.rider_index.insert_waiting(origin, eid);
782        self.events.emit(Event::RiderSpawned {
783            rider: eid,
784            origin,
785            destination,
786            tick: self.tick,
787        });
788
789        // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
790        let stop_tag = self
791            .world
792            .stop(origin)
793            .map(|s| format!("stop:{}", s.name()));
794
795        // Inherit metric tags from the origin stop.
796        if let Some(tags_res) = self
797            .world
798            .resource_mut::<crate::tagged_metrics::MetricTags>()
799        {
800            let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
801            for tag in origin_tags {
802                tags_res.tag(eid, tag);
803            }
804            // Apply the origin stop tag.
805            if let Some(tag) = stop_tag {
806                tags_res.tag(eid, tag);
807            }
808        }
809
810        eid
811    }
812
813    /// Convenience: spawn a rider by config `StopId`.
814    ///
815    /// Returns `Err` if either stop ID is not found.
816    ///
817    /// # Errors
818    ///
819    /// Returns [`SimError::StopNotFound`] if the origin or destination stop ID
820    /// is not in the building configuration.
821    ///
822    /// ```
823    /// use elevator_core::prelude::*;
824    ///
825    /// // Default builder has StopId(0) and StopId(1).
826    /// let mut sim = SimulationBuilder::demo().build().unwrap();
827    ///
828    /// let rider = sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 80.0).unwrap();
829    /// sim.step(); // metrics are updated during the tick
830    /// assert_eq!(sim.metrics().total_spawned(), 1);
831    /// ```
832    pub fn spawn_rider_by_stop_id(
833        &mut self,
834        origin: StopId,
835        destination: StopId,
836        weight: f64,
837    ) -> Result<EntityId, SimError> {
838        let origin_eid = self
839            .stop_lookup
840            .get(&origin)
841            .copied()
842            .ok_or(SimError::StopNotFound(origin))?;
843        let dest_eid = self
844            .stop_lookup
845            .get(&destination)
846            .copied()
847            .ok_or(SimError::StopNotFound(destination))?;
848        self.spawn_rider(origin_eid, dest_eid, weight)
849    }
850
851    /// Spawn a rider using a specific group for routing.
852    ///
853    /// Like [`spawn_rider`](Self::spawn_rider) but skips auto-detection —
854    /// uses the given group directly. Useful when the caller already knows
855    /// the group, or to resolve an [`AmbiguousRoute`](crate::error::SimError::AmbiguousRoute).
856    ///
857    /// # Errors
858    ///
859    /// Returns [`SimError::GroupNotFound`] if the group does not exist.
860    pub fn spawn_rider_in_group(
861        &mut self,
862        origin: EntityId,
863        destination: EntityId,
864        weight: f64,
865        group: GroupId,
866    ) -> Result<EntityId, SimError> {
867        if !self.groups.iter().any(|g| g.id() == group) {
868            return Err(SimError::GroupNotFound(group));
869        }
870        let route = Route::direct(origin, destination, group);
871        Ok(self.spawn_rider_inner(origin, destination, weight, route))
872    }
873
874    /// Convenience: spawn a rider by config `StopId` in a specific group.
875    ///
876    /// # Errors
877    ///
878    /// Returns [`SimError::StopNotFound`] if a stop ID is unknown, or
879    /// [`SimError::GroupNotFound`] if the group does not exist.
880    pub fn spawn_rider_in_group_by_stop_id(
881        &mut self,
882        origin: StopId,
883        destination: StopId,
884        weight: f64,
885        group: GroupId,
886    ) -> Result<EntityId, SimError> {
887        let origin_eid = self
888            .stop_lookup
889            .get(&origin)
890            .copied()
891            .ok_or(SimError::StopNotFound(origin))?;
892        let dest_eid = self
893            .stop_lookup
894            .get(&destination)
895            .copied()
896            .ok_or(SimError::StopNotFound(destination))?;
897        self.spawn_rider_in_group(origin_eid, dest_eid, weight, group)
898    }
899
900    /// Drain all pending events from completed ticks.
901    ///
902    /// Events emitted during `step()` (or per-phase methods) are buffered
903    /// and made available here after `advance_tick()` is called.
904    /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
905    /// are also included.
906    ///
907    /// ```
908    /// use elevator_core::prelude::*;
909    ///
910    /// let mut sim = SimulationBuilder::demo().build().unwrap();
911    ///
912    /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
913    /// sim.step();
914    ///
915    /// let events = sim.drain_events();
916    /// assert!(!events.is_empty());
917    /// ```
918    pub fn drain_events(&mut self) -> Vec<Event> {
919        // Flush any events still in the bus (from spawn_rider, disable, etc.)
920        self.pending_output.extend(self.events.drain());
921        std::mem::take(&mut self.pending_output)
922    }
923
924    /// Drain only events matching a predicate.
925    ///
926    /// Events that don't match the predicate remain in the buffer
927    /// and will be returned by future `drain_events` or
928    /// `drain_events_where` calls.
929    ///
930    /// ```
931    /// use elevator_core::prelude::*;
932    ///
933    /// let mut sim = SimulationBuilder::demo().build().unwrap();
934    /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
935    /// sim.step();
936    ///
937    /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
938    ///     matches!(e, Event::RiderSpawned { .. })
939    /// });
940    /// ```
941    pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
942        // Flush bus into pending_output first.
943        self.pending_output.extend(self.events.drain());
944
945        let mut matched = Vec::new();
946        let mut remaining = Vec::new();
947        for event in std::mem::take(&mut self.pending_output) {
948            if predicate(&event) {
949                matched.push(event);
950            } else {
951                remaining.push(event);
952            }
953        }
954        self.pending_output = remaining;
955        matched
956    }
957
958    // ── Sub-stepping ────────────────────────────────────────────────
959
960    /// Get the dispatch strategies map (for advanced sub-stepping).
961    #[must_use]
962    pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
963        &self.dispatchers
964    }
965
966    /// Get the dispatch strategies map mutably (for advanced sub-stepping).
967    pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
968        &mut self.dispatchers
969    }
970
971    /// Get a mutable reference to the event bus.
972    pub const fn events_mut(&mut self) -> &mut EventBus {
973        &mut self.events
974    }
975
976    /// Get a mutable reference to the metrics.
977    pub const fn metrics_mut(&mut self) -> &mut Metrics {
978        &mut self.metrics
979    }
980
981    /// Build the `PhaseContext` for the current tick.
982    #[must_use]
983    pub const fn phase_context(&self) -> PhaseContext {
984        PhaseContext {
985            tick: self.tick,
986            dt: self.dt,
987        }
988    }
989
990    /// Run only the `advance_transient` phase (with hooks).
991    pub fn run_advance_transient(&mut self) {
992        self.hooks
993            .run_before(Phase::AdvanceTransient, &mut self.world);
994        for group in &self.groups {
995            self.hooks
996                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
997        }
998        let ctx = self.phase_context();
999        crate::systems::advance_transient::run(
1000            &mut self.world,
1001            &mut self.events,
1002            &ctx,
1003            &mut self.rider_index,
1004        );
1005        for group in &self.groups {
1006            self.hooks
1007                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1008        }
1009        self.hooks
1010            .run_after(Phase::AdvanceTransient, &mut self.world);
1011    }
1012
1013    /// Run only the dispatch phase (with hooks).
1014    pub fn run_dispatch(&mut self) {
1015        self.hooks.run_before(Phase::Dispatch, &mut self.world);
1016        for group in &self.groups {
1017            self.hooks
1018                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
1019        }
1020        let ctx = self.phase_context();
1021        crate::systems::dispatch::run(
1022            &mut self.world,
1023            &mut self.events,
1024            &ctx,
1025            &self.groups,
1026            &mut self.dispatchers,
1027            &self.rider_index,
1028        );
1029        for group in &self.groups {
1030            self.hooks
1031                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
1032        }
1033        self.hooks.run_after(Phase::Dispatch, &mut self.world);
1034    }
1035
1036    /// Run only the movement phase (with hooks).
1037    pub fn run_movement(&mut self) {
1038        self.hooks.run_before(Phase::Movement, &mut self.world);
1039        for group in &self.groups {
1040            self.hooks
1041                .run_before_group(Phase::Movement, group.id(), &mut self.world);
1042        }
1043        let ctx = self.phase_context();
1044        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1045        crate::systems::movement::run(
1046            &mut self.world,
1047            &mut self.events,
1048            &ctx,
1049            &self.elevator_ids_buf,
1050            &mut self.metrics,
1051        );
1052        for group in &self.groups {
1053            self.hooks
1054                .run_after_group(Phase::Movement, group.id(), &mut self.world);
1055        }
1056        self.hooks.run_after(Phase::Movement, &mut self.world);
1057    }
1058
1059    /// Run only the doors phase (with hooks).
1060    pub fn run_doors(&mut self) {
1061        self.hooks.run_before(Phase::Doors, &mut self.world);
1062        for group in &self.groups {
1063            self.hooks
1064                .run_before_group(Phase::Doors, group.id(), &mut self.world);
1065        }
1066        let ctx = self.phase_context();
1067        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1068        crate::systems::doors::run(
1069            &mut self.world,
1070            &mut self.events,
1071            &ctx,
1072            &self.elevator_ids_buf,
1073        );
1074        for group in &self.groups {
1075            self.hooks
1076                .run_after_group(Phase::Doors, group.id(), &mut self.world);
1077        }
1078        self.hooks.run_after(Phase::Doors, &mut self.world);
1079    }
1080
1081    /// Run only the loading phase (with hooks).
1082    pub fn run_loading(&mut self) {
1083        self.hooks.run_before(Phase::Loading, &mut self.world);
1084        for group in &self.groups {
1085            self.hooks
1086                .run_before_group(Phase::Loading, group.id(), &mut self.world);
1087        }
1088        let ctx = self.phase_context();
1089        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1090        crate::systems::loading::run(
1091            &mut self.world,
1092            &mut self.events,
1093            &ctx,
1094            &self.elevator_ids_buf,
1095            &mut self.rider_index,
1096        );
1097        for group in &self.groups {
1098            self.hooks
1099                .run_after_group(Phase::Loading, group.id(), &mut self.world);
1100        }
1101        self.hooks.run_after(Phase::Loading, &mut self.world);
1102    }
1103
1104    /// Run only the advance-queue phase (with hooks).
1105    ///
1106    /// Reconciles each elevator's phase/target with the front of its
1107    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
1108    /// between Reposition and Movement.
1109    pub fn run_advance_queue(&mut self) {
1110        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
1111        for group in &self.groups {
1112            self.hooks
1113                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1114        }
1115        let ctx = self.phase_context();
1116        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1117        crate::systems::advance_queue::run(
1118            &mut self.world,
1119            &mut self.events,
1120            &ctx,
1121            &self.elevator_ids_buf,
1122        );
1123        for group in &self.groups {
1124            self.hooks
1125                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1126        }
1127        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
1128    }
1129
1130    /// Run only the reposition phase (with hooks).
1131    ///
1132    /// Only runs if at least one group has a [`RepositionStrategy`] configured.
1133    /// Idle elevators with no pending dispatch assignment are repositioned
1134    /// according to their group's strategy.
1135    pub fn run_reposition(&mut self) {
1136        if self.repositioners.is_empty() {
1137            return;
1138        }
1139        self.hooks.run_before(Phase::Reposition, &mut self.world);
1140        // Only run per-group hooks for groups that have a repositioner.
1141        for group in &self.groups {
1142            if self.repositioners.contains_key(&group.id()) {
1143                self.hooks
1144                    .run_before_group(Phase::Reposition, group.id(), &mut self.world);
1145            }
1146        }
1147        let ctx = self.phase_context();
1148        crate::systems::reposition::run(
1149            &mut self.world,
1150            &mut self.events,
1151            &ctx,
1152            &self.groups,
1153            &mut self.repositioners,
1154        );
1155        for group in &self.groups {
1156            if self.repositioners.contains_key(&group.id()) {
1157                self.hooks
1158                    .run_after_group(Phase::Reposition, group.id(), &mut self.world);
1159            }
1160        }
1161        self.hooks.run_after(Phase::Reposition, &mut self.world);
1162    }
1163
1164    /// Run the energy system (no hooks — inline phase).
1165    #[cfg(feature = "energy")]
1166    fn run_energy(&mut self) {
1167        let ctx = self.phase_context();
1168        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1169        crate::systems::energy::run(
1170            &mut self.world,
1171            &mut self.events,
1172            &ctx,
1173            &self.elevator_ids_buf,
1174        );
1175    }
1176
1177    /// Run only the metrics phase (with hooks).
1178    pub fn run_metrics(&mut self) {
1179        self.hooks.run_before(Phase::Metrics, &mut self.world);
1180        for group in &self.groups {
1181            self.hooks
1182                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
1183        }
1184        let ctx = self.phase_context();
1185        crate::systems::metrics::run(
1186            &mut self.world,
1187            &self.events,
1188            &mut self.metrics,
1189            &ctx,
1190            &self.groups,
1191        );
1192        for group in &self.groups {
1193            self.hooks
1194                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
1195        }
1196        self.hooks.run_after(Phase::Metrics, &mut self.world);
1197    }
1198
1199    // Phase-hook registration lives in `sim/construction.rs`.
1200
1201    /// Increment the tick counter and flush events to the output buffer.
1202    ///
1203    /// Call after running all desired phases. Events emitted during this tick
1204    /// are moved to the output buffer and available via `drain_events()`.
1205    pub fn advance_tick(&mut self) {
1206        self.pending_output.extend(self.events.drain());
1207        self.tick += 1;
1208    }
1209
1210    /// Advance the simulation by one tick.
1211    ///
1212    /// Events from this tick are buffered internally and available via
1213    /// `drain_events()`. The metrics system only processes events from
1214    /// the current tick, regardless of whether the consumer drains them.
1215    ///
1216    /// ```
1217    /// use elevator_core::prelude::*;
1218    ///
1219    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1220    /// sim.step();
1221    /// assert_eq!(sim.current_tick(), 1);
1222    /// ```
1223    pub fn step(&mut self) {
1224        self.run_advance_transient();
1225        self.run_dispatch();
1226        self.run_reposition();
1227        self.run_advance_queue();
1228        self.run_movement();
1229        self.run_doors();
1230        self.run_loading();
1231        #[cfg(feature = "energy")]
1232        self.run_energy();
1233        self.run_metrics();
1234        self.advance_tick();
1235    }
1236}
1237
1238impl fmt::Debug for Simulation {
1239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1240        f.debug_struct("Simulation")
1241            .field("tick", &self.tick)
1242            .field("dt", &self.dt)
1243            .field("groups", &self.groups.len())
1244            .field("entities", &self.world.entity_count())
1245            .finish_non_exhaustive()
1246    }
1247}