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}