Skip to main content

elevator_core/
sim.rs

1//! Top-level simulation runner and tick loop.
2//!
3//! # Essential API
4//!
5//! `Simulation` exposes a large surface, but most users only need the
6//! ~15 methods below, grouped by the order they appear in a typical
7//! game loop.
8//!
9//! ### Construction
10//!
11//! - [`SimulationBuilder::demo()`](crate::builder::SimulationBuilder::demo)
12//!   or [`SimulationBuilder::from_config()`](crate::builder::SimulationBuilder::from_config)
13//!   — fluent entry point; call [`.build()`](crate::builder::SimulationBuilder::build)
14//!   to get a `Simulation`.
15//! - [`Simulation::new()`](crate::sim::Simulation::new) — direct construction from
16//!   `&SimConfig` + a dispatch strategy.
17//!
18//! ### Per-tick driving
19//!
20//! - [`Simulation::step()`](crate::sim::Simulation::step) — run all 8 phases.
21//! - [`Simulation::current_tick()`](crate::sim::Simulation::current_tick) — the
22//!   current tick counter.
23//!
24//! ### Spawning and rerouting riders
25//!
26//! - [`Simulation::spawn_rider_by_stop_id()`](crate::sim::Simulation::spawn_rider_by_stop_id)
27//!   — simple origin/destination/weight spawn.
28//! - [`Simulation::build_rider_by_stop_id()`](crate::sim::Simulation::build_rider_by_stop_id)
29//!   — fluent [`RiderBuilder`](crate::sim::RiderBuilder) for patience, preferences, access
30//!   control, explicit groups, multi-leg routes.
31//! - [`Simulation::reroute()`](crate::sim::Simulation::reroute) — change a waiting
32//!   rider's destination mid-trip.
33//! - [`Simulation::settle_rider()`](crate::sim::Simulation::settle_rider) /
34//!   [`Simulation::despawn_rider()`](crate::sim::Simulation::despawn_rider) —
35//!   terminal-state cleanup for `Arrived`/`Abandoned` riders.
36//!
37//! ### Observability
38//!
39//! - [`Simulation::drain_events()`](crate::sim::Simulation::drain_events) — consume
40//!   the event stream emitted by the last tick.
41//! - [`Simulation::metrics()`](crate::sim::Simulation::metrics) — aggregate
42//!   wait/ride/throughput stats.
43//! - [`Simulation::waiting_at()`](crate::sim::Simulation::waiting_at) /
44//!   [`Simulation::residents_at()`](crate::sim::Simulation::residents_at) — O(1)
45//!   population queries by stop.
46//!
47//! ### Imperative control
48//!
49//! - [`Simulation::push_destination()`](crate::sim::Simulation::push_destination) /
50//!   [`Simulation::push_destination_front()`](crate::sim::Simulation::push_destination_front) /
51//!   [`Simulation::clear_destinations()`](crate::sim::Simulation::clear_destinations)
52//!   — override dispatch by pushing/clearing stops on an elevator's
53//!   [`DestinationQueue`](crate::components::DestinationQueue).
54//!
55//! ### Persistence
56//!
57//! - [`Simulation::snapshot()`](crate::sim::Simulation::snapshot) — capture full
58//!   state as a serializable [`WorldSnapshot`](crate::snapshot::WorldSnapshot).
59//! - [`WorldSnapshot::restore()`](crate::snapshot::WorldSnapshot::restore)
60//!   — rebuild a `Simulation` from a snapshot.
61//!
62//! Everything else (phase-runners, world-level accessors, energy, tag
63//! metrics, topology queries) is available for advanced use but is not
64//! required for the common case.
65
66mod construction;
67mod lifecycle;
68mod topology;
69
70use crate::components::{
71    AccessControl, FloorPosition, Orientation, Patience, Preferences, Rider, RiderPhase, Route,
72    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    /// Mutable access to the group collection. Use this to flip a group
448    /// into [`HallCallMode::Destination`](crate::dispatch::HallCallMode)
449    /// or tune its `ack_latency_ticks` after construction. Changing the
450    /// line/elevator structure here is not supported — use the dedicated
451    /// topology mutators for that.
452    pub fn groups_mut(&mut self) -> &mut [ElevatorGroup] {
453        &mut self.groups
454    }
455
456    /// Resolve a config `StopId` to its runtime `EntityId`.
457    #[must_use]
458    pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
459        self.stop_lookup.get(&id).copied()
460    }
461
462    /// Get the strategy identifier for a group.
463    #[must_use]
464    pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
465        self.strategy_ids.get(&group)
466    }
467
468    /// Iterate over the stop ID → entity ID mapping.
469    pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
470        self.stop_lookup.iter()
471    }
472
473    /// Peek at events pending for consumer retrieval.
474    #[must_use]
475    pub fn pending_events(&self) -> &[Event] {
476        &self.pending_output
477    }
478
479    // ── Destination queue (imperative dispatch) ────────────────────
480
481    /// Read-only view of an elevator's destination queue (FIFO of target
482    /// stop `EntityId`s).
483    ///
484    /// Returns `None` if `elev` is not an elevator entity. Returns
485    /// `Some(&[])` for elevators with an empty queue.
486    #[must_use]
487    pub fn destination_queue(&self, elev: EntityId) -> Option<&[EntityId]> {
488        self.world
489            .destination_queue(elev)
490            .map(crate::components::DestinationQueue::queue)
491    }
492
493    /// Push a stop onto the back of an elevator's destination queue.
494    ///
495    /// Adjacent duplicates are suppressed: if the last entry already equals
496    /// `stop`, the queue is unchanged and no event is emitted.
497    /// Otherwise emits [`Event::DestinationQueued`].
498    ///
499    /// # Errors
500    ///
501    /// - [`SimError::InvalidState`] if `elev` is not an elevator.
502    /// - [`SimError::InvalidState`] if `stop` is not a stop.
503    pub fn push_destination(&mut self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
504        self.validate_push_targets(elev, stop)?;
505        let appended = self
506            .world
507            .destination_queue_mut(elev)
508            .is_some_and(|q| q.push_back(stop));
509        if appended {
510            self.events.emit(Event::DestinationQueued {
511                elevator: elev,
512                stop,
513                tick: self.tick,
514            });
515        }
516        Ok(())
517    }
518
519    /// Insert a stop at the front of an elevator's destination queue —
520    /// "go here next, before anything else in the queue".
521    ///
522    /// On the next `AdvanceQueue` phase (between Dispatch and Movement),
523    /// the elevator redirects to this new front if it differs from the
524    /// current target.
525    ///
526    /// Adjacent duplicates are suppressed: if the first entry already equals
527    /// `stop`, the queue is unchanged and no event is emitted.
528    ///
529    /// # Errors
530    ///
531    /// - [`SimError::InvalidState`] if `elev` is not an elevator.
532    /// - [`SimError::InvalidState`] if `stop` is not a stop.
533    pub fn push_destination_front(
534        &mut self,
535        elev: EntityId,
536        stop: EntityId,
537    ) -> Result<(), SimError> {
538        self.validate_push_targets(elev, stop)?;
539        let inserted = self
540            .world
541            .destination_queue_mut(elev)
542            .is_some_and(|q| q.push_front(stop));
543        if inserted {
544            self.events.emit(Event::DestinationQueued {
545                elevator: elev,
546                stop,
547                tick: self.tick,
548            });
549        }
550        Ok(())
551    }
552
553    /// Clear an elevator's destination queue.
554    ///
555    /// TODO: clearing does not currently abort an in-flight movement — the
556    /// elevator will finish its current leg and then go idle (since the
557    /// queue is empty). A future change can add a phase transition to
558    /// cancel mid-flight.
559    ///
560    /// # Errors
561    ///
562    /// Returns [`SimError::InvalidState`] if `elev` is not an elevator.
563    pub fn clear_destinations(&mut self, elev: EntityId) -> Result<(), SimError> {
564        if self.world.elevator(elev).is_none() {
565            return Err(SimError::InvalidState {
566                entity: elev,
567                reason: "not an elevator".into(),
568            });
569        }
570        if let Some(q) = self.world.destination_queue_mut(elev) {
571            q.clear();
572        }
573        Ok(())
574    }
575
576    /// Validate that `elev` is an elevator and `stop` is a stop.
577    fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
578        if self.world.elevator(elev).is_none() {
579            return Err(SimError::InvalidState {
580                entity: elev,
581                reason: "not an elevator".into(),
582            });
583        }
584        if self.world.stop(stop).is_none() {
585            return Err(SimError::InvalidState {
586                entity: stop,
587                reason: "not a stop".into(),
588            });
589        }
590        Ok(())
591    }
592
593    // ── ETA queries ─────────────────────────────────────────────────
594
595    /// Estimated time until `elev` arrives at `stop`, summing closed-form
596    /// trapezoidal travel time for every leg up to (and including) the leg
597    /// that ends at `stop`, plus the door dwell at every *intermediate* stop.
598    ///
599    /// "Arrival" is the moment the door cycle begins at `stop` — door time
600    /// at `stop` itself is **not** added; door time at earlier stops along
601    /// the route **is**.
602    ///
603    /// Returns `None` if:
604    /// - `elev` is not an elevator or `stop` is not a stop,
605    /// - the elevator's [`ServiceMode`](crate::components::ServiceMode) is
606    ///   dispatch-excluded (`Manual` / `Independent`), or
607    /// - `stop` is neither the elevator's current movement target nor anywhere
608    ///   in its [`destination_queue`](Self::destination_queue).
609    ///
610    /// The estimate is best-effort. It assumes the queue is served in order
611    /// with no mid-trip insertions; dispatch decisions, manual door commands,
612    /// and rider boarding/exiting beyond the configured dwell will perturb
613    /// the actual arrival.
614    #[must_use]
615    pub fn eta(&self, elev: EntityId, stop: EntityId) -> Option<Duration> {
616        let elevator = self.world.elevator(elev)?;
617        self.world.stop(stop)?;
618        let svc = self.world.service_mode(elev).copied().unwrap_or_default();
619        if svc.is_dispatch_excluded() {
620            return None;
621        }
622
623        // Build the route in service order: current target first (if any),
624        // then queue entries, with adjacent duplicates collapsed.
625        let mut route: Vec<EntityId> = Vec::new();
626        if let Some(t) = elevator.phase().moving_target() {
627            route.push(t);
628        }
629        if let Some(q) = self.world.destination_queue(elev) {
630            for &s in q.queue() {
631                if route.last() != Some(&s) {
632                    route.push(s);
633                }
634            }
635        }
636        if !route.contains(&stop) {
637            return None;
638        }
639
640        let max_speed = elevator.max_speed();
641        let accel = elevator.acceleration();
642        let decel = elevator.deceleration();
643        let door_cycle_ticks =
644            u64::from(elevator.door_transition_ticks()) * 2 + u64::from(elevator.door_open_ticks());
645        let door_cycle_secs = (door_cycle_ticks as f64) * self.dt;
646
647        // Account for any in-progress door cycle before the first travel leg:
648        // the elevator is parked at its current stop and won't move until the
649        // door FSM returns to Closed.
650        let mut total = match elevator.door() {
651            crate::door::DoorState::Opening {
652                ticks_remaining,
653                open_duration,
654                close_duration,
655            } => f64::from(*ticks_remaining + *open_duration + *close_duration) * self.dt,
656            crate::door::DoorState::Open {
657                ticks_remaining,
658                close_duration,
659            } => f64::from(*ticks_remaining + *close_duration) * self.dt,
660            crate::door::DoorState::Closing { ticks_remaining } => {
661                f64::from(*ticks_remaining) * self.dt
662            }
663            crate::door::DoorState::Closed => 0.0,
664        };
665
666        let in_door_cycle = !matches!(elevator.door(), crate::door::DoorState::Closed);
667        let mut pos = self.world.position(elev)?.value;
668        let vel_signed = self.world.velocity(elev).map_or(0.0, Velocity::value);
669
670        for (idx, &s) in route.iter().enumerate() {
671            let Some(s_pos) = self.world.stop_position(s) else {
672                // A queued entry without a position can only mean the stop
673                // entity was despawned out from under us. Bail rather than
674                // returning a partial accumulation that would silently
675                // understate the ETA.
676                return None;
677            };
678            let dist = (s_pos - pos).abs();
679            // Only the first leg can carry initial velocity, and only if
680            // the car is already moving toward this stop and not stuck in
681            // a door cycle (which forces it to stop first).
682            let v0 = if idx == 0 && !in_door_cycle && vel_signed.abs() > f64::EPSILON {
683                let dir = (s_pos - pos).signum();
684                if dir * vel_signed > 0.0 {
685                    vel_signed.abs()
686                } else {
687                    0.0
688                }
689            } else {
690                0.0
691            };
692            total += crate::eta::travel_time(dist, v0, max_speed, accel, decel);
693            if s == stop {
694                return Some(Duration::from_secs_f64(total.max(0.0)));
695            }
696            total += door_cycle_secs;
697            pos = s_pos;
698        }
699        // `route.contains(&stop)` was true above, so the loop must hit `stop`.
700        // Fall through to `None` as a defensive backstop.
701        None
702    }
703
704    /// Best ETA to `stop` across all dispatch-eligible elevators, optionally
705    /// filtered by indicator-lamp [`Direction`](crate::components::Direction).
706    ///
707    /// Pass [`Direction::Either`](crate::components::Direction::Either) to
708    /// consider every car. Otherwise, only cars whose committed direction is
709    /// `Either` or matches the requested direction are considered — useful
710    /// for hall-call assignment ("which up-going car arrives first?").
711    ///
712    /// Returns the entity ID of the winning elevator and its ETA, or `None`
713    /// if no eligible car has `stop` queued.
714    #[must_use]
715    pub fn best_eta(
716        &self,
717        stop: EntityId,
718        direction: crate::components::Direction,
719    ) -> Option<(EntityId, Duration)> {
720        use crate::components::Direction;
721        self.world
722            .iter_elevators()
723            .filter_map(|(eid, _, elev)| {
724                let car_dir = elev.direction();
725                let direction_ok = match direction {
726                    Direction::Either => true,
727                    requested => car_dir == Direction::Either || car_dir == requested,
728                };
729                if !direction_ok {
730                    return None;
731                }
732                self.eta(eid, stop).map(|d| (eid, d))
733            })
734            .min_by_key(|(_, d)| *d)
735    }
736
737    // ── Runtime elevator upgrades ────────────────────────────────────
738    //
739    // Games that want to mutate elevator parameters at runtime (e.g.
740    // an RPG speed-upgrade purchase, a scripted capacity boost) go
741    // through these setters rather than poking `Elevator` directly via
742    // `world_mut()`. Each setter validates its input, updates the
743    // underlying component, and emits an [`Event::ElevatorUpgraded`]
744    // so game code can react without polling.
745    //
746    // ### Semantics
747    //
748    // - `max_speed`, `acceleration`, `deceleration`: applied on the next
749    //   movement integration step. The car's **current velocity is
750    //   preserved** — there is no instantaneous jerk. If `max_speed`
751    //   is lowered below the current velocity, the movement integrator
752    //   clamps velocity to the new cap on the next tick.
753    // - `weight_capacity`: applied immediately. If the new capacity is
754    //   below `current_load` the car ends up temporarily overweight —
755    //   no riders are ejected, but the next boarding pass will reject
756    //   any rider that would push the load further over the new cap.
757    // - `door_transition_ticks`, `door_open_ticks`: applied on the
758    //   **next** door cycle. An in-progress door transition keeps its
759    //   original timing, so setters never cause visual glitches.
760
761    /// Set the maximum travel speed for an elevator at runtime.
762    ///
763    /// The new value applies on the next movement integration step;
764    /// the car's current velocity is preserved (see the
765    /// [runtime upgrades section](crate#runtime-upgrades) of the crate
766    /// docs). If the new cap is below the current velocity, the movement
767    /// system clamps velocity down on the next tick.
768    ///
769    /// # Errors
770    ///
771    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
772    /// - [`SimError::InvalidConfig`] if `speed` is not a positive finite number.
773    ///
774    /// # Example
775    ///
776    /// ```
777    /// use elevator_core::prelude::*;
778    ///
779    /// let mut sim = SimulationBuilder::demo().build().unwrap();
780    /// let elev = sim.world().iter_elevators().next().unwrap().0;
781    /// sim.set_max_speed(elev, 4.0).unwrap();
782    /// assert_eq!(sim.world().elevator(elev).unwrap().max_speed(), 4.0);
783    /// ```
784    pub fn set_max_speed(&mut self, elevator: EntityId, speed: f64) -> Result<(), SimError> {
785        Self::validate_positive_finite_f64(speed, "elevators.max_speed")?;
786        let old = self.require_elevator(elevator)?.max_speed;
787        if let Some(car) = self.world.elevator_mut(elevator) {
788            car.max_speed = speed;
789        }
790        self.emit_upgrade(
791            elevator,
792            crate::events::UpgradeField::MaxSpeed,
793            crate::events::UpgradeValue::float(old),
794            crate::events::UpgradeValue::float(speed),
795        );
796        Ok(())
797    }
798
799    /// Set the acceleration rate for an elevator at runtime.
800    ///
801    /// See [`set_max_speed`](Self::set_max_speed) for the general
802    /// velocity-preservation rules that apply to kinematic setters.
803    ///
804    /// # Errors
805    ///
806    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
807    /// - [`SimError::InvalidConfig`] if `accel` is not a positive finite number.
808    ///
809    /// # Example
810    ///
811    /// ```
812    /// use elevator_core::prelude::*;
813    ///
814    /// let mut sim = SimulationBuilder::demo().build().unwrap();
815    /// let elev = sim.world().iter_elevators().next().unwrap().0;
816    /// sim.set_acceleration(elev, 3.0).unwrap();
817    /// assert_eq!(sim.world().elevator(elev).unwrap().acceleration(), 3.0);
818    /// ```
819    pub fn set_acceleration(&mut self, elevator: EntityId, accel: f64) -> Result<(), SimError> {
820        Self::validate_positive_finite_f64(accel, "elevators.acceleration")?;
821        let old = self.require_elevator(elevator)?.acceleration;
822        if let Some(car) = self.world.elevator_mut(elevator) {
823            car.acceleration = accel;
824        }
825        self.emit_upgrade(
826            elevator,
827            crate::events::UpgradeField::Acceleration,
828            crate::events::UpgradeValue::float(old),
829            crate::events::UpgradeValue::float(accel),
830        );
831        Ok(())
832    }
833
834    /// Set the deceleration rate for an elevator at runtime.
835    ///
836    /// See [`set_max_speed`](Self::set_max_speed) for the general
837    /// velocity-preservation rules that apply to kinematic setters.
838    ///
839    /// # Errors
840    ///
841    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
842    /// - [`SimError::InvalidConfig`] if `decel` is not a positive finite number.
843    ///
844    /// # Example
845    ///
846    /// ```
847    /// use elevator_core::prelude::*;
848    ///
849    /// let mut sim = SimulationBuilder::demo().build().unwrap();
850    /// let elev = sim.world().iter_elevators().next().unwrap().0;
851    /// sim.set_deceleration(elev, 3.5).unwrap();
852    /// assert_eq!(sim.world().elevator(elev).unwrap().deceleration(), 3.5);
853    /// ```
854    pub fn set_deceleration(&mut self, elevator: EntityId, decel: f64) -> Result<(), SimError> {
855        Self::validate_positive_finite_f64(decel, "elevators.deceleration")?;
856        let old = self.require_elevator(elevator)?.deceleration;
857        if let Some(car) = self.world.elevator_mut(elevator) {
858            car.deceleration = decel;
859        }
860        self.emit_upgrade(
861            elevator,
862            crate::events::UpgradeField::Deceleration,
863            crate::events::UpgradeValue::float(old),
864            crate::events::UpgradeValue::float(decel),
865        );
866        Ok(())
867    }
868
869    /// Set the weight capacity for an elevator at runtime.
870    ///
871    /// Applied immediately. If the new capacity is below the car's
872    /// current load the car is temporarily overweight; no riders are
873    /// ejected, but subsequent boarding attempts that would push load
874    /// further over the cap will be rejected as
875    /// [`RejectionReason::OverCapacity`](crate::error::RejectionReason::OverCapacity).
876    ///
877    /// # Errors
878    ///
879    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
880    /// - [`SimError::InvalidConfig`] if `capacity` is not a positive finite number.
881    ///
882    /// # Example
883    ///
884    /// ```
885    /// use elevator_core::prelude::*;
886    ///
887    /// let mut sim = SimulationBuilder::demo().build().unwrap();
888    /// let elev = sim.world().iter_elevators().next().unwrap().0;
889    /// sim.set_weight_capacity(elev, 1200.0).unwrap();
890    /// assert_eq!(sim.world().elevator(elev).unwrap().weight_capacity(), 1200.0);
891    /// ```
892    pub fn set_weight_capacity(
893        &mut self,
894        elevator: EntityId,
895        capacity: f64,
896    ) -> Result<(), SimError> {
897        Self::validate_positive_finite_f64(capacity, "elevators.weight_capacity")?;
898        let old = self.require_elevator(elevator)?.weight_capacity;
899        if let Some(car) = self.world.elevator_mut(elevator) {
900            car.weight_capacity = capacity;
901        }
902        self.emit_upgrade(
903            elevator,
904            crate::events::UpgradeField::WeightCapacity,
905            crate::events::UpgradeValue::float(old),
906            crate::events::UpgradeValue::float(capacity),
907        );
908        Ok(())
909    }
910
911    /// Set the door open/close transition duration for an elevator.
912    ///
913    /// Applied on the **next** door cycle — an in-progress transition
914    /// keeps its original timing to avoid visual glitches.
915    ///
916    /// # Errors
917    ///
918    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
919    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
920    ///
921    /// # Example
922    ///
923    /// ```
924    /// use elevator_core::prelude::*;
925    ///
926    /// let mut sim = SimulationBuilder::demo().build().unwrap();
927    /// let elev = sim.world().iter_elevators().next().unwrap().0;
928    /// sim.set_door_transition_ticks(elev, 3).unwrap();
929    /// assert_eq!(sim.world().elevator(elev).unwrap().door_transition_ticks(), 3);
930    /// ```
931    pub fn set_door_transition_ticks(
932        &mut self,
933        elevator: EntityId,
934        ticks: u32,
935    ) -> Result<(), SimError> {
936        Self::validate_nonzero_u32(ticks, "elevators.door_transition_ticks")?;
937        let old = self.require_elevator(elevator)?.door_transition_ticks;
938        if let Some(car) = self.world.elevator_mut(elevator) {
939            car.door_transition_ticks = ticks;
940        }
941        self.emit_upgrade(
942            elevator,
943            crate::events::UpgradeField::DoorTransitionTicks,
944            crate::events::UpgradeValue::ticks(old),
945            crate::events::UpgradeValue::ticks(ticks),
946        );
947        Ok(())
948    }
949
950    /// Set how long doors hold fully open for an elevator.
951    ///
952    /// Applied on the **next** door cycle — a door that is currently
953    /// holding open will complete its original dwell before the new
954    /// value takes effect.
955    ///
956    /// # Errors
957    ///
958    /// - [`SimError::InvalidState`] if `elevator` is not an elevator entity.
959    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
960    ///
961    /// # Example
962    ///
963    /// ```
964    /// use elevator_core::prelude::*;
965    ///
966    /// let mut sim = SimulationBuilder::demo().build().unwrap();
967    /// let elev = sim.world().iter_elevators().next().unwrap().0;
968    /// sim.set_door_open_ticks(elev, 20).unwrap();
969    /// assert_eq!(sim.world().elevator(elev).unwrap().door_open_ticks(), 20);
970    /// ```
971    pub fn set_door_open_ticks(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
972        Self::validate_nonzero_u32(ticks, "elevators.door_open_ticks")?;
973        let old = self.require_elevator(elevator)?.door_open_ticks;
974        if let Some(car) = self.world.elevator_mut(elevator) {
975            car.door_open_ticks = ticks;
976        }
977        self.emit_upgrade(
978            elevator,
979            crate::events::UpgradeField::DoorOpenTicks,
980            crate::events::UpgradeValue::ticks(old),
981            crate::events::UpgradeValue::ticks(ticks),
982        );
983        Ok(())
984    }
985
986    // ── Manual door control ──────────────────────────────────────────
987    //
988    // These methods let games drive door state directly — e.g. a
989    // cab-panel open/close button in a first-person game, or an RPG
990    // where the player *is* the elevator and decides when to cycle doors.
991    //
992    // Each method either applies the command immediately (if the car is
993    // in a matching door-FSM state) or queues it on the elevator for
994    // application at the next valid moment. This way games can call
995    // these any time without worrying about FSM timing, and get a clean
996    // success/failure split between "bad entity" and "bad moment".
997
998    /// Request the doors to open.
999    ///
1000    /// Applied immediately if the car is stopped at a stop with closed
1001    /// or closing doors; otherwise queued until the car next arrives.
1002    /// A no-op if the doors are already open or opening.
1003    ///
1004    /// # Errors
1005    ///
1006    /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1007    ///   entity or is disabled.
1008    ///
1009    /// # Example
1010    ///
1011    /// ```
1012    /// use elevator_core::prelude::*;
1013    ///
1014    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1015    /// let elev = sim.world().iter_elevators().next().unwrap().0;
1016    /// sim.request_door_open(elev).unwrap();
1017    /// ```
1018    pub fn request_door_open(&mut self, elevator: EntityId) -> Result<(), SimError> {
1019        self.require_enabled_elevator(elevator)?;
1020        self.enqueue_door_command(elevator, crate::door::DoorCommand::Open);
1021        Ok(())
1022    }
1023
1024    /// Request the doors to close now.
1025    ///
1026    /// Applied immediately if the doors are open or loading — forcing an
1027    /// early close — unless a rider is mid-boarding/exiting this car, in
1028    /// which case the close waits for the rider to finish. If doors are
1029    /// currently opening, the close queues and fires once fully open.
1030    ///
1031    /// # Errors
1032    ///
1033    /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1034    ///   entity or is disabled.
1035    ///
1036    /// # Example
1037    ///
1038    /// ```
1039    /// use elevator_core::prelude::*;
1040    ///
1041    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1042    /// let elev = sim.world().iter_elevators().next().unwrap().0;
1043    /// sim.request_door_close(elev).unwrap();
1044    /// ```
1045    pub fn request_door_close(&mut self, elevator: EntityId) -> Result<(), SimError> {
1046        self.require_enabled_elevator(elevator)?;
1047        self.enqueue_door_command(elevator, crate::door::DoorCommand::Close);
1048        Ok(())
1049    }
1050
1051    /// Extend the doors' open dwell by `ticks`.
1052    ///
1053    /// Cumulative — two calls of 30 ticks each extend the dwell by 60
1054    /// ticks in total. If the doors aren't open yet, the hold is queued
1055    /// and applied when they next reach the fully-open state.
1056    ///
1057    /// # Errors
1058    ///
1059    /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1060    ///   entity or is disabled.
1061    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
1062    ///
1063    /// # Example
1064    ///
1065    /// ```
1066    /// use elevator_core::prelude::*;
1067    ///
1068    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1069    /// let elev = sim.world().iter_elevators().next().unwrap().0;
1070    /// sim.hold_door_open(elev, 30).unwrap();
1071    /// ```
1072    pub fn hold_door_open(&mut self, elevator: EntityId, ticks: u32) -> Result<(), SimError> {
1073        Self::validate_nonzero_u32(ticks, "hold_door_open.ticks")?;
1074        self.require_enabled_elevator(elevator)?;
1075        self.enqueue_door_command(elevator, crate::door::DoorCommand::HoldOpen { ticks });
1076        Ok(())
1077    }
1078
1079    /// Cancel any pending hold extension.
1080    ///
1081    /// If the base open timer has already elapsed the doors close on
1082    /// the next doors-phase tick.
1083    ///
1084    /// # Errors
1085    ///
1086    /// - [`SimError::InvalidState`] if `elevator` is not an elevator
1087    ///   entity or is disabled.
1088    ///
1089    /// # Example
1090    ///
1091    /// ```
1092    /// use elevator_core::prelude::*;
1093    ///
1094    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1095    /// let elev = sim.world().iter_elevators().next().unwrap().0;
1096    /// sim.hold_door_open(elev, 100).unwrap();
1097    /// sim.cancel_door_hold(elev).unwrap();
1098    /// ```
1099    pub fn cancel_door_hold(&mut self, elevator: EntityId) -> Result<(), SimError> {
1100        self.require_enabled_elevator(elevator)?;
1101        self.enqueue_door_command(elevator, crate::door::DoorCommand::CancelHold);
1102        Ok(())
1103    }
1104
1105    /// Set the target velocity for a manual-mode elevator.
1106    ///
1107    /// The velocity is clamped to the elevator's `[-max_speed, max_speed]`
1108    /// range after validation. The car ramps toward the target each tick
1109    /// using `acceleration` (speeding up, or starting from rest) or
1110    /// `deceleration` (slowing down, or reversing direction). Positive
1111    /// values command upward travel, negative values command downward travel.
1112    ///
1113    /// # Errors
1114    /// - Entity is not an elevator, or is disabled.
1115    /// - Elevator is not in [`ServiceMode::Manual`].
1116    /// - `velocity` is not finite (NaN or infinite).
1117    ///
1118    /// [`ServiceMode::Manual`]: crate::components::ServiceMode::Manual
1119    pub fn set_target_velocity(
1120        &mut self,
1121        elevator: EntityId,
1122        velocity: f64,
1123    ) -> Result<(), SimError> {
1124        self.require_enabled_elevator(elevator)?;
1125        self.require_manual_mode(elevator)?;
1126        if !velocity.is_finite() {
1127            return Err(SimError::InvalidConfig {
1128                field: "target_velocity",
1129                reason: format!("must be finite, got {velocity}"),
1130            });
1131        }
1132        let max = self
1133            .world
1134            .elevator(elevator)
1135            .map_or(f64::INFINITY, |c| c.max_speed);
1136        let clamped = velocity.clamp(-max, max);
1137        if let Some(car) = self.world.elevator_mut(elevator) {
1138            car.manual_target_velocity = Some(clamped);
1139        }
1140        self.events.emit(Event::ManualVelocityCommanded {
1141            elevator,
1142            target_velocity: Some(ordered_float::OrderedFloat(clamped)),
1143            tick: self.tick,
1144        });
1145        Ok(())
1146    }
1147
1148    /// Command an immediate stop on a manual-mode elevator.
1149    ///
1150    /// Sets the target velocity to zero; the car decelerates at its
1151    /// configured `deceleration` rate. Equivalent to
1152    /// `set_target_velocity(elevator, 0.0)` but emits a distinct
1153    /// [`Event::ManualVelocityCommanded`] with `None` payload so games can
1154    /// distinguish an emergency stop from a deliberate hold.
1155    ///
1156    /// # Errors
1157    /// Same as [`set_target_velocity`](Self::set_target_velocity), minus
1158    /// the finite-velocity check.
1159    pub fn emergency_stop(&mut self, elevator: EntityId) -> Result<(), SimError> {
1160        self.require_enabled_elevator(elevator)?;
1161        self.require_manual_mode(elevator)?;
1162        if let Some(car) = self.world.elevator_mut(elevator) {
1163            car.manual_target_velocity = Some(0.0);
1164        }
1165        self.events.emit(Event::ManualVelocityCommanded {
1166            elevator,
1167            target_velocity: None,
1168            tick: self.tick,
1169        });
1170        Ok(())
1171    }
1172
1173    /// Internal: require an elevator be in `ServiceMode::Manual`.
1174    fn require_manual_mode(&self, elevator: EntityId) -> Result<(), SimError> {
1175        let is_manual = self
1176            .world
1177            .service_mode(elevator)
1178            .is_some_and(|m| *m == crate::components::ServiceMode::Manual);
1179        if !is_manual {
1180            return Err(SimError::InvalidState {
1181                entity: elevator,
1182                reason: "elevator is not in ServiceMode::Manual".into(),
1183            });
1184        }
1185        Ok(())
1186    }
1187
1188    /// Internal: push a command onto the queue, collapsing adjacent
1189    /// duplicates, capping length, and emitting `DoorCommandQueued`.
1190    fn enqueue_door_command(&mut self, elevator: EntityId, command: crate::door::DoorCommand) {
1191        if let Some(car) = self.world.elevator_mut(elevator) {
1192            let q = &mut car.door_command_queue;
1193            // Collapse adjacent duplicates for idempotent commands
1194            // (Open/Close/CancelHold) — repeating them adds nothing.
1195            // HoldOpen is explicitly cumulative, so never collapsed.
1196            let collapse = matches!(
1197                command,
1198                crate::door::DoorCommand::Open
1199                    | crate::door::DoorCommand::Close
1200                    | crate::door::DoorCommand::CancelHold
1201            ) && q.last().copied() == Some(command);
1202            if !collapse {
1203                q.push(command);
1204                if q.len() > crate::components::DOOR_COMMAND_QUEUE_CAP {
1205                    q.remove(0);
1206                }
1207            }
1208        }
1209        self.events.emit(Event::DoorCommandQueued {
1210            elevator,
1211            command,
1212            tick: self.tick,
1213        });
1214    }
1215
1216    /// Internal: resolve an elevator entity that is not disabled.
1217    fn require_enabled_elevator(&self, elevator: EntityId) -> Result<(), SimError> {
1218        if self.world.elevator(elevator).is_none() {
1219            return Err(SimError::InvalidState {
1220                entity: elevator,
1221                reason: "not an elevator".into(),
1222            });
1223        }
1224        if self.world.is_disabled(elevator) {
1225            return Err(SimError::InvalidState {
1226                entity: elevator,
1227                reason: "elevator is disabled".into(),
1228            });
1229        }
1230        Ok(())
1231    }
1232
1233    /// Internal: resolve an elevator entity or return a clear error.
1234    fn require_elevator(
1235        &self,
1236        elevator: EntityId,
1237    ) -> Result<&crate::components::Elevator, SimError> {
1238        self.world
1239            .elevator(elevator)
1240            .ok_or_else(|| SimError::InvalidState {
1241                entity: elevator,
1242                reason: "not an elevator".into(),
1243            })
1244    }
1245
1246    /// Internal: positive-finite validator matching the construction-time
1247    /// error shape in `sim/construction.rs::validate_elevator_config`.
1248    fn validate_positive_finite_f64(value: f64, field: &'static str) -> Result<(), SimError> {
1249        if !value.is_finite() {
1250            return Err(SimError::InvalidConfig {
1251                field,
1252                reason: format!("must be finite, got {value}"),
1253            });
1254        }
1255        if value <= 0.0 {
1256            return Err(SimError::InvalidConfig {
1257                field,
1258                reason: format!("must be positive, got {value}"),
1259            });
1260        }
1261        Ok(())
1262    }
1263
1264    /// Internal: reject zero-tick timings.
1265    fn validate_nonzero_u32(value: u32, field: &'static str) -> Result<(), SimError> {
1266        if value == 0 {
1267            return Err(SimError::InvalidConfig {
1268                field,
1269                reason: "must be > 0".into(),
1270            });
1271        }
1272        Ok(())
1273    }
1274
1275    /// Internal: emit a single `ElevatorUpgraded` event for the current tick.
1276    fn emit_upgrade(
1277        &mut self,
1278        elevator: EntityId,
1279        field: crate::events::UpgradeField,
1280        old: crate::events::UpgradeValue,
1281        new: crate::events::UpgradeValue,
1282    ) {
1283        self.events.emit(Event::ElevatorUpgraded {
1284            elevator,
1285            field,
1286            old,
1287            new,
1288            tick: self.tick,
1289        });
1290    }
1291
1292    // Dispatch & reposition management live in `sim/construction.rs`.
1293
1294    // ── Tagging ──────────────────────────────────────────────────────
1295
1296    /// Attach a metric tag to an entity (rider, stop, elevator, etc.).
1297    ///
1298    /// Tags enable per-tag metric breakdowns. An entity can have multiple tags.
1299    /// Riders automatically inherit tags from their origin stop when spawned.
1300    pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) {
1301        if let Some(tags) = self
1302            .world
1303            .resource_mut::<crate::tagged_metrics::MetricTags>()
1304        {
1305            tags.tag(id, tag);
1306        }
1307    }
1308
1309    /// Remove a metric tag from an entity.
1310    pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
1311        if let Some(tags) = self
1312            .world
1313            .resource_mut::<crate::tagged_metrics::MetricTags>()
1314        {
1315            tags.untag(id, tag);
1316        }
1317    }
1318
1319    /// Query the metric accumulator for a specific tag.
1320    #[must_use]
1321    pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
1322        self.world
1323            .resource::<crate::tagged_metrics::MetricTags>()
1324            .and_then(|tags| tags.metric(tag))
1325    }
1326
1327    /// List all registered metric tags.
1328    pub fn all_tags(&self) -> Vec<&str> {
1329        self.world
1330            .resource::<crate::tagged_metrics::MetricTags>()
1331            .map_or_else(Vec::new, |tags| tags.all_tags().collect())
1332    }
1333
1334    // ── Rider spawning ───────────────────────────────────────────────
1335
1336    /// Create a rider builder for fluent rider spawning.
1337    ///
1338    /// ```
1339    /// use elevator_core::prelude::*;
1340    ///
1341    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1342    /// let s0 = sim.stop_entity(StopId(0)).unwrap();
1343    /// let s1 = sim.stop_entity(StopId(1)).unwrap();
1344    /// let rider = sim.build_rider(s0, s1)
1345    ///     .weight(80.0)
1346    ///     .spawn()
1347    ///     .unwrap();
1348    /// ```
1349    pub const fn build_rider(
1350        &mut self,
1351        origin: EntityId,
1352        destination: EntityId,
1353    ) -> RiderBuilder<'_> {
1354        RiderBuilder {
1355            sim: self,
1356            origin,
1357            destination,
1358            weight: 75.0,
1359            group: None,
1360            route: None,
1361            patience: None,
1362            preferences: None,
1363            access_control: None,
1364        }
1365    }
1366
1367    /// Create a rider builder using config `StopId`s.
1368    ///
1369    /// # Errors
1370    ///
1371    /// Returns [`SimError::StopNotFound`] if either stop ID is unknown.
1372    ///
1373    /// ```
1374    /// use elevator_core::prelude::*;
1375    ///
1376    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1377    /// let rider = sim.build_rider_by_stop_id(StopId(0), StopId(1))
1378    ///     .unwrap()
1379    ///     .weight(80.0)
1380    ///     .spawn()
1381    ///     .unwrap();
1382    /// ```
1383    pub fn build_rider_by_stop_id(
1384        &mut self,
1385        origin: StopId,
1386        destination: StopId,
1387    ) -> Result<RiderBuilder<'_>, SimError> {
1388        let origin_eid = self
1389            .stop_lookup
1390            .get(&origin)
1391            .copied()
1392            .ok_or(SimError::StopNotFound(origin))?;
1393        let dest_eid = self
1394            .stop_lookup
1395            .get(&destination)
1396            .copied()
1397            .ok_or(SimError::StopNotFound(destination))?;
1398        Ok(RiderBuilder {
1399            sim: self,
1400            origin: origin_eid,
1401            destination: dest_eid,
1402            weight: 75.0,
1403            group: None,
1404            route: None,
1405            patience: None,
1406            preferences: None,
1407            access_control: None,
1408        })
1409    }
1410
1411    /// Spawn a rider at the given origin stop entity, headed to destination stop entity.
1412    ///
1413    /// Auto-detects the elevator group by finding groups that serve both origin
1414    /// and destination stops.
1415    ///
1416    /// # Errors
1417    ///
1418    /// Returns [`SimError::NoRoute`] if no group serves both stops.
1419    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
1420    pub fn spawn_rider(
1421        &mut self,
1422        origin: EntityId,
1423        destination: EntityId,
1424        weight: f64,
1425    ) -> Result<EntityId, SimError> {
1426        let matching: Vec<GroupId> = self
1427            .groups
1428            .iter()
1429            .filter(|g| {
1430                g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
1431            })
1432            .map(ElevatorGroup::id)
1433            .collect();
1434
1435        let group = match matching.len() {
1436            0 => {
1437                let origin_groups: Vec<GroupId> = self
1438                    .groups
1439                    .iter()
1440                    .filter(|g| g.stop_entities().contains(&origin))
1441                    .map(ElevatorGroup::id)
1442                    .collect();
1443                let destination_groups: Vec<GroupId> = self
1444                    .groups
1445                    .iter()
1446                    .filter(|g| g.stop_entities().contains(&destination))
1447                    .map(ElevatorGroup::id)
1448                    .collect();
1449                return Err(SimError::NoRoute {
1450                    origin,
1451                    destination,
1452                    origin_groups,
1453                    destination_groups,
1454                });
1455            }
1456            1 => matching[0],
1457            _ => {
1458                return Err(SimError::AmbiguousRoute {
1459                    origin,
1460                    destination,
1461                    groups: matching,
1462                });
1463            }
1464        };
1465
1466        let route = Route::direct(origin, destination, group);
1467        Ok(self.spawn_rider_inner(origin, destination, weight, route))
1468    }
1469
1470    /// Spawn a rider with an explicit route.
1471    ///
1472    /// Same as [`spawn_rider`](Self::spawn_rider) but uses the provided route
1473    /// instead of auto-detecting the group.
1474    ///
1475    /// # Errors
1476    ///
1477    /// Returns [`SimError::EntityNotFound`] if origin does not exist.
1478    /// Returns [`SimError::InvalidState`] if origin doesn't match the route's
1479    /// first leg `from`.
1480    pub fn spawn_rider_with_route(
1481        &mut self,
1482        origin: EntityId,
1483        destination: EntityId,
1484        weight: f64,
1485        route: Route,
1486    ) -> Result<EntityId, SimError> {
1487        if self.world.stop(origin).is_none() {
1488            return Err(SimError::EntityNotFound(origin));
1489        }
1490        if let Some(leg) = route.current()
1491            && leg.from != origin
1492        {
1493            return Err(SimError::InvalidState {
1494                entity: origin,
1495                reason: format!(
1496                    "origin {origin:?} does not match route first leg from {:?}",
1497                    leg.from
1498                ),
1499            });
1500        }
1501        Ok(self.spawn_rider_inner(origin, destination, weight, route))
1502    }
1503
1504    /// Internal helper: spawn a rider entity with the given route.
1505    fn spawn_rider_inner(
1506        &mut self,
1507        origin: EntityId,
1508        destination: EntityId,
1509        weight: f64,
1510        route: Route,
1511    ) -> EntityId {
1512        let eid = self.world.spawn();
1513        self.world.set_rider(
1514            eid,
1515            Rider {
1516                weight,
1517                phase: RiderPhase::Waiting,
1518                current_stop: Some(origin),
1519                spawn_tick: self.tick,
1520                board_tick: None,
1521            },
1522        );
1523        self.world.set_route(eid, route);
1524        self.rider_index.insert_waiting(origin, eid);
1525        self.events.emit(Event::RiderSpawned {
1526            rider: eid,
1527            origin,
1528            destination,
1529            tick: self.tick,
1530        });
1531
1532        // Auto-press the hall button for this rider. Direction is the
1533        // sign of `dest_pos - origin_pos`; if the two coincide (walk
1534        // leg, identity trip) no call is registered.
1535        if let (Some(op), Some(dp)) = (
1536            self.world.stop_position(origin),
1537            self.world.stop_position(destination),
1538        ) && let Some(direction) = crate::components::CallDirection::between(op, dp)
1539        {
1540            self.register_hall_call_for_rider(origin, direction, eid, destination);
1541        }
1542
1543        // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
1544        let stop_tag = self
1545            .world
1546            .stop(origin)
1547            .map(|s| format!("stop:{}", s.name()));
1548
1549        // Inherit metric tags from the origin stop.
1550        if let Some(tags_res) = self
1551            .world
1552            .resource_mut::<crate::tagged_metrics::MetricTags>()
1553        {
1554            let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
1555            for tag in origin_tags {
1556                tags_res.tag(eid, tag);
1557            }
1558            // Apply the origin stop tag.
1559            if let Some(tag) = stop_tag {
1560                tags_res.tag(eid, tag);
1561            }
1562        }
1563
1564        eid
1565    }
1566
1567    /// Convenience: spawn a rider by config `StopId`.
1568    ///
1569    /// Returns `Err` if either stop ID is not found.
1570    ///
1571    /// # Errors
1572    ///
1573    /// Returns [`SimError::StopNotFound`] if the origin or destination stop ID
1574    /// is not in the building configuration.
1575    ///
1576    /// ```
1577    /// use elevator_core::prelude::*;
1578    ///
1579    /// // Default builder has StopId(0) and StopId(1).
1580    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1581    ///
1582    /// let rider = sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 80.0).unwrap();
1583    /// sim.step(); // metrics are updated during the tick
1584    /// assert_eq!(sim.metrics().total_spawned(), 1);
1585    /// ```
1586    pub fn spawn_rider_by_stop_id(
1587        &mut self,
1588        origin: StopId,
1589        destination: StopId,
1590        weight: f64,
1591    ) -> Result<EntityId, SimError> {
1592        let origin_eid = self
1593            .stop_lookup
1594            .get(&origin)
1595            .copied()
1596            .ok_or(SimError::StopNotFound(origin))?;
1597        let dest_eid = self
1598            .stop_lookup
1599            .get(&destination)
1600            .copied()
1601            .ok_or(SimError::StopNotFound(destination))?;
1602        self.spawn_rider(origin_eid, dest_eid, weight)
1603    }
1604
1605    /// Spawn a rider using a specific group for routing.
1606    ///
1607    /// Like [`spawn_rider`](Self::spawn_rider) but skips auto-detection —
1608    /// uses the given group directly. Useful when the caller already knows
1609    /// the group, or to resolve an [`AmbiguousRoute`](crate::error::SimError::AmbiguousRoute).
1610    ///
1611    /// # Errors
1612    ///
1613    /// Returns [`SimError::GroupNotFound`] if the group does not exist.
1614    pub fn spawn_rider_in_group(
1615        &mut self,
1616        origin: EntityId,
1617        destination: EntityId,
1618        weight: f64,
1619        group: GroupId,
1620    ) -> Result<EntityId, SimError> {
1621        if !self.groups.iter().any(|g| g.id() == group) {
1622            return Err(SimError::GroupNotFound(group));
1623        }
1624        let route = Route::direct(origin, destination, group);
1625        Ok(self.spawn_rider_inner(origin, destination, weight, route))
1626    }
1627
1628    /// Convenience: spawn a rider by config `StopId` in a specific group.
1629    ///
1630    /// # Errors
1631    ///
1632    /// Returns [`SimError::StopNotFound`] if a stop ID is unknown, or
1633    /// [`SimError::GroupNotFound`] if the group does not exist.
1634    pub fn spawn_rider_in_group_by_stop_id(
1635        &mut self,
1636        origin: StopId,
1637        destination: StopId,
1638        weight: f64,
1639        group: GroupId,
1640    ) -> Result<EntityId, SimError> {
1641        let origin_eid = self
1642            .stop_lookup
1643            .get(&origin)
1644            .copied()
1645            .ok_or(SimError::StopNotFound(origin))?;
1646        let dest_eid = self
1647            .stop_lookup
1648            .get(&destination)
1649            .copied()
1650            .ok_or(SimError::StopNotFound(destination))?;
1651        self.spawn_rider_in_group(origin_eid, dest_eid, weight, group)
1652    }
1653
1654    /// Drain all pending events from completed ticks.
1655    ///
1656    /// Events emitted during `step()` (or per-phase methods) are buffered
1657    /// and made available here after `advance_tick()` is called.
1658    /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
1659    /// are also included.
1660    ///
1661    /// ```
1662    /// use elevator_core::prelude::*;
1663    ///
1664    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1665    ///
1666    /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
1667    /// sim.step();
1668    ///
1669    /// let events = sim.drain_events();
1670    /// assert!(!events.is_empty());
1671    /// ```
1672    pub fn drain_events(&mut self) -> Vec<Event> {
1673        // Flush any events still in the bus (from spawn_rider, disable, etc.)
1674        self.pending_output.extend(self.events.drain());
1675        std::mem::take(&mut self.pending_output)
1676    }
1677
1678    /// Drain only events matching a predicate.
1679    ///
1680    /// Events that don't match the predicate remain in the buffer
1681    /// and will be returned by future `drain_events` or
1682    /// `drain_events_where` calls.
1683    ///
1684    /// ```
1685    /// use elevator_core::prelude::*;
1686    ///
1687    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1688    /// sim.spawn_rider_by_stop_id(StopId(0), StopId(1), 70.0).unwrap();
1689    /// sim.step();
1690    ///
1691    /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
1692    ///     matches!(e, Event::RiderSpawned { .. })
1693    /// });
1694    /// ```
1695    pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
1696        // Flush bus into pending_output first.
1697        self.pending_output.extend(self.events.drain());
1698
1699        let mut matched = Vec::new();
1700        let mut remaining = Vec::new();
1701        for event in std::mem::take(&mut self.pending_output) {
1702            if predicate(&event) {
1703                matched.push(event);
1704            } else {
1705                remaining.push(event);
1706            }
1707        }
1708        self.pending_output = remaining;
1709        matched
1710    }
1711
1712    // ── Sub-stepping ────────────────────────────────────────────────
1713
1714    /// Get the dispatch strategies map (for advanced sub-stepping).
1715    #[must_use]
1716    pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1717        &self.dispatchers
1718    }
1719
1720    /// Get the dispatch strategies map mutably (for advanced sub-stepping).
1721    pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1722        &mut self.dispatchers
1723    }
1724
1725    /// Get a mutable reference to the event bus.
1726    pub const fn events_mut(&mut self) -> &mut EventBus {
1727        &mut self.events
1728    }
1729
1730    /// Get a mutable reference to the metrics.
1731    pub const fn metrics_mut(&mut self) -> &mut Metrics {
1732        &mut self.metrics
1733    }
1734
1735    /// Build the `PhaseContext` for the current tick.
1736    #[must_use]
1737    pub const fn phase_context(&self) -> PhaseContext {
1738        PhaseContext {
1739            tick: self.tick,
1740            dt: self.dt,
1741        }
1742    }
1743
1744    /// Run only the `advance_transient` phase (with hooks).
1745    pub fn run_advance_transient(&mut self) {
1746        self.hooks
1747            .run_before(Phase::AdvanceTransient, &mut self.world);
1748        for group in &self.groups {
1749            self.hooks
1750                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1751        }
1752        let ctx = self.phase_context();
1753        crate::systems::advance_transient::run(
1754            &mut self.world,
1755            &mut self.events,
1756            &ctx,
1757            &mut self.rider_index,
1758        );
1759        for group in &self.groups {
1760            self.hooks
1761                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1762        }
1763        self.hooks
1764            .run_after(Phase::AdvanceTransient, &mut self.world);
1765    }
1766
1767    /// Run only the dispatch phase (with hooks).
1768    pub fn run_dispatch(&mut self) {
1769        self.hooks.run_before(Phase::Dispatch, &mut self.world);
1770        for group in &self.groups {
1771            self.hooks
1772                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
1773        }
1774        let ctx = self.phase_context();
1775        crate::systems::dispatch::run(
1776            &mut self.world,
1777            &mut self.events,
1778            &ctx,
1779            &self.groups,
1780            &mut self.dispatchers,
1781            &self.rider_index,
1782        );
1783        for group in &self.groups {
1784            self.hooks
1785                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
1786        }
1787        self.hooks.run_after(Phase::Dispatch, &mut self.world);
1788    }
1789
1790    /// Run only the movement phase (with hooks).
1791    pub fn run_movement(&mut self) {
1792        self.hooks.run_before(Phase::Movement, &mut self.world);
1793        for group in &self.groups {
1794            self.hooks
1795                .run_before_group(Phase::Movement, group.id(), &mut self.world);
1796        }
1797        let ctx = self.phase_context();
1798        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1799        crate::systems::movement::run(
1800            &mut self.world,
1801            &mut self.events,
1802            &ctx,
1803            &self.elevator_ids_buf,
1804            &mut self.metrics,
1805        );
1806        for group in &self.groups {
1807            self.hooks
1808                .run_after_group(Phase::Movement, group.id(), &mut self.world);
1809        }
1810        self.hooks.run_after(Phase::Movement, &mut self.world);
1811    }
1812
1813    /// Run only the doors phase (with hooks).
1814    pub fn run_doors(&mut self) {
1815        self.hooks.run_before(Phase::Doors, &mut self.world);
1816        for group in &self.groups {
1817            self.hooks
1818                .run_before_group(Phase::Doors, group.id(), &mut self.world);
1819        }
1820        let ctx = self.phase_context();
1821        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1822        crate::systems::doors::run(
1823            &mut self.world,
1824            &mut self.events,
1825            &ctx,
1826            &self.elevator_ids_buf,
1827        );
1828        for group in &self.groups {
1829            self.hooks
1830                .run_after_group(Phase::Doors, group.id(), &mut self.world);
1831        }
1832        self.hooks.run_after(Phase::Doors, &mut self.world);
1833    }
1834
1835    /// Run only the loading phase (with hooks).
1836    pub fn run_loading(&mut self) {
1837        self.hooks.run_before(Phase::Loading, &mut self.world);
1838        for group in &self.groups {
1839            self.hooks
1840                .run_before_group(Phase::Loading, group.id(), &mut self.world);
1841        }
1842        let ctx = self.phase_context();
1843        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1844        crate::systems::loading::run(
1845            &mut self.world,
1846            &mut self.events,
1847            &ctx,
1848            &self.elevator_ids_buf,
1849            &mut self.rider_index,
1850        );
1851        for group in &self.groups {
1852            self.hooks
1853                .run_after_group(Phase::Loading, group.id(), &mut self.world);
1854        }
1855        self.hooks.run_after(Phase::Loading, &mut self.world);
1856    }
1857
1858    /// Run only the advance-queue phase (with hooks).
1859    ///
1860    /// Reconciles each elevator's phase/target with the front of its
1861    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
1862    /// between Reposition and Movement.
1863    pub fn run_advance_queue(&mut self) {
1864        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
1865        for group in &self.groups {
1866            self.hooks
1867                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1868        }
1869        let ctx = self.phase_context();
1870        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1871        crate::systems::advance_queue::run(
1872            &mut self.world,
1873            &mut self.events,
1874            &ctx,
1875            &self.elevator_ids_buf,
1876        );
1877        for group in &self.groups {
1878            self.hooks
1879                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1880        }
1881        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
1882    }
1883
1884    /// Run only the reposition phase (with hooks).
1885    ///
1886    /// Only runs if at least one group has a [`RepositionStrategy`] configured.
1887    /// Idle elevators with no pending dispatch assignment are repositioned
1888    /// according to their group's strategy.
1889    pub fn run_reposition(&mut self) {
1890        if self.repositioners.is_empty() {
1891            return;
1892        }
1893        self.hooks.run_before(Phase::Reposition, &mut self.world);
1894        // Only run per-group hooks for groups that have a repositioner.
1895        for group in &self.groups {
1896            if self.repositioners.contains_key(&group.id()) {
1897                self.hooks
1898                    .run_before_group(Phase::Reposition, group.id(), &mut self.world);
1899            }
1900        }
1901        let ctx = self.phase_context();
1902        crate::systems::reposition::run(
1903            &mut self.world,
1904            &mut self.events,
1905            &ctx,
1906            &self.groups,
1907            &mut self.repositioners,
1908        );
1909        for group in &self.groups {
1910            if self.repositioners.contains_key(&group.id()) {
1911                self.hooks
1912                    .run_after_group(Phase::Reposition, group.id(), &mut self.world);
1913            }
1914        }
1915        self.hooks.run_after(Phase::Reposition, &mut self.world);
1916    }
1917
1918    /// Run the energy system (no hooks — inline phase).
1919    #[cfg(feature = "energy")]
1920    fn run_energy(&mut self) {
1921        let ctx = self.phase_context();
1922        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1923        crate::systems::energy::run(
1924            &mut self.world,
1925            &mut self.events,
1926            &ctx,
1927            &self.elevator_ids_buf,
1928        );
1929    }
1930
1931    /// Run only the metrics phase (with hooks).
1932    pub fn run_metrics(&mut self) {
1933        self.hooks.run_before(Phase::Metrics, &mut self.world);
1934        for group in &self.groups {
1935            self.hooks
1936                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
1937        }
1938        let ctx = self.phase_context();
1939        crate::systems::metrics::run(
1940            &mut self.world,
1941            &self.events,
1942            &mut self.metrics,
1943            &ctx,
1944            &self.groups,
1945        );
1946        for group in &self.groups {
1947            self.hooks
1948                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
1949        }
1950        self.hooks.run_after(Phase::Metrics, &mut self.world);
1951    }
1952
1953    // Phase-hook registration lives in `sim/construction.rs`.
1954
1955    /// Increment the tick counter and flush events to the output buffer.
1956    ///
1957    /// Call after running all desired phases. Events emitted during this tick
1958    /// are moved to the output buffer and available via `drain_events()`.
1959    pub fn advance_tick(&mut self) {
1960        self.pending_output.extend(self.events.drain());
1961        self.tick += 1;
1962    }
1963
1964    /// Advance the simulation by one tick.
1965    ///
1966    /// Events from this tick are buffered internally and available via
1967    /// `drain_events()`. The metrics system only processes events from
1968    /// the current tick, regardless of whether the consumer drains them.
1969    ///
1970    /// ```
1971    /// use elevator_core::prelude::*;
1972    ///
1973    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1974    /// sim.step();
1975    /// assert_eq!(sim.current_tick(), 1);
1976    /// ```
1977    pub fn step(&mut self) {
1978        self.world.snapshot_prev_positions();
1979        self.run_advance_transient();
1980        self.run_dispatch();
1981        self.run_reposition();
1982        self.run_advance_queue();
1983        self.run_movement();
1984        self.run_doors();
1985        self.run_loading();
1986        #[cfg(feature = "energy")]
1987        self.run_energy();
1988        self.run_metrics();
1989        self.advance_tick();
1990    }
1991
1992    // ── Hall / car call API ─────────────────────────────────────────
1993
1994    /// Press an up/down hall button at `stop` without associating it
1995    /// with any particular rider. Useful for scripted NPCs, player
1996    /// input, or cutscene cues.
1997    ///
1998    /// If a call in this direction already exists at `stop`, the press
1999    /// tick is left untouched (first press wins for latency purposes).
2000    ///
2001    /// # Errors
2002    /// Returns [`SimError::EntityNotFound`] if `stop` is not a valid
2003    /// stop entity.
2004    pub fn press_hall_button(
2005        &mut self,
2006        stop: EntityId,
2007        direction: crate::components::CallDirection,
2008    ) -> Result<(), SimError> {
2009        if self.world.stop(stop).is_none() {
2010            return Err(SimError::EntityNotFound(stop));
2011        }
2012        self.ensure_hall_call(stop, direction, None, None);
2013        Ok(())
2014    }
2015
2016    /// Press a floor button from inside `car`. No-op if the car already
2017    /// has a pending call for `floor`.
2018    ///
2019    /// # Errors
2020    /// Returns [`SimError::EntityNotFound`] if `car` or `floor` is invalid.
2021    pub fn press_car_button(&mut self, car: EntityId, floor: EntityId) -> Result<(), SimError> {
2022        if self.world.elevator(car).is_none() {
2023            return Err(SimError::EntityNotFound(car));
2024        }
2025        if self.world.stop(floor).is_none() {
2026            return Err(SimError::EntityNotFound(floor));
2027        }
2028        self.ensure_car_call(car, floor, None);
2029        Ok(())
2030    }
2031
2032    /// Pin the hall call at `(stop, direction)` to `car`. Dispatch is
2033    /// forbidden from reassigning the call to a different car until
2034    /// [`unpin_assignment`](Self::unpin_assignment) is called or the
2035    /// call is cleared.
2036    ///
2037    /// # Errors
2038    /// - [`SimError::EntityNotFound`] — `car` is not a valid elevator.
2039    /// - [`SimError::InvalidState`] with `entity = stop` — no hall call
2040    ///   exists at that `(stop, direction)` pair yet.
2041    /// - [`SimError::InvalidState`] with `entity = car` — the car's
2042    ///   line does not serve `stop`. Without this check a cross-line
2043    ///   pin would be silently dropped at dispatch time yet leave the
2044    ///   call `pinned`, blocking every other car.
2045    pub fn pin_assignment(
2046        &mut self,
2047        car: EntityId,
2048        stop: EntityId,
2049        direction: crate::components::CallDirection,
2050    ) -> Result<(), SimError> {
2051        let Some(elev) = self.world.elevator(car) else {
2052            return Err(SimError::EntityNotFound(car));
2053        };
2054        let car_line = elev.line;
2055        // Validate the car's line can reach the stop. If the line has
2056        // an entry in any group, we consult its `serves` list. A car
2057        // whose line entity doesn't match any line in any group falls
2058        // through — older test fixtures create elevators without a
2059        // line entity, and we don't want to regress them.
2060        let line_serves_stop = self
2061            .groups
2062            .iter()
2063            .flat_map(|g| g.lines().iter())
2064            .find(|li| li.entity() == car_line)
2065            .map(|li| li.serves().contains(&stop));
2066        if line_serves_stop == Some(false) {
2067            return Err(SimError::InvalidState {
2068                entity: car,
2069                reason: format!(
2070                    "car's line does not serve stop {stop:?}; pinning would orphan the call"
2071                ),
2072            });
2073        }
2074        let Some(call) = self.world.hall_call_mut(stop, direction) else {
2075            return Err(SimError::InvalidState {
2076                entity: stop,
2077                reason: "no hall call exists at that stop and direction".to_string(),
2078            });
2079        };
2080        call.assigned_car = Some(car);
2081        call.pinned = true;
2082        Ok(())
2083    }
2084
2085    /// Release a previous pin at `(stop, direction)`. No-op if the call
2086    /// doesn't exist or wasn't pinned.
2087    pub fn unpin_assignment(
2088        &mut self,
2089        stop: EntityId,
2090        direction: crate::components::CallDirection,
2091    ) {
2092        if let Some(call) = self.world.hall_call_mut(stop, direction) {
2093            call.pinned = false;
2094        }
2095    }
2096
2097    /// Iterate every active hall call across the simulation. Yields a
2098    /// reference per live `(stop, direction)` press; games use this to
2099    /// render lobby lamp states, pending-rider counts, or per-floor
2100    /// button animations.
2101    pub fn hall_calls(&self) -> impl Iterator<Item = &crate::components::HallCall> {
2102        self.world.iter_hall_calls()
2103    }
2104
2105    /// Floor buttons currently pressed inside `car`. Returns an empty
2106    /// slice when the car has no aboard riders or hasn't been used.
2107    #[must_use]
2108    pub fn car_calls(&self, car: EntityId) -> &[crate::components::CarCall] {
2109        self.world.car_calls(car)
2110    }
2111
2112    /// Car currently assigned to serve the call at `(stop, direction)`,
2113    /// if dispatch has made an assignment yet.
2114    #[must_use]
2115    pub fn assigned_car(
2116        &self,
2117        stop: EntityId,
2118        direction: crate::components::CallDirection,
2119    ) -> Option<EntityId> {
2120        self.world
2121            .hall_call(stop, direction)
2122            .and_then(|c| c.assigned_car)
2123    }
2124
2125    /// Estimated ticks remaining before the assigned car reaches the
2126    /// call at `(stop, direction)`. Returns `None` when no car is
2127    /// assigned or the car has no positional data.
2128    #[must_use]
2129    pub fn eta_for_call(
2130        &self,
2131        stop: EntityId,
2132        direction: crate::components::CallDirection,
2133    ) -> Option<u64> {
2134        let call = self.world.hall_call(stop, direction)?;
2135        let car = call.assigned_car?;
2136        let car_pos = self.world.position(car)?.value;
2137        let stop_pos = self.world.stop_position(stop)?;
2138        let max_speed = self.world.elevator(car)?.max_speed();
2139        if max_speed <= 0.0 {
2140            return None;
2141        }
2142        let distance = (car_pos - stop_pos).abs();
2143        // Simple kinematic estimate. The `eta` module has a richer
2144        // trapezoidal model; the one-liner suits most hall-display use.
2145        Some((distance / max_speed).ceil() as u64)
2146    }
2147
2148    // ── Internal helpers ────────────────────────────────────────────
2149
2150    /// Register (or aggregate) a hall call on behalf of a specific
2151    /// rider, including their destination in DCS mode.
2152    fn register_hall_call_for_rider(
2153        &mut self,
2154        stop: EntityId,
2155        direction: crate::components::CallDirection,
2156        rider: EntityId,
2157        destination: EntityId,
2158    ) {
2159        let mode = self
2160            .groups
2161            .iter()
2162            .find(|g| g.stop_entities().contains(&stop))
2163            .map(crate::dispatch::ElevatorGroup::hall_call_mode);
2164        let dest = match mode {
2165            Some(crate::dispatch::HallCallMode::Destination) => Some(destination),
2166            _ => None,
2167        };
2168        self.ensure_hall_call(stop, direction, Some(rider), dest);
2169    }
2170
2171    /// Create or aggregate into the hall call at `(stop, direction)`.
2172    /// Emits [`Event::HallButtonPressed`] only on the *first* press.
2173    fn ensure_hall_call(
2174        &mut self,
2175        stop: EntityId,
2176        direction: crate::components::CallDirection,
2177        rider: Option<EntityId>,
2178        destination: Option<EntityId>,
2179    ) {
2180        let mut fresh_press = false;
2181        if self.world.hall_call(stop, direction).is_none() {
2182            let mut call = crate::components::HallCall::new(stop, direction, self.tick);
2183            call.destination = destination;
2184            call.ack_latency_ticks = self.ack_latency_for_stop(stop);
2185            if call.ack_latency_ticks == 0 {
2186                // Controller has zero-tick latency — mark acknowledged
2187                // immediately so dispatch sees the call this same tick.
2188                call.acknowledged_at = Some(self.tick);
2189            }
2190            if let Some(rid) = rider {
2191                call.pending_riders.push(rid);
2192            }
2193            self.world.set_hall_call(call);
2194            fresh_press = true;
2195        } else if let Some(existing) = self.world.hall_call_mut(stop, direction) {
2196            if let Some(rid) = rider
2197                && !existing.pending_riders.contains(&rid)
2198            {
2199                existing.pending_riders.push(rid);
2200            }
2201            // Prefer a populated destination over None; don't overwrite
2202            // an existing destination even if a later press omits it.
2203            if existing.destination.is_none() {
2204                existing.destination = destination;
2205            }
2206        }
2207        if fresh_press {
2208            self.events.emit(Event::HallButtonPressed {
2209                stop,
2210                direction,
2211                tick: self.tick,
2212            });
2213            // Zero-latency controllers acknowledge on the press tick.
2214            if let Some(call) = self.world.hall_call(stop, direction)
2215                && call.acknowledged_at == Some(self.tick)
2216            {
2217                self.events.emit(Event::HallCallAcknowledged {
2218                    stop,
2219                    direction,
2220                    tick: self.tick,
2221                });
2222            }
2223        }
2224    }
2225
2226    /// Ack latency for the group whose `members` slice contains `entity`.
2227    /// Defaults to 0 if no group matches (unreachable in normal builds).
2228    fn ack_latency_for(
2229        &self,
2230        entity: EntityId,
2231        members: impl Fn(&crate::dispatch::ElevatorGroup) -> &[EntityId],
2232    ) -> u32 {
2233        self.groups
2234            .iter()
2235            .find(|g| members(g).contains(&entity))
2236            .map_or(0, crate::dispatch::ElevatorGroup::ack_latency_ticks)
2237    }
2238
2239    /// Ack latency for the group that owns `stop` (0 if no group).
2240    fn ack_latency_for_stop(&self, stop: EntityId) -> u32 {
2241        self.ack_latency_for(stop, crate::dispatch::ElevatorGroup::stop_entities)
2242    }
2243
2244    /// Ack latency for the group that owns `car` (0 if no group).
2245    fn ack_latency_for_car(&self, car: EntityId) -> u32 {
2246        self.ack_latency_for(car, crate::dispatch::ElevatorGroup::elevator_entities)
2247    }
2248
2249    /// Create or aggregate into a car call for `(car, floor)`.
2250    /// Emits [`Event::CarButtonPressed`] on first press; repeat presses
2251    /// by other riders append to `pending_riders` without re-emitting.
2252    fn ensure_car_call(&mut self, car: EntityId, floor: EntityId, rider: Option<EntityId>) {
2253        let press_tick = self.tick;
2254        let ack_latency = self.ack_latency_for_car(car);
2255        let Some(queue) = self.world.car_calls_mut(car) else {
2256            return;
2257        };
2258        let existing_idx = queue.iter().position(|c| c.floor == floor);
2259        let fresh = existing_idx.is_none();
2260        if let Some(idx) = existing_idx {
2261            if let Some(rid) = rider
2262                && !queue[idx].pending_riders.contains(&rid)
2263            {
2264                queue[idx].pending_riders.push(rid);
2265            }
2266        } else {
2267            let mut call = crate::components::CarCall::new(car, floor, press_tick);
2268            call.ack_latency_ticks = ack_latency;
2269            if ack_latency == 0 {
2270                call.acknowledged_at = Some(press_tick);
2271            }
2272            if let Some(rid) = rider {
2273                call.pending_riders.push(rid);
2274            }
2275            queue.push(call);
2276        }
2277        if fresh {
2278            self.events.emit(Event::CarButtonPressed {
2279                car,
2280                floor,
2281                rider,
2282                tick: press_tick,
2283            });
2284        }
2285    }
2286}
2287
2288impl fmt::Debug for Simulation {
2289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2290        f.debug_struct("Simulation")
2291            .field("tick", &self.tick)
2292            .field("dt", &self.dt)
2293            .field("groups", &self.groups.len())
2294            .field("entities", &self.world.entity_count())
2295            .finish_non_exhaustive()
2296    }
2297}