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 Velocity,
73};
74use crate::dispatch::{BuiltinReposition, DispatchStrategy, ElevatorGroup, RepositionStrategy};
75use crate::entity::EntityId;
76use crate::error::SimError;
77use crate::events::{Event, EventBus};
78use crate::hooks::{Phase, PhaseHooks};
79use crate::ids::GroupId;
80use crate::metrics::Metrics;
81use crate::rider_index::RiderIndex;
82use crate::stop::StopId;
83use crate::systems::PhaseContext;
84use crate::time::TimeAdapter;
85use crate::topology::TopologyGraph;
86use crate::world::World;
87use std::collections::{BTreeMap, HashMap, HashSet};
88use std::fmt;
89use std::sync::Mutex;
90use std::time::Duration;
91
92/// Parameters for creating a new elevator at runtime.
93#[derive(Debug, Clone)]
94pub struct ElevatorParams {
95 /// Maximum travel speed (distance/tick).
96 pub max_speed: f64,
97 /// Acceleration rate (distance/tick^2).
98 pub acceleration: f64,
99 /// Deceleration rate (distance/tick^2).
100 pub deceleration: f64,
101 /// Maximum weight the car can carry.
102 pub weight_capacity: f64,
103 /// Ticks for a door open/close transition.
104 pub door_transition_ticks: u32,
105 /// Ticks the door stays fully open.
106 pub door_open_ticks: u32,
107 /// Stop entity IDs this elevator cannot serve (access restriction).
108 pub restricted_stops: HashSet<EntityId>,
109 /// Speed multiplier for Inspection mode (0.0..1.0).
110 pub inspection_speed_factor: f64,
111}
112
113impl Default for ElevatorParams {
114 fn default() -> Self {
115 Self {
116 max_speed: 2.0,
117 acceleration: 1.5,
118 deceleration: 2.0,
119 weight_capacity: 800.0,
120 door_transition_ticks: 5,
121 door_open_ticks: 10,
122 restricted_stops: HashSet::new(),
123 inspection_speed_factor: 0.25,
124 }
125 }
126}
127
128/// Parameters for creating a new line at runtime.
129#[derive(Debug, Clone)]
130pub struct LineParams {
131 /// Human-readable name.
132 pub name: String,
133 /// Dispatch group to add this line to.
134 pub group: GroupId,
135 /// Physical orientation.
136 pub orientation: Orientation,
137 /// Lowest reachable position on the line axis.
138 pub min_position: f64,
139 /// Highest reachable position on the line axis.
140 pub max_position: f64,
141 /// Optional floor-plan position.
142 pub position: Option<FloorPosition>,
143 /// Maximum cars on this line (None = unlimited).
144 pub max_cars: Option<usize>,
145}
146
147impl LineParams {
148 /// Create line parameters with the given name and group, defaulting
149 /// everything else.
150 pub fn new(name: impl Into<String>, group: GroupId) -> Self {
151 Self {
152 name: name.into(),
153 group,
154 orientation: Orientation::default(),
155 min_position: 0.0,
156 max_position: 0.0,
157 position: None,
158 max_cars: None,
159 }
160 }
161}
162
163/// Fluent builder for spawning riders with optional configuration.
164///
165/// Created via [`Simulation::build_rider`] or [`Simulation::build_rider_by_stop_id`].
166///
167/// ```
168/// use elevator_core::prelude::*;
169///
170/// let mut sim = SimulationBuilder::demo().build().unwrap();
171/// let rider = sim.build_rider_by_stop_id(StopId(0), StopId(1))
172/// .unwrap()
173/// .weight(80.0)
174/// .spawn()
175/// .unwrap();
176/// ```
177pub struct RiderBuilder<'a> {
178 /// Mutable reference to the simulation (consumed on spawn).
179 sim: &'a mut Simulation,
180 /// Origin stop entity.
181 origin: EntityId,
182 /// Destination stop entity.
183 destination: EntityId,
184 /// Rider weight (default: 75.0).
185 weight: f64,
186 /// Explicit dispatch group (skips auto-detection).
187 group: Option<GroupId>,
188 /// Explicit multi-leg route.
189 route: Option<Route>,
190 /// Maximum wait ticks before abandoning.
191 patience: Option<u64>,
192 /// Boarding preferences.
193 preferences: Option<Preferences>,
194 /// Per-rider access control.
195 access_control: Option<AccessControl>,
196}
197
198impl RiderBuilder<'_> {
199 /// Set the rider's weight (default: 75.0).
200 #[must_use]
201 pub const fn weight(mut self, weight: f64) -> Self {
202 self.weight = weight;
203 self
204 }
205
206 /// Set the dispatch group explicitly, skipping auto-detection.
207 #[must_use]
208 pub const fn group(mut self, group: GroupId) -> Self {
209 self.group = Some(group);
210 self
211 }
212
213 /// Provide an explicit multi-leg route.
214 #[must_use]
215 pub fn route(mut self, route: Route) -> Self {
216 self.route = Some(route);
217 self
218 }
219
220 /// Set maximum wait ticks before the rider abandons.
221 #[must_use]
222 pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
223 self.patience = Some(max_wait_ticks);
224 self
225 }
226
227 /// Set boarding preferences.
228 #[must_use]
229 pub const fn preferences(mut self, prefs: Preferences) -> Self {
230 self.preferences = Some(prefs);
231 self
232 }
233
234 /// Set per-rider access control (allowed stops).
235 #[must_use]
236 pub fn access_control(mut self, ac: AccessControl) -> Self {
237 self.access_control = Some(ac);
238 self
239 }
240
241 /// Spawn the rider with the configured options.
242 ///
243 /// # Errors
244 ///
245 /// Returns [`SimError::NoRoute`] if no group serves both stops (when auto-detecting).
246 /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops (when auto-detecting).
247 /// Returns [`SimError::GroupNotFound`] if an explicit group does not exist.
248 pub fn spawn(self) -> Result<EntityId, SimError> {
249 let route = if let Some(route) = self.route {
250 route
251 } else if let Some(group) = self.group {
252 if !self.sim.groups.iter().any(|g| g.id() == group) {
253 return Err(SimError::GroupNotFound(group));
254 }
255 Route::direct(self.origin, self.destination, group)
256 } else {
257 // Auto-detect group (same logic as spawn_rider).
258 let matching: Vec<GroupId> = self
259 .sim
260 .groups
261 .iter()
262 .filter(|g| {
263 g.stop_entities().contains(&self.origin)
264 && g.stop_entities().contains(&self.destination)
265 })
266 .map(ElevatorGroup::id)
267 .collect();
268
269 match matching.len() {
270 0 => {
271 let origin_groups: Vec<GroupId> = self
272 .sim
273 .groups
274 .iter()
275 .filter(|g| g.stop_entities().contains(&self.origin))
276 .map(ElevatorGroup::id)
277 .collect();
278 let destination_groups: Vec<GroupId> = self
279 .sim
280 .groups
281 .iter()
282 .filter(|g| g.stop_entities().contains(&self.destination))
283 .map(ElevatorGroup::id)
284 .collect();
285 return Err(SimError::NoRoute {
286 origin: self.origin,
287 destination: self.destination,
288 origin_groups,
289 destination_groups,
290 });
291 }
292 1 => Route::direct(self.origin, self.destination, matching[0]),
293 _ => {
294 return Err(SimError::AmbiguousRoute {
295 origin: self.origin,
296 destination: self.destination,
297 groups: matching,
298 });
299 }
300 }
301 };
302
303 let eid = self
304 .sim
305 .spawn_rider_inner(self.origin, self.destination, self.weight, route);
306
307 // Apply optional components.
308 if let Some(max_wait) = self.patience {
309 self.sim.world.set_patience(
310 eid,
311 Patience {
312 max_wait_ticks: max_wait,
313 waited_ticks: 0,
314 },
315 );
316 }
317 if let Some(prefs) = self.preferences {
318 self.sim.world.set_preferences(eid, prefs);
319 }
320 if let Some(ac) = self.access_control {
321 self.sim.world.set_access_control(eid, ac);
322 }
323
324 Ok(eid)
325 }
326}
327
328/// The core simulation state, advanced by calling `step()`.
329pub struct Simulation {
330 /// The ECS world containing all entity data.
331 world: World,
332 /// Internal event bus — only holds events from the current tick.
333 events: EventBus,
334 /// Events from completed ticks, available to consumers via `drain_events()`.
335 pending_output: Vec<Event>,
336 /// Current simulation tick.
337 tick: u64,
338 /// Time delta per tick (seconds).
339 dt: f64,
340 /// Elevator groups in this simulation.
341 groups: Vec<ElevatorGroup>,
342 /// Config `StopId` to `EntityId` mapping for spawn helpers.
343 stop_lookup: HashMap<StopId, EntityId>,
344 /// Dispatch strategies keyed by group.
345 dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
346 /// Serializable strategy identifiers (for snapshot).
347 strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
348 /// Reposition strategies keyed by group (optional per group).
349 repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
350 /// Serializable reposition strategy identifiers (for snapshot).
351 reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
352 /// Aggregated metrics.
353 metrics: Metrics,
354 /// Time conversion utility.
355 time: TimeAdapter,
356 /// Lifecycle hooks (before/after each phase).
357 hooks: PhaseHooks,
358 /// Reusable buffer for elevator IDs (avoids per-tick allocation).
359 elevator_ids_buf: Vec<EntityId>,
360 /// Lazy-rebuilt connectivity graph for cross-line topology queries.
361 topo_graph: Mutex<TopologyGraph>,
362 /// Phase-partitioned reverse index for O(1) population queries.
363 rider_index: RiderIndex,
364}
365
366impl Simulation {
367 // ── Accessors ────────────────────────────────────────────────────
368
369 /// Get a shared reference to the world.
370 #[must_use]
371 pub const fn world(&self) -> &World {
372 &self.world
373 }
374
375 /// Get a mutable reference to the world.
376 ///
377 /// Exposed for advanced use cases (manual rider management, custom
378 /// component attachment). Prefer `spawn_rider` / `spawn_rider_by_stop_id`
379 /// for standard operations.
380 pub const fn world_mut(&mut self) -> &mut World {
381 &mut self.world
382 }
383
384 /// Current simulation tick.
385 #[must_use]
386 pub const fn current_tick(&self) -> u64 {
387 self.tick
388 }
389
390 /// Time delta per tick (seconds).
391 #[must_use]
392 pub const fn dt(&self) -> f64 {
393 self.dt
394 }
395
396 /// Interpolated position between the previous and current tick.
397 ///
398 /// `alpha` is clamped to `[0.0, 1.0]`, where `0.0` returns the entity's
399 /// position at the start of the last completed tick and `1.0` returns
400 /// the current position. Intended for smooth rendering when a render
401 /// frame falls between simulation ticks.
402 ///
403 /// Returns `None` if the entity has no position component. Returns the
404 /// current position unchanged if no previous snapshot exists (i.e. before
405 /// the first [`step`](Self::step)).
406 ///
407 /// [`step`]: Self::step
408 #[must_use]
409 pub fn position_at(&self, id: EntityId, alpha: f64) -> Option<f64> {
410 let current = self.world.position(id)?.value;
411 let alpha = if alpha.is_nan() {
412 0.0
413 } else {
414 alpha.clamp(0.0, 1.0)
415 };
416 let prev = self.world.prev_position(id).map_or(current, |p| p.value);
417 Some((current - prev).mul_add(alpha, prev))
418 }
419
420 /// Current velocity of an entity along the shaft axis (signed: +up, -down).
421 ///
422 /// Convenience wrapper over [`World::velocity`] that returns the raw
423 /// `f64` value. Returns `None` if the entity has no velocity component.
424 #[must_use]
425 pub fn velocity(&self, id: EntityId) -> Option<f64> {
426 self.world.velocity(id).map(Velocity::value)
427 }
428
429 /// Get current simulation metrics.
430 #[must_use]
431 pub const fn metrics(&self) -> &Metrics {
432 &self.metrics
433 }
434
435 /// The time adapter for tick↔wall-clock conversion.
436 #[must_use]
437 pub const fn time(&self) -> &TimeAdapter {
438 &self.time
439 }
440
441 /// Get the elevator groups.
442 #[must_use]
443 pub fn groups(&self) -> &[ElevatorGroup] {
444 &self.groups
445 }
446
447 /// Resolve a config `StopId` to its runtime `EntityId`.
448 #[must_use]
449 pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
450 self.stop_lookup.get(&id).copied()
451 }
452
453 /// Get the strategy identifier for a group.
454 #[must_use]
455 pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
456 self.strategy_ids.get(&group)
457 }
458
459 /// Iterate over the stop ID → entity ID mapping.
460 pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
461 self.stop_lookup.iter()
462 }
463
464 /// Peek at events pending for consumer retrieval.
465 #[must_use]
466 pub fn pending_events(&self) -> &[Event] {
467 &self.pending_output
468 }
469
470 // ── Destination queue (imperative dispatch) ────────────────────
471
472 /// Read-only view of an elevator's destination queue (FIFO of target
473 /// stop `EntityId`s).
474 ///
475 /// Returns `None` if `elev` is not an elevator entity. Returns
476 /// `Some(&[])` for elevators with an empty queue.
477 #[must_use]
478 pub fn destination_queue(&self, elev: EntityId) -> Option<&[EntityId]> {
479 self.world
480 .destination_queue(elev)
481 .map(crate::components::DestinationQueue::queue)
482 }
483
484 /// Push a stop onto the back of an elevator's destination queue.
485 ///
486 /// Adjacent duplicates are suppressed: if the last entry already equals
487 /// `stop`, the queue is unchanged and no event is emitted.
488 /// Otherwise emits [`Event::DestinationQueued`].
489 ///
490 /// # Errors
491 ///
492 /// - [`SimError::InvalidState`] if `elev` is not an elevator.
493 /// - [`SimError::InvalidState`] if `stop` is not a stop.
494 pub fn push_destination(&mut self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
495 self.validate_push_targets(elev, stop)?;
496 let appended = self
497 .world
498 .destination_queue_mut(elev)
499 .is_some_and(|q| q.push_back(stop));
500 if appended {
501 self.events.emit(Event::DestinationQueued {
502 elevator: elev,
503 stop,
504 tick: self.tick,
505 });
506 }
507 Ok(())
508 }
509
510 /// Insert a stop at the front of an elevator's destination queue —
511 /// "go here next, before anything else in the queue".
512 ///
513 /// On the next `AdvanceQueue` phase (between Dispatch and Movement),
514 /// the elevator redirects to this new front if it differs from the
515 /// current target.
516 ///
517 /// Adjacent duplicates are suppressed: if the first entry already equals
518 /// `stop`, the queue is unchanged and no event is emitted.
519 ///
520 /// # Errors
521 ///
522 /// - [`SimError::InvalidState`] if `elev` is not an elevator.
523 /// - [`SimError::InvalidState`] if `stop` is not a stop.
524 pub fn push_destination_front(
525 &mut self,
526 elev: EntityId,
527 stop: EntityId,
528 ) -> Result<(), SimError> {
529 self.validate_push_targets(elev, stop)?;
530 let inserted = self
531 .world
532 .destination_queue_mut(elev)
533 .is_some_and(|q| q.push_front(stop));
534 if inserted {
535 self.events.emit(Event::DestinationQueued {
536 elevator: elev,
537 stop,
538 tick: self.tick,
539 });
540 }
541 Ok(())
542 }
543
544 /// Clear an elevator's destination queue.
545 ///
546 /// TODO: clearing does not currently abort an in-flight movement — the
547 /// elevator will finish its current leg and then go idle (since the
548 /// queue is empty). A future change can add a phase transition to
549 /// cancel mid-flight.
550 ///
551 /// # Errors
552 ///
553 /// Returns [`SimError::InvalidState`] if `elev` is not an elevator.
554 pub fn clear_destinations(&mut self, elev: EntityId) -> Result<(), SimError> {
555 if self.world.elevator(elev).is_none() {
556 return Err(SimError::InvalidState {
557 entity: elev,
558 reason: "not an elevator".into(),
559 });
560 }
561 if let Some(q) = self.world.destination_queue_mut(elev) {
562 q.clear();
563 }
564 Ok(())
565 }
566
567 /// Validate that `elev` is an elevator and `stop` is a stop.
568 fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
569 if self.world.elevator(elev).is_none() {
570 return Err(SimError::InvalidState {
571 entity: elev,
572 reason: "not an elevator".into(),
573 });
574 }
575 if self.world.stop(stop).is_none() {
576 return Err(SimError::InvalidState {
577 entity: stop,
578 reason: "not a stop".into(),
579 });
580 }
581 Ok(())
582 }
583
584 // ── ETA queries ─────────────────────────────────────────────────
585
586 /// Estimated time until `elev` arrives at `stop`, summing closed-form
587 /// trapezoidal travel time for every leg up to (and including) the leg
588 /// that ends at `stop`, plus the door dwell at every *intermediate* stop.
589 ///
590 /// "Arrival" is the moment the door cycle begins at `stop` — door time
591 /// at `stop` itself is **not** added; door time at earlier stops along
592 /// the route **is**.
593 ///
594 /// Returns `None` if:
595 /// - `elev` is not an elevator or `stop` is not a stop,
596 /// - the elevator's [`ServiceMode`](crate::components::ServiceMode) is
597 /// dispatch-excluded (`Manual` / `Independent`), or
598 /// - `stop` is neither the elevator's current movement target nor anywhere
599 /// in its [`destination_queue`](Self::destination_queue).
600 ///
601 /// The estimate is best-effort. It assumes the queue is served in order
602 /// with no mid-trip insertions; dispatch decisions, manual door commands,
603 /// and rider boarding/exiting beyond the configured dwell will perturb
604 /// the actual arrival.
605 #[must_use]
606 pub fn eta(&self, elev: EntityId, stop: EntityId) -> Option<Duration> {
607 let elevator = self.world.elevator(elev)?;
608 self.world.stop(stop)?;
609 let svc = self.world.service_mode(elev).copied().unwrap_or_default();
610 if svc.is_dispatch_excluded() {
611 return None;
612 }
613
614 // Build the route in service order: current target first (if any),
615 // then queue entries, with adjacent duplicates collapsed.
616 let mut route: Vec<EntityId> = Vec::new();
617 if let Some(t) = elevator.phase().moving_target() {
618 route.push(t);
619 }
620 if let Some(q) = self.world.destination_queue(elev) {
621 for &s in q.queue() {
622 if route.last() != Some(&s) {
623 route.push(s);
624 }
625 }
626 }
627 if !route.contains(&stop) {
628 return None;
629 }
630
631 let max_speed = elevator.max_speed();
632 let accel = elevator.acceleration();
633 let decel = elevator.deceleration();
634 let door_cycle_ticks =
635 u64::from(elevator.door_transition_ticks()) * 2 + u64::from(elevator.door_open_ticks());
636 let door_cycle_secs = (door_cycle_ticks as f64) * self.dt;
637
638 // Account for any in-progress door cycle before the first travel leg:
639 // the elevator is parked at its current stop and won't move until the
640 // door FSM returns to Closed.
641 let mut total = match elevator.door() {
642 crate::door::DoorState::Opening {
643 ticks_remaining,
644 open_duration,
645 close_duration,
646 } => f64::from(*ticks_remaining + *open_duration + *close_duration) * self.dt,
647 crate::door::DoorState::Open {
648 ticks_remaining,
649 close_duration,
650 } => f64::from(*ticks_remaining + *close_duration) * self.dt,
651 crate::door::DoorState::Closing { ticks_remaining } => {
652 f64::from(*ticks_remaining) * self.dt
653 }
654 crate::door::DoorState::Closed => 0.0,
655 };
656
657 let in_door_cycle = !matches!(elevator.door(), crate::door::DoorState::Closed);
658 let mut pos = self.world.position(elev)?.value;
659 let vel_signed = self.world.velocity(elev).map_or(0.0, Velocity::value);
660
661 for (idx, &s) in route.iter().enumerate() {
662 let Some(s_pos) = self.world.stop_position(s) else {
663 // A queued entry without a position can only mean the stop
664 // entity was despawned out from under us. Bail rather than
665 // returning a partial accumulation that would silently
666 // understate the ETA.
667 return None;
668 };
669 let dist = (s_pos - pos).abs();
670 // Only the first leg can carry initial velocity, and only if
671 // the car is already moving toward this stop and not stuck in
672 // a door cycle (which forces it to stop first).
673 let v0 = if idx == 0 && !in_door_cycle && vel_signed.abs() > f64::EPSILON {
674 let dir = (s_pos - pos).signum();
675 if dir * vel_signed > 0.0 {
676 vel_signed.abs()
677 } else {
678 0.0
679 }
680 } else {
681 0.0
682 };
683 total += crate::eta::travel_time(dist, v0, max_speed, accel, decel);
684 if s == stop {
685 return Some(Duration::from_secs_f64(total.max(0.0)));
686 }
687 total += door_cycle_secs;
688 pos = s_pos;
689 }
690 // `route.contains(&stop)` was true above, so the loop must hit `stop`.
691 // Fall through to `None` as a defensive backstop.
692 None
693 }
694
695 /// Best ETA to `stop` across all dispatch-eligible elevators, optionally
696 /// filtered by indicator-lamp [`Direction`](crate::components::Direction).
697 ///
698 /// Pass [`Direction::Either`](crate::components::Direction::Either) to
699 /// consider every car. Otherwise, only cars whose committed direction is
700 /// `Either` or matches the requested direction are considered — useful
701 /// for hall-call assignment ("which up-going car arrives first?").
702 ///
703 /// Returns the entity ID of the winning elevator and its ETA, or `None`
704 /// if no eligible car has `stop` queued.
705 #[must_use]
706 pub fn best_eta(
707 &self,
708 stop: EntityId,
709 direction: crate::components::Direction,
710 ) -> Option<(EntityId, Duration)> {
711 use crate::components::Direction;
712 self.world
713 .iter_elevators()
714 .filter_map(|(eid, _, elev)| {
715 let car_dir = elev.direction();
716 let direction_ok = match direction {
717 Direction::Either => true,
718 requested => car_dir == Direction::Either || car_dir == requested,
719 };
720 if !direction_ok {
721 return None;
722 }
723 self.eta(eid, stop).map(|d| (eid, d))
724 })
725 .min_by_key(|(_, d)| *d)
726 }
727
728 // ── Runtime elevator upgrades ────────────────────────────────────
729 //
730 // Games that want to mutate elevator parameters at runtime (e.g.
731 // an RPG speed-upgrade purchase, a scripted capacity boost) go
732 // through these setters rather than poking `Elevator` directly via
733 // `world_mut()`. Each setter validates its input, updates the
734 // underlying component, and emits an [`Event::ElevatorUpgraded`]
735 // so game code can react without polling.
736 //
737 // ### Semantics
738 //
739 // - `max_speed`, `acceleration`, `deceleration`: applied on the next
740 // movement integration step. The car's **current velocity is
741 // preserved** — there is no instantaneous jerk. If `max_speed`
742 // is lowered below the current velocity, the movement integrator
743 // clamps velocity to the new cap on the next tick.
744 // - `weight_capacity`: applied immediately. If the new capacity is
745 // below `current_load` the car ends up temporarily overweight —
746 // no riders are ejected, but the next boarding pass will reject
747 // any rider that would push the load further over the new cap.
748 // - `door_transition_ticks`, `door_open_ticks`: applied on the
749 // **next** door cycle. An in-progress door transition keeps its
750 // original timing, so setters never cause visual glitches.
751
752 /// Set the maximum travel speed for an elevator at runtime.
753 ///
754 /// The new value applies on the next movement integration step;
755 /// the car's current velocity is preserved (see the
756 /// [runtime upgrades section](crate#runtime-upgrades) of the crate
757 /// docs). If the new cap is below the current velocity, the movement
758 /// system clamps velocity down on the next tick.
759 ///
760 /// # Errors
761 ///
762 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
763 /// - [`SimError::InvalidConfig`] if `speed` is not a positive finite number.
764 ///
765 /// # Example
766 ///
767 /// ```
768 /// use elevator_core::prelude::*;
769 ///
770 /// let mut sim = SimulationBuilder::demo().build().unwrap();
771 /// let elev = sim.world().iter_elevators().next().unwrap().0;
772 /// sim.set_max_speed(elev, 4.0).unwrap();
773 /// assert_eq!(sim.world().elevator(elev).unwrap().max_speed(), 4.0);
774 /// ```
775 pub fn set_max_speed(&mut self, elevator: EntityId, speed: f64) -> Result<(), SimError> {
776 Self::validate_positive_finite_f64(speed, "elevators.max_speed")?;
777 let old = self.require_elevator(elevator)?.max_speed;
778 if let Some(car) = self.world.elevator_mut(elevator) {
779 car.max_speed = speed;
780 }
781 self.emit_upgrade(
782 elevator,
783 crate::events::UpgradeField::MaxSpeed,
784 crate::events::UpgradeValue::float(old),
785 crate::events::UpgradeValue::float(speed),
786 );
787 Ok(())
788 }
789
790 /// Set the acceleration rate for an elevator at runtime.
791 ///
792 /// See [`set_max_speed`](Self::set_max_speed) for the general
793 /// velocity-preservation rules that apply to kinematic setters.
794 ///
795 /// # Errors
796 ///
797 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
798 /// - [`SimError::InvalidConfig`] if `accel` is not a positive finite number.
799 ///
800 /// # Example
801 ///
802 /// ```
803 /// use elevator_core::prelude::*;
804 ///
805 /// let mut sim = SimulationBuilder::demo().build().unwrap();
806 /// let elev = sim.world().iter_elevators().next().unwrap().0;
807 /// sim.set_acceleration(elev, 3.0).unwrap();
808 /// assert_eq!(sim.world().elevator(elev).unwrap().acceleration(), 3.0);
809 /// ```
810 pub fn set_acceleration(&mut self, elevator: EntityId, accel: f64) -> Result<(), SimError> {
811 Self::validate_positive_finite_f64(accel, "elevators.acceleration")?;
812 let old = self.require_elevator(elevator)?.acceleration;
813 if let Some(car) = self.world.elevator_mut(elevator) {
814 car.acceleration = accel;
815 }
816 self.emit_upgrade(
817 elevator,
818 crate::events::UpgradeField::Acceleration,
819 crate::events::UpgradeValue::float(old),
820 crate::events::UpgradeValue::float(accel),
821 );
822 Ok(())
823 }
824
825 /// Set the deceleration rate for an elevator at runtime.
826 ///
827 /// See [`set_max_speed`](Self::set_max_speed) for the general
828 /// velocity-preservation rules that apply to kinematic setters.
829 ///
830 /// # Errors
831 ///
832 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
833 /// - [`SimError::InvalidConfig`] if `decel` is not a positive finite number.
834 ///
835 /// # Example
836 ///
837 /// ```
838 /// use elevator_core::prelude::*;
839 ///
840 /// let mut sim = SimulationBuilder::demo().build().unwrap();
841 /// let elev = sim.world().iter_elevators().next().unwrap().0;
842 /// sim.set_deceleration(elev, 3.5).unwrap();
843 /// assert_eq!(sim.world().elevator(elev).unwrap().deceleration(), 3.5);
844 /// ```
845 pub fn set_deceleration(&mut self, elevator: EntityId, decel: f64) -> Result<(), SimError> {
846 Self::validate_positive_finite_f64(decel, "elevators.deceleration")?;
847 let old = self.require_elevator(elevator)?.deceleration;
848 if let Some(car) = self.world.elevator_mut(elevator) {
849 car.deceleration = decel;
850 }
851 self.emit_upgrade(
852 elevator,
853 crate::events::UpgradeField::Deceleration,
854 crate::events::UpgradeValue::float(old),
855 crate::events::UpgradeValue::float(decel),
856 );
857 Ok(())
858 }
859
860 /// Set the weight capacity for an elevator at runtime.
861 ///
862 /// Applied immediately. If the new capacity is below the car's
863 /// current load the car is temporarily overweight; no riders are
864 /// ejected, but subsequent boarding attempts that would push load
865 /// further over the cap will be rejected as
866 /// [`RejectionReason::OverCapacity`](crate::error::RejectionReason::OverCapacity).
867 ///
868 /// # Errors
869 ///
870 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
871 /// - [`SimError::InvalidConfig`] if `capacity` is not a positive finite number.
872 ///
873 /// # Example
874 ///
875 /// ```
876 /// use elevator_core::prelude::*;
877 ///
878 /// let mut sim = SimulationBuilder::demo().build().unwrap();
879 /// let elev = sim.world().iter_elevators().next().unwrap().0;
880 /// sim.set_weight_capacity(elev, 1200.0).unwrap();
881 /// assert_eq!(sim.world().elevator(elev).unwrap().weight_capacity(), 1200.0);
882 /// ```
883 pub fn set_weight_capacity(
884 &mut self,
885 elevator: EntityId,
886 capacity: f64,
887 ) -> Result<(), SimError> {
888 Self::validate_positive_finite_f64(capacity, "elevators.weight_capacity")?;
889 let old = self.require_elevator(elevator)?.weight_capacity;
890 if let Some(car) = self.world.elevator_mut(elevator) {
891 car.weight_capacity = capacity;
892 }
893 self.emit_upgrade(
894 elevator,
895 crate::events::UpgradeField::WeightCapacity,
896 crate::events::UpgradeValue::float(old),
897 crate::events::UpgradeValue::float(capacity),
898 );
899 Ok(())
900 }
901
902 /// Set the door open/close transition duration for an elevator.
903 ///
904 /// Applied on the **next** door cycle — an in-progress transition
905 /// keeps its original timing to avoid visual glitches.
906 ///
907 /// # Errors
908 ///
909 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
910 /// - [`SimError::InvalidConfig`] if `ticks` is zero.
911 ///
912 /// # Example
913 ///
914 /// ```
915 /// use elevator_core::prelude::*;
916 ///
917 /// let mut sim = SimulationBuilder::demo().build().unwrap();
918 /// let elev = sim.world().iter_elevators().next().unwrap().0;
919 /// sim.set_door_transition_ticks(elev, 3).unwrap();
920 /// assert_eq!(sim.world().elevator(elev).unwrap().door_transition_ticks(), 3);
921 /// ```
922 pub fn set_door_transition_ticks(
923 &mut self,
924 elevator: EntityId,
925 ticks: u32,
926 ) -> Result<(), SimError> {
927 Self::validate_nonzero_u32(ticks, "elevators.door_transition_ticks")?;
928 let old = self.require_elevator(elevator)?.door_transition_ticks;
929 if let Some(car) = self.world.elevator_mut(elevator) {
930 car.door_transition_ticks = ticks;
931 }
932 self.emit_upgrade(
933 elevator,
934 crate::events::UpgradeField::DoorTransitionTicks,
935 crate::events::UpgradeValue::ticks(old),
936 crate::events::UpgradeValue::ticks(ticks),
937 );
938 Ok(())
939 }
940
941 /// Set how long doors hold fully open for an elevator.
942 ///
943 /// Applied on the **next** door cycle — a door that is currently
944 /// holding open will complete its original dwell before the new
945 /// value takes effect.
946 ///
947 /// # Errors
948 ///
949 /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
950 /// - [`SimError::InvalidConfig`] if `ticks` is zero.
951 ///
952 /// # Example
953 ///
954 /// ```
955 /// use elevator_core::prelude::*;
956 ///
957 /// let mut sim = SimulationBuilder::demo().build().unwrap();
958 /// let elev = sim.world().iter_elevators().next().unwrap().0;
959 /// sim.set_door_open_ticks(elev, 20).unwrap();
960 /// assert_eq!(sim.world().elevator(elev).unwrap().door_open_ticks(), 20);
961 /// ```
962 pub fn set_door_open_ticks(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
963 Self::validate_nonzero_u32(ticks, "elevators.door_open_ticks")?;
964 let old = self.require_elevator(elevator)?.door_open_ticks;
965 if let Some(car) = self.world.elevator_mut(elevator) {
966 car.door_open_ticks = ticks;
967 }
968 self.emit_upgrade(
969 elevator,
970 crate::events::UpgradeField::DoorOpenTicks,
971 crate::events::UpgradeValue::ticks(old),
972 crate::events::UpgradeValue::ticks(ticks),
973 );
974 Ok(())
975 }
976
977 // ── Manual door control ──────────────────────────────────────────
978 //
979 // These methods let games drive door state directly — e.g. a
980 // cab-panel open/close button in a first-person game, or an RPG
981 // where the player *is* the elevator and decides when to cycle doors.
982 //
983 // Each method either applies the command immediately (if the car is
984 // in a matching door-FSM state) or queues it on the elevator for
985 // application at the next valid moment. This way games can call
986 // these any time without worrying about FSM timing, and get a clean
987 // success/failure split between "bad entity" and "bad moment".
988
989 /// Request the doors to open.
990 ///
991 /// Applied immediately if the car is stopped at a stop with closed
992 /// or closing doors; otherwise queued until the car next arrives.
993 /// A no-op if the doors are already open or opening.
994 ///
995 /// # Errors
996 ///
997 /// - [`SimError::InvalidState`] if `elevator` is not an elevator
998 /// entity or is disabled.
999 ///
1000 /// # Example
1001 ///
1002 /// ```
1003 /// use elevator_core::prelude::*;
1004 ///
1005 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1006 /// let elev = sim.world().iter_elevators().next().unwrap().0;
1007 /// sim.request_door_open(elev).unwrap();
1008 /// ```
1009 pub fn request_door_open(&mut self, elevator: EntityId) -> Result<(), SimError> {
1010 self.require_enabled_elevator(elevator)?;
1011 self.enqueue_door_command(elevator, crate::door::DoorCommand::Open);
1012 Ok(())
1013 }
1014
1015 /// Request the doors to close now.
1016 ///
1017 /// Applied immediately if the doors are open or loading — forcing an
1018 /// early close — unless a rider is mid-boarding/exiting this car, in
1019 /// which case the close waits for the rider to finish. If doors are
1020 /// currently opening, the close queues and fires once fully open.
1021 ///
1022 /// # Errors
1023 ///
1024 /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1025 /// entity or is disabled.
1026 ///
1027 /// # Example
1028 ///
1029 /// ```
1030 /// use elevator_core::prelude::*;
1031 ///
1032 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1033 /// let elev = sim.world().iter_elevators().next().unwrap().0;
1034 /// sim.request_door_close(elev).unwrap();
1035 /// ```
1036 pub fn request_door_close(&mut self, elevator: EntityId) -> Result<(), SimError> {
1037 self.require_enabled_elevator(elevator)?;
1038 self.enqueue_door_command(elevator, crate::door::DoorCommand::Close);
1039 Ok(())
1040 }
1041
1042 /// Extend the doors' open dwell by `ticks`.
1043 ///
1044 /// Cumulative — two calls of 30 ticks each extend the dwell by 60
1045 /// ticks in total. If the doors aren't open yet, the hold is queued
1046 /// and applied when they next reach the fully-open state.
1047 ///
1048 /// # Errors
1049 ///
1050 /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1051 /// entity or is disabled.
1052 /// - [`SimError::InvalidConfig`] if `ticks` is zero.
1053 ///
1054 /// # Example
1055 ///
1056 /// ```
1057 /// use elevator_core::prelude::*;
1058 ///
1059 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1060 /// let elev = sim.world().iter_elevators().next().unwrap().0;
1061 /// sim.hold_door_open(elev, 30).unwrap();
1062 /// ```
1063 pub fn hold_door_open(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
1064 Self::validate_nonzero_u32(ticks, "hold_door_open.ticks")?;
1065 self.require_enabled_elevator(elevator)?;
1066 self.enqueue_door_command(elevator, crate::door::DoorCommand::HoldOpen { ticks });
1067 Ok(())
1068 }
1069
1070 /// Cancel any pending hold extension.
1071 ///
1072 /// If the base open timer has already elapsed the doors close on
1073 /// the next doors-phase tick.
1074 ///
1075 /// # Errors
1076 ///
1077 /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1078 /// entity or is disabled.
1079 ///
1080 /// # Example
1081 ///
1082 /// ```
1083 /// use elevator_core::prelude::*;
1084 ///
1085 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1086 /// let elev = sim.world().iter_elevators().next().unwrap().0;
1087 /// sim.hold_door_open(elev, 100).unwrap();
1088 /// sim.cancel_door_hold(elev).unwrap();
1089 /// ```
1090 pub fn cancel_door_hold(&mut self, elevator: EntityId) -> Result<(), SimError> {
1091 self.require_enabled_elevator(elevator)?;
1092 self.enqueue_door_command(elevator, crate::door::DoorCommand::CancelHold);
1093 Ok(())
1094 }
1095
1096 /// Set the target velocity for a manual-mode elevator.
1097 ///
1098 /// The velocity is clamped to the elevator's `[-max_speed, max_speed]`
1099 /// range after validation. The car ramps toward the target each tick
1100 /// using `acceleration` (speeding up, or starting from rest) or
1101 /// `deceleration` (slowing down, or reversing direction). Positive
1102 /// values command upward travel, negative values command downward travel.
1103 ///
1104 /// # Errors
1105 /// - Entity is not an elevator, or is disabled.
1106 /// - Elevator is not in [`ServiceMode::Manual`].
1107 /// - `velocity` is not finite (NaN or infinite).
1108 ///
1109 /// [`ServiceMode::Manual`]: crate::components::ServiceMode::Manual
1110 pub fn set_target_velocity(
1111 &mut self,
1112 elevator: EntityId,
1113 velocity: f64,
1114 ) -> Result<(), SimError> {
1115 self.require_enabled_elevator(elevator)?;
1116 self.require_manual_mode(elevator)?;
1117 if !velocity.is_finite() {
1118 return Err(SimError::InvalidConfig {
1119 field: "target_velocity",
1120 reason: format!("must be finite, got {velocity}"),
1121 });
1122 }
1123 let max = self
1124 .world
1125 .elevator(elevator)
1126 .map_or(f64::INFINITY, |c| c.max_speed);
1127 let clamped = velocity.clamp(-max, max);
1128 if let Some(car) = self.world.elevator_mut(elevator) {
1129 car.manual_target_velocity = Some(clamped);
1130 }
1131 self.events.emit(Event::ManualVelocityCommanded {
1132 elevator,
1133 target_velocity: Some(ordered_float::OrderedFloat(clamped)),
1134 tick: self.tick,
1135 });
1136 Ok(())
1137 }
1138
1139 /// Command an immediate stop on a manual-mode elevator.
1140 ///
1141 /// Sets the target velocity to zero; the car decelerates at its
1142 /// configured `deceleration` rate. Equivalent to
1143 /// `set_target_velocity(elevator, 0.0)` but emits a distinct
1144 /// [`Event::ManualVelocityCommanded`] with `None` payload so games can
1145 /// distinguish an emergency stop from a deliberate hold.
1146 ///
1147 /// # Errors
1148 /// Same as [`set_target_velocity`](Self::set_target_velocity), minus
1149 /// the finite-velocity check.
1150 pub fn emergency_stop(&mut self, elevator: EntityId) -> Result<(), SimError> {
1151 self.require_enabled_elevator(elevator)?;
1152 self.require_manual_mode(elevator)?;
1153 if let Some(car) = self.world.elevator_mut(elevator) {
1154 car.manual_target_velocity = Some(0.0);
1155 }
1156 self.events.emit(Event::ManualVelocityCommanded {
1157 elevator,
1158 target_velocity: None,
1159 tick: self.tick,
1160 });
1161 Ok(())
1162 }
1163
1164 /// Internal: require an elevator be in `ServiceMode::Manual`.
1165 fn require_manual_mode(&self, elevator: EntityId) -> Result<(), SimError> {
1166 let is_manual = self
1167 .world
1168 .service_mode(elevator)
1169 .is_some_and(|m| *m == crate::components::ServiceMode::Manual);
1170 if !is_manual {
1171 return Err(SimError::InvalidState {
1172 entity: elevator,
1173 reason: "elevator is not in ServiceMode::Manual".into(),
1174 });
1175 }
1176 Ok(())
1177 }
1178
1179 /// Internal: push a command onto the queue, collapsing adjacent
1180 /// duplicates, capping length, and emitting `DoorCommandQueued`.
1181 fn enqueue_door_command(&mut self, elevator: EntityId, command: crate::door::DoorCommand) {
1182 if let Some(car) = self.world.elevator_mut(elevator) {
1183 let q = &mut car.door_command_queue;
1184 // Collapse adjacent duplicates for idempotent commands
1185 // (Open/Close/CancelHold) — repeating them adds nothing.
1186 // HoldOpen is explicitly cumulative, so never collapsed.
1187 let collapse = matches!(
1188 command,
1189 crate::door::DoorCommand::Open
1190 | crate::door::DoorCommand::Close
1191 | crate::door::DoorCommand::CancelHold
1192 ) && q.last().copied() == Some(command);
1193 if !collapse {
1194 q.push(command);
1195 if q.len() > crate::components::DOOR_COMMAND_QUEUE_CAP {
1196 q.remove(0);
1197 }
1198 }
1199 }
1200 self.events.emit(Event::DoorCommandQueued {
1201 elevator,
1202 command,
1203 tick: self.tick,
1204 });
1205 }
1206
1207 /// Internal: resolve an elevator entity that is not disabled.
1208 fn require_enabled_elevator(&self, elevator: EntityId) -> Result<(), SimError> {
1209 if self.world.elevator(elevator).is_none() {
1210 return Err(SimError::InvalidState {
1211 entity: elevator,
1212 reason: "not an elevator".into(),
1213 });
1214 }
1215 if self.world.is_disabled(elevator) {
1216 return Err(SimError::InvalidState {
1217 entity: elevator,
1218 reason: "elevator is disabled".into(),
1219 });
1220 }
1221 Ok(())
1222 }
1223
1224 /// Internal: resolve an elevator entity or return a clear error.
1225 fn require_elevator(
1226 &self,
1227 elevator: EntityId,
1228 ) -> Result<&crate::components::Elevator, SimError> {
1229 self.world
1230 .elevator(elevator)
1231 .ok_or_else(|| SimError::InvalidState {
1232 entity: elevator,
1233 reason: "not an elevator".into(),
1234 })
1235 }
1236
1237 /// Internal: positive-finite validator matching the construction-time
1238 /// error shape in `sim/construction.rs::validate_elevator_config`.
1239 fn validate_positive_finite_f64(value: f64, field: &'static str) -> Result<(), SimError> {
1240 if !value.is_finite() {
1241 return Err(SimError::InvalidConfig {
1242 field,
1243 reason: format!("must be finite, got {value}"),
1244 });
1245 }
1246 if value <= 0.0 {
1247 return Err(SimError::InvalidConfig {
1248 field,
1249 reason: format!("must be positive, got {value}"),
1250 });
1251 }
1252 Ok(())
1253 }
1254
1255 /// Internal: reject zero-tick timings.
1256 fn validate_nonzero_u32(value: u32, field: &'static str) -> Result<(), SimError> {
1257 if value == 0 {
1258 return Err(SimError::InvalidConfig {
1259 field,
1260 reason: "must be > 0".into(),
1261 });
1262 }
1263 Ok(())
1264 }
1265
1266 /// Internal: emit a single `ElevatorUpgraded` event for the current tick.
1267 fn emit_upgrade(
1268 &mut self,
1269 elevator: EntityId,
1270 field: crate::events::UpgradeField,
1271 old: crate::events::UpgradeValue,
1272 new: crate::events::UpgradeValue,
1273 ) {
1274 self.events.emit(Event::ElevatorUpgraded {
1275 elevator,
1276 field,
1277 old,
1278 new,
1279 tick: self.tick,
1280 });
1281 }
1282
1283 // Dispatch & reposition management live in `sim/construction.rs`.
1284
1285 // ── Tagging ──────────────────────────────────────────────────────
1286
1287 /// Attach a metric tag to an entity (rider, stop, elevator, etc.).
1288 ///
1289 /// Tags enable per-tag metric breakdowns. An entity can have multiple tags.
1290 /// Riders automatically inherit tags from their origin stop when spawned.
1291 pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) {
1292 if let Some(tags) = self
1293 .world
1294 .resource_mut::<crate::tagged_metrics::MetricTags>()
1295 {
1296 tags.tag(id, tag);
1297 }
1298 }
1299
1300 /// Remove a metric tag from an entity.
1301 pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
1302 if let Some(tags) = self
1303 .world
1304 .resource_mut::<crate::tagged_metrics::MetricTags>()
1305 {
1306 tags.untag(id, tag);
1307 }
1308 }
1309
1310 /// Query the metric accumulator for a specific tag.
1311 #[must_use]
1312 pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
1313 self.world
1314 .resource::<crate::tagged_metrics::MetricTags>()
1315 .and_then(|tags| tags.metric(tag))
1316 }
1317
1318 /// List all registered metric tags.
1319 pub fn all_tags(&self) -> Vec<&str> {
1320 self.world
1321 .resource::<crate::tagged_metrics::MetricTags>()
1322 .map_or_else(Vec::new, |tags| tags.all_tags().collect())
1323 }
1324
1325 // ── Rider spawning ───────────────────────────────────────────────
1326
1327 /// Create a rider builder for fluent rider spawning.
1328 ///
1329 /// ```
1330 /// use elevator_core::prelude::*;
1331 ///
1332 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1333 /// let s0 = sim.stop_entity(StopId(0)).unwrap();
1334 /// let s1 = sim.stop_entity(StopId(1)).unwrap();
1335 /// let rider = sim.build_rider(s0, s1)
1336 /// .weight(80.0)
1337 /// .spawn()
1338 /// .unwrap();
1339 /// ```
1340 pub const fn build_rider(
1341 &mut self,
1342 origin: EntityId,
1343 destination: EntityId,
1344 ) -> RiderBuilder<'_> {
1345 RiderBuilder {
1346 sim: self,
1347 origin,
1348 destination,
1349 weight: 75.0,
1350 group: None,
1351 route: None,
1352 patience: None,
1353 preferences: None,
1354 access_control: None,
1355 }
1356 }
1357
1358 /// Create a rider builder using config `StopId`s.
1359 ///
1360 /// # Errors
1361 ///
1362 /// Returns [`SimError::StopNotFound`] if either stop ID is unknown.
1363 ///
1364 /// ```
1365 /// use elevator_core::prelude::*;
1366 ///
1367 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1368 /// let rider = sim.build_rider_by_stop_id(StopId(0), StopId(1))
1369 /// .unwrap()
1370 /// .weight(80.0)
1371 /// .spawn()
1372 /// .unwrap();
1373 /// ```
1374 pub fn build_rider_by_stop_id(
1375 &mut self,
1376 origin: StopId,
1377 destination: StopId,
1378 ) -> Result<RiderBuilder<'_>, SimError> {
1379 let origin_eid = self
1380 .stop_lookup
1381 .get(&origin)
1382 .copied()
1383 .ok_or(SimError::StopNotFound(origin))?;
1384 let dest_eid = self
1385 .stop_lookup
1386 .get(&destination)
1387 .copied()
1388 .ok_or(SimError::StopNotFound(destination))?;
1389 Ok(RiderBuilder {
1390 sim: self,
1391 origin: origin_eid,
1392 destination: dest_eid,
1393 weight: 75.0,
1394 group: None,
1395 route: None,
1396 patience: None,
1397 preferences: None,
1398 access_control: None,
1399 })
1400 }
1401
1402 /// Spawn a rider at the given origin stop entity, headed to destination stop entity.
1403 ///
1404 /// Auto-detects the elevator group by finding groups that serve both origin
1405 /// and destination stops.
1406 ///
1407 /// # Errors
1408 ///
1409 /// Returns [`SimError::NoRoute`] if no group serves both stops.
1410 /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
1411 pub fn spawn_rider(
1412 &mut self,
1413 origin: EntityId,
1414 destination: EntityId,
1415 weight: f64,
1416 ) -> Result<EntityId, SimError> {
1417 let matching: Vec<GroupId> = self
1418 .groups
1419 .iter()
1420 .filter(|g| {
1421 g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
1422 })
1423 .map(ElevatorGroup::id)
1424 .collect();
1425
1426 let group = match matching.len() {
1427 0 => {
1428 let origin_groups: Vec<GroupId> = self
1429 .groups
1430 .iter()
1431 .filter(|g| g.stop_entities().contains(&origin))
1432 .map(ElevatorGroup::id)
1433 .collect();
1434 let destination_groups: Vec<GroupId> = self
1435 .groups
1436 .iter()
1437 .filter(|g| g.stop_entities().contains(&destination))
1438 .map(ElevatorGroup::id)
1439 .collect();
1440 return Err(SimError::NoRoute {
1441 origin,
1442 destination,
1443 origin_groups,
1444 destination_groups,
1445 });
1446 }
1447 1 => matching[0],
1448 _ => {
1449 return Err(SimError::AmbiguousRoute {
1450 origin,
1451 destination,
1452 groups: matching,
1453 });
1454 }
1455 };
1456
1457 let route = Route::direct(origin, destination, group);
1458 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1459 }
1460
1461 /// Spawn a rider with an explicit route.
1462 ///
1463 /// Same as [`spawn_rider`](Self::spawn_rider) but uses the provided route
1464 /// instead of auto-detecting the group.
1465 ///
1466 /// # Errors
1467 ///
1468 /// Returns [`SimError::EntityNotFound`] if origin does not exist.
1469 /// Returns [`SimError::InvalidState`] if origin doesn't match the route's
1470 /// first leg `from`.
1471 pub fn spawn_rider_with_route(
1472 &mut self,
1473 origin: EntityId,
1474 destination: EntityId,
1475 weight: f64,
1476 route: Route,
1477 ) -> Result<EntityId, SimError> {
1478 if self.world.stop(origin).is_none() {
1479 return Err(SimError::EntityNotFound(origin));
1480 }
1481 if let Some(leg) = route.current()
1482 && leg.from != origin
1483 {
1484 return Err(SimError::InvalidState {
1485 entity: origin,
1486 reason: format!(
1487 "origin {origin:?} does not match route first leg from {:?}",
1488 leg.from
1489 ),
1490 });
1491 }
1492 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1493 }
1494
1495 /// Internal helper: spawn a rider entity with the given route.
1496 fn spawn_rider_inner(
1497 &mut self,
1498 origin: EntityId,
1499 destination: EntityId,
1500 weight: f64,
1501 route: Route,
1502 ) -> EntityId {
1503 let eid = self.world.spawn();
1504 self.world.set_rider(
1505 eid,
1506 Rider {
1507 weight,
1508 phase: RiderPhase::Waiting,
1509 current_stop: Some(origin),
1510 spawn_tick: self.tick,
1511 board_tick: None,
1512 },
1513 );
1514 self.world.set_route(eid, route);
1515 self.rider_index.insert_waiting(origin, eid);
1516 self.events.emit(Event::RiderSpawned {
1517 rider: eid,
1518 origin,
1519 destination,
1520 tick: self.tick,
1521 });
1522
1523 // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
1524 let stop_tag = self
1525 .world
1526 .stop(origin)
1527 .map(|s| format!("stop:{}", s.name()));
1528
1529 // Inherit metric tags from the origin stop.
1530 if let Some(tags_res) = self
1531 .world
1532 .resource_mut::<crate::tagged_metrics::MetricTags>()
1533 {
1534 let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
1535 for tag in origin_tags {
1536 tags_res.tag(eid, tag);
1537 }
1538 // Apply the origin stop tag.
1539 if let Some(tag) = stop_tag {
1540 tags_res.tag(eid, tag);
1541 }
1542 }
1543
1544 eid
1545 }
1546
1547 /// Convenience: spawn a rider by config `StopId`.
1548 ///
1549 /// Returns `Err` if either stop ID is not found.
1550 ///
1551 /// # Errors
1552 ///
1553 /// Returns [`SimError::StopNotFound`] if the origin or destination stop ID
1554 /// is not in the building configuration.
1555 ///
1556 /// ```
1557 /// use elevator_core::prelude::*;
1558 ///
1559 /// // Default builder has StopId(0) and StopId(1).
1560 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1561 ///
1562 /// let rider = sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 80.0).unwrap();
1563 /// sim.step(); // metrics are updated during the tick
1564 /// assert_eq!(sim.metrics().total_spawned(), 1);
1565 /// ```
1566 pub fn spawn_rider_by_stop_id(
1567 &mut self,
1568 origin: StopId,
1569 destination: StopId,
1570 weight: f64,
1571 ) -> Result<EntityId, SimError> {
1572 let origin_eid = self
1573 .stop_lookup
1574 .get(&origin)
1575 .copied()
1576 .ok_or(SimError::StopNotFound(origin))?;
1577 let dest_eid = self
1578 .stop_lookup
1579 .get(&destination)
1580 .copied()
1581 .ok_or(SimError::StopNotFound(destination))?;
1582 self.spawn_rider(origin_eid, dest_eid, weight)
1583 }
1584
1585 /// Spawn a rider using a specific group for routing.
1586 ///
1587 /// Like [`spawn_rider`](Self::spawn_rider) but skips auto-detection —
1588 /// uses the given group directly. Useful when the caller already knows
1589 /// the group, or to resolve an [`AmbiguousRoute`](crate::error::SimError::AmbiguousRoute).
1590 ///
1591 /// # Errors
1592 ///
1593 /// Returns [`SimError::GroupNotFound`] if the group does not exist.
1594 pub fn spawn_rider_in_group(
1595 &mut self,
1596 origin: EntityId,
1597 destination: EntityId,
1598 weight: f64,
1599 group: GroupId,
1600 ) -> Result<EntityId, SimError> {
1601 if !self.groups.iter().any(|g| g.id() == group) {
1602 return Err(SimError::GroupNotFound(group));
1603 }
1604 let route = Route::direct(origin, destination, group);
1605 Ok(self.spawn_rider_inner(origin, destination, weight, route))
1606 }
1607
1608 /// Convenience: spawn a rider by config `StopId` in a specific group.
1609 ///
1610 /// # Errors
1611 ///
1612 /// Returns [`SimError::StopNotFound`] if a stop ID is unknown, or
1613 /// [`SimError::GroupNotFound`] if the group does not exist.
1614 pub fn spawn_rider_in_group_by_stop_id(
1615 &mut self,
1616 origin: StopId,
1617 destination: StopId,
1618 weight: f64,
1619 group: GroupId,
1620 ) -> Result<EntityId, SimError> {
1621 let origin_eid = self
1622 .stop_lookup
1623 .get(&origin)
1624 .copied()
1625 .ok_or(SimError::StopNotFound(origin))?;
1626 let dest_eid = self
1627 .stop_lookup
1628 .get(&destination)
1629 .copied()
1630 .ok_or(SimError::StopNotFound(destination))?;
1631 self.spawn_rider_in_group(origin_eid, dest_eid, weight, group)
1632 }
1633
1634 /// Drain all pending events from completed ticks.
1635 ///
1636 /// Events emitted during `step()` (or per-phase methods) are buffered
1637 /// and made available here after `advance_tick()` is called.
1638 /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
1639 /// are also included.
1640 ///
1641 /// ```
1642 /// use elevator_core::prelude::*;
1643 ///
1644 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1645 ///
1646 /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
1647 /// sim.step();
1648 ///
1649 /// let events = sim.drain_events();
1650 /// assert!(!events.is_empty());
1651 /// ```
1652 pub fn drain_events(&mut self) -> Vec<Event> {
1653 // Flush any events still in the bus (from spawn_rider, disable, etc.)
1654 self.pending_output.extend(self.events.drain());
1655 std::mem::take(&mut self.pending_output)
1656 }
1657
1658 /// Drain only events matching a predicate.
1659 ///
1660 /// Events that don't match the predicate remain in the buffer
1661 /// and will be returned by future `drain_events` or
1662 /// `drain_events_where` calls.
1663 ///
1664 /// ```
1665 /// use elevator_core::prelude::*;
1666 ///
1667 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1668 /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
1669 /// sim.step();
1670 ///
1671 /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
1672 /// matches!(e, Event::RiderSpawned { .. })
1673 /// });
1674 /// ```
1675 pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
1676 // Flush bus into pending_output first.
1677 self.pending_output.extend(self.events.drain());
1678
1679 let mut matched = Vec::new();
1680 let mut remaining = Vec::new();
1681 for event in std::mem::take(&mut self.pending_output) {
1682 if predicate(&event) {
1683 matched.push(event);
1684 } else {
1685 remaining.push(event);
1686 }
1687 }
1688 self.pending_output = remaining;
1689 matched
1690 }
1691
1692 // ── Sub-stepping ────────────────────────────────────────────────
1693
1694 /// Get the dispatch strategies map (for advanced sub-stepping).
1695 #[must_use]
1696 pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1697 &self.dispatchers
1698 }
1699
1700 /// Get the dispatch strategies map mutably (for advanced sub-stepping).
1701 pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1702 &mut self.dispatchers
1703 }
1704
1705 /// Get a mutable reference to the event bus.
1706 pub const fn events_mut(&mut self) -> &mut EventBus {
1707 &mut self.events
1708 }
1709
1710 /// Get a mutable reference to the metrics.
1711 pub const fn metrics_mut(&mut self) -> &mut Metrics {
1712 &mut self.metrics
1713 }
1714
1715 /// Build the `PhaseContext` for the current tick.
1716 #[must_use]
1717 pub const fn phase_context(&self) -> PhaseContext {
1718 PhaseContext {
1719 tick: self.tick,
1720 dt: self.dt,
1721 }
1722 }
1723
1724 /// Run only the `advance_transient` phase (with hooks).
1725 pub fn run_advance_transient(&mut self) {
1726 self.hooks
1727 .run_before(Phase::AdvanceTransient, &mut self.world);
1728 for group in &self.groups {
1729 self.hooks
1730 .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1731 }
1732 let ctx = self.phase_context();
1733 crate::systems::advance_transient::run(
1734 &mut self.world,
1735 &mut self.events,
1736 &ctx,
1737 &mut self.rider_index,
1738 );
1739 for group in &self.groups {
1740 self.hooks
1741 .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1742 }
1743 self.hooks
1744 .run_after(Phase::AdvanceTransient, &mut self.world);
1745 }
1746
1747 /// Run only the dispatch phase (with hooks).
1748 pub fn run_dispatch(&mut self) {
1749 self.hooks.run_before(Phase::Dispatch, &mut self.world);
1750 for group in &self.groups {
1751 self.hooks
1752 .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
1753 }
1754 let ctx = self.phase_context();
1755 crate::systems::dispatch::run(
1756 &mut self.world,
1757 &mut self.events,
1758 &ctx,
1759 &self.groups,
1760 &mut self.dispatchers,
1761 &self.rider_index,
1762 );
1763 for group in &self.groups {
1764 self.hooks
1765 .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
1766 }
1767 self.hooks.run_after(Phase::Dispatch, &mut self.world);
1768 }
1769
1770 /// Run only the movement phase (with hooks).
1771 pub fn run_movement(&mut self) {
1772 self.hooks.run_before(Phase::Movement, &mut self.world);
1773 for group in &self.groups {
1774 self.hooks
1775 .run_before_group(Phase::Movement, group.id(), &mut self.world);
1776 }
1777 let ctx = self.phase_context();
1778 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1779 crate::systems::movement::run(
1780 &mut self.world,
1781 &mut self.events,
1782 &ctx,
1783 &self.elevator_ids_buf,
1784 &mut self.metrics,
1785 );
1786 for group in &self.groups {
1787 self.hooks
1788 .run_after_group(Phase::Movement, group.id(), &mut self.world);
1789 }
1790 self.hooks.run_after(Phase::Movement, &mut self.world);
1791 }
1792
1793 /// Run only the doors phase (with hooks).
1794 pub fn run_doors(&mut self) {
1795 self.hooks.run_before(Phase::Doors, &mut self.world);
1796 for group in &self.groups {
1797 self.hooks
1798 .run_before_group(Phase::Doors, group.id(), &mut self.world);
1799 }
1800 let ctx = self.phase_context();
1801 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1802 crate::systems::doors::run(
1803 &mut self.world,
1804 &mut self.events,
1805 &ctx,
1806 &self.elevator_ids_buf,
1807 );
1808 for group in &self.groups {
1809 self.hooks
1810 .run_after_group(Phase::Doors, group.id(), &mut self.world);
1811 }
1812 self.hooks.run_after(Phase::Doors, &mut self.world);
1813 }
1814
1815 /// Run only the loading phase (with hooks).
1816 pub fn run_loading(&mut self) {
1817 self.hooks.run_before(Phase::Loading, &mut self.world);
1818 for group in &self.groups {
1819 self.hooks
1820 .run_before_group(Phase::Loading, group.id(), &mut self.world);
1821 }
1822 let ctx = self.phase_context();
1823 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1824 crate::systems::loading::run(
1825 &mut self.world,
1826 &mut self.events,
1827 &ctx,
1828 &self.elevator_ids_buf,
1829 &mut self.rider_index,
1830 );
1831 for group in &self.groups {
1832 self.hooks
1833 .run_after_group(Phase::Loading, group.id(), &mut self.world);
1834 }
1835 self.hooks.run_after(Phase::Loading, &mut self.world);
1836 }
1837
1838 /// Run only the advance-queue phase (with hooks).
1839 ///
1840 /// Reconciles each elevator's phase/target with the front of its
1841 /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
1842 /// between Reposition and Movement.
1843 pub fn run_advance_queue(&mut self) {
1844 self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
1845 for group in &self.groups {
1846 self.hooks
1847 .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1848 }
1849 let ctx = self.phase_context();
1850 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1851 crate::systems::advance_queue::run(
1852 &mut self.world,
1853 &mut self.events,
1854 &ctx,
1855 &self.elevator_ids_buf,
1856 );
1857 for group in &self.groups {
1858 self.hooks
1859 .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1860 }
1861 self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
1862 }
1863
1864 /// Run only the reposition phase (with hooks).
1865 ///
1866 /// Only runs if at least one group has a [`RepositionStrategy`] configured.
1867 /// Idle elevators with no pending dispatch assignment are repositioned
1868 /// according to their group's strategy.
1869 pub fn run_reposition(&mut self) {
1870 if self.repositioners.is_empty() {
1871 return;
1872 }
1873 self.hooks.run_before(Phase::Reposition, &mut self.world);
1874 // Only run per-group hooks for groups that have a repositioner.
1875 for group in &self.groups {
1876 if self.repositioners.contains_key(&group.id()) {
1877 self.hooks
1878 .run_before_group(Phase::Reposition, group.id(), &mut self.world);
1879 }
1880 }
1881 let ctx = self.phase_context();
1882 crate::systems::reposition::run(
1883 &mut self.world,
1884 &mut self.events,
1885 &ctx,
1886 &self.groups,
1887 &mut self.repositioners,
1888 );
1889 for group in &self.groups {
1890 if self.repositioners.contains_key(&group.id()) {
1891 self.hooks
1892 .run_after_group(Phase::Reposition, group.id(), &mut self.world);
1893 }
1894 }
1895 self.hooks.run_after(Phase::Reposition, &mut self.world);
1896 }
1897
1898 /// Run the energy system (no hooks — inline phase).
1899 #[cfg(feature = "energy")]
1900 fn run_energy(&mut self) {
1901 let ctx = self.phase_context();
1902 self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1903 crate::systems::energy::run(
1904 &mut self.world,
1905 &mut self.events,
1906 &ctx,
1907 &self.elevator_ids_buf,
1908 );
1909 }
1910
1911 /// Run only the metrics phase (with hooks).
1912 pub fn run_metrics(&mut self) {
1913 self.hooks.run_before(Phase::Metrics, &mut self.world);
1914 for group in &self.groups {
1915 self.hooks
1916 .run_before_group(Phase::Metrics, group.id(), &mut self.world);
1917 }
1918 let ctx = self.phase_context();
1919 crate::systems::metrics::run(
1920 &mut self.world,
1921 &self.events,
1922 &mut self.metrics,
1923 &ctx,
1924 &self.groups,
1925 );
1926 for group in &self.groups {
1927 self.hooks
1928 .run_after_group(Phase::Metrics, group.id(), &mut self.world);
1929 }
1930 self.hooks.run_after(Phase::Metrics, &mut self.world);
1931 }
1932
1933 // Phase-hook registration lives in `sim/construction.rs`.
1934
1935 /// Increment the tick counter and flush events to the output buffer.
1936 ///
1937 /// Call after running all desired phases. Events emitted during this tick
1938 /// are moved to the output buffer and available via `drain_events()`.
1939 pub fn advance_tick(&mut self) {
1940 self.pending_output.extend(self.events.drain());
1941 self.tick += 1;
1942 }
1943
1944 /// Advance the simulation by one tick.
1945 ///
1946 /// Events from this tick are buffered internally and available via
1947 /// `drain_events()`. The metrics system only processes events from
1948 /// the current tick, regardless of whether the consumer drains them.
1949 ///
1950 /// ```
1951 /// use elevator_core::prelude::*;
1952 ///
1953 /// let mut sim = SimulationBuilder::demo().build().unwrap();
1954 /// sim.step();
1955 /// assert_eq!(sim.current_tick(), 1);
1956 /// ```
1957 pub fn step(&mut self) {
1958 self.world.snapshot_prev_positions();
1959 self.run_advance_transient();
1960 self.run_dispatch();
1961 self.run_reposition();
1962 self.run_advance_queue();
1963 self.run_movement();
1964 self.run_doors();
1965 self.run_loading();
1966 #[cfg(feature = "energy")]
1967 self.run_energy();
1968 self.run_metrics();
1969 self.advance_tick();
1970 }
1971}
1972
1973impl fmt::Debug for Simulation {
1974 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1975 f.debug_struct("Simulation")
1976 .field("tick", &self.tick)
1977 .field("dt", &self.dt)
1978 .field("groups", &self.groups.len())
1979 .field("entities", &self.world.entity_count())
1980 .finish_non_exhaustive()
1981 }
1982}