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()`](crate::sim::Simulation::spawn_rider)
27//!   — simple origin/destination/weight spawn (accepts `EntityId` or `StopId`).
28//! - [`Simulation::build_rider()`](crate::sim::Simulation::build_rider)
29//!   — fluent [`RiderBuilder`](crate::sim::RiderBuilder) for patience, preferences, access
30//!   control, explicit groups, multi-leg routes (accepts `EntityId` or `StopId`).
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//! - [`Simulation::abort_movement()`](crate::sim::Simulation::abort_movement)
55//!   — hard-abort an in-flight trip, braking the car to the nearest
56//!   reachable stop without opening doors (riders stay aboard).
57//!
58//! ### Persistence
59//!
60//! - [`Simulation::snapshot()`](crate::sim::Simulation::snapshot) — capture full
61//!   state as a serializable [`WorldSnapshot`](crate::snapshot::WorldSnapshot).
62//! - [`WorldSnapshot::restore()`](crate::snapshot::WorldSnapshot::restore)
63//!   — rebuild a `Simulation` from a snapshot.
64//!
65//! Everything else (phase-runners, world-level accessors, energy, tag
66//! metrics, topology queries) is available for advanced use but is not
67//! required for the common case.
68
69mod construction;
70mod lifecycle;
71mod topology;
72
73use crate::components::{
74    Accel, AccessControl, ElevatorPhase, Orientation, Patience, Preferences, Rider, RiderPhase,
75    Route, SpatialPosition, Speed, Velocity, Weight,
76};
77use crate::dispatch::{BuiltinReposition, DispatchStrategy, ElevatorGroup, RepositionStrategy};
78use crate::entity::{ElevatorId, EntityId, RiderId};
79use crate::error::{EtaError, SimError};
80use crate::events::{Event, EventBus};
81use crate::hooks::{Phase, PhaseHooks};
82use crate::ids::GroupId;
83use crate::metrics::Metrics;
84use crate::rider_index::RiderIndex;
85use crate::stop::{StopId, StopRef};
86use crate::systems::PhaseContext;
87use crate::time::TimeAdapter;
88use crate::topology::TopologyGraph;
89use crate::world::World;
90use std::collections::{BTreeMap, HashMap, HashSet};
91use std::fmt;
92use std::sync::Mutex;
93use std::time::Duration;
94
95/// Parameters for creating a new elevator at runtime.
96#[derive(Debug, Clone)]
97pub struct ElevatorParams {
98    /// Maximum travel speed (distance/tick).
99    pub max_speed: Speed,
100    /// Acceleration rate (distance/tick^2).
101    pub acceleration: Accel,
102    /// Deceleration rate (distance/tick^2).
103    pub deceleration: Accel,
104    /// Maximum weight the car can carry.
105    pub weight_capacity: Weight,
106    /// Ticks for a door open/close transition.
107    pub door_transition_ticks: u32,
108    /// Ticks the door stays fully open.
109    pub door_open_ticks: u32,
110    /// Stop entity IDs this elevator cannot serve (access restriction).
111    pub restricted_stops: HashSet<EntityId>,
112    /// Speed multiplier for Inspection mode (0.0..1.0).
113    pub inspection_speed_factor: f64,
114}
115
116impl Default for ElevatorParams {
117    fn default() -> Self {
118        Self {
119            max_speed: Speed::from(2.0),
120            acceleration: Accel::from(1.5),
121            deceleration: Accel::from(2.0),
122            weight_capacity: Weight::from(800.0),
123            door_transition_ticks: 5,
124            door_open_ticks: 10,
125            restricted_stops: HashSet::new(),
126            inspection_speed_factor: 0.25,
127        }
128    }
129}
130
131/// Parameters for creating a new line at runtime.
132#[derive(Debug, Clone)]
133pub struct LineParams {
134    /// Human-readable name.
135    pub name: String,
136    /// Dispatch group to add this line to.
137    pub group: GroupId,
138    /// Physical orientation.
139    pub orientation: Orientation,
140    /// Lowest reachable position on the line axis.
141    pub min_position: f64,
142    /// Highest reachable position on the line axis.
143    pub max_position: f64,
144    /// Optional floor-plan position.
145    pub position: Option<SpatialPosition>,
146    /// Maximum cars on this line (None = unlimited).
147    pub max_cars: Option<usize>,
148}
149
150impl LineParams {
151    /// Create line parameters with the given name and group, defaulting
152    /// everything else.
153    pub fn new(name: impl Into<String>, group: GroupId) -> Self {
154        Self {
155            name: name.into(),
156            group,
157            orientation: Orientation::default(),
158            min_position: 0.0,
159            max_position: 0.0,
160            position: None,
161            max_cars: None,
162        }
163    }
164}
165
166/// Fluent builder for spawning riders with optional configuration.
167///
168/// Created via [`Simulation::build_rider`].
169///
170/// ```
171/// use elevator_core::prelude::*;
172///
173/// let mut sim = SimulationBuilder::demo().build().unwrap();
174/// let rider = sim.build_rider(StopId(0), StopId(1))
175///     .unwrap()
176///     .weight(80.0)
177///     .spawn()
178///     .unwrap();
179/// ```
180pub struct RiderBuilder<'a> {
181    /// Mutable reference to the simulation (consumed on spawn).
182    sim: &'a mut Simulation,
183    /// Origin stop entity.
184    origin: EntityId,
185    /// Destination stop entity.
186    destination: EntityId,
187    /// Rider weight (default: 75.0).
188    weight: Weight,
189    /// Explicit dispatch group (skips auto-detection).
190    group: Option<GroupId>,
191    /// Explicit multi-leg route.
192    route: Option<Route>,
193    /// Maximum wait ticks before abandoning.
194    patience: Option<u64>,
195    /// Boarding preferences.
196    preferences: Option<Preferences>,
197    /// Per-rider access control.
198    access_control: Option<AccessControl>,
199}
200
201impl RiderBuilder<'_> {
202    /// Set the rider's weight (default: 75.0).
203    #[must_use]
204    pub fn weight(mut self, weight: impl Into<Weight>) -> Self {
205        self.weight = weight.into();
206        self
207    }
208
209    /// Set the dispatch group explicitly, skipping auto-detection.
210    #[must_use]
211    pub const fn group(mut self, group: GroupId) -> Self {
212        self.group = Some(group);
213        self
214    }
215
216    /// Provide an explicit multi-leg route.
217    #[must_use]
218    pub fn route(mut self, route: Route) -> Self {
219        self.route = Some(route);
220        self
221    }
222
223    /// Set maximum wait ticks before the rider abandons.
224    #[must_use]
225    pub const fn patience(mut self, max_wait_ticks: u64) -> Self {
226        self.patience = Some(max_wait_ticks);
227        self
228    }
229
230    /// Set boarding preferences.
231    #[must_use]
232    pub const fn preferences(mut self, prefs: Preferences) -> Self {
233        self.preferences = Some(prefs);
234        self
235    }
236
237    /// Set per-rider access control (allowed stops).
238    #[must_use]
239    pub fn access_control(mut self, ac: AccessControl) -> Self {
240        self.access_control = Some(ac);
241        self
242    }
243
244    /// Spawn the rider with the configured options.
245    ///
246    /// # Errors
247    ///
248    /// Returns [`SimError::NoRoute`] if no group serves both stops (when auto-detecting).
249    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops (when auto-detecting).
250    /// Returns [`SimError::GroupNotFound`] if an explicit group does not exist.
251    /// Returns [`SimError::RouteOriginMismatch`] if an explicit route's first leg
252    /// does not start at `origin`.
253    pub fn spawn(self) -> Result<RiderId, SimError> {
254        let route = if let Some(route) = self.route {
255            // Validate route origin matches the spawn origin.
256            if let Some(leg) = route.current()
257                && leg.from != self.origin
258            {
259                return Err(SimError::RouteOriginMismatch {
260                    expected_origin: self.origin,
261                    route_origin: leg.from,
262                });
263            }
264            route
265        } else if let Some(group) = self.group {
266            if !self.sim.groups.iter().any(|g| g.id() == group) {
267                return Err(SimError::GroupNotFound(group));
268            }
269            Route::direct(self.origin, self.destination, group)
270        } else {
271            // Auto-detect group (same logic as spawn_rider).
272            let matching: Vec<GroupId> = self
273                .sim
274                .groups
275                .iter()
276                .filter(|g| {
277                    g.stop_entities().contains(&self.origin)
278                        && g.stop_entities().contains(&self.destination)
279                })
280                .map(ElevatorGroup::id)
281                .collect();
282
283            match matching.len() {
284                0 => {
285                    let origin_groups: Vec<GroupId> = self
286                        .sim
287                        .groups
288                        .iter()
289                        .filter(|g| g.stop_entities().contains(&self.origin))
290                        .map(ElevatorGroup::id)
291                        .collect();
292                    let destination_groups: Vec<GroupId> = self
293                        .sim
294                        .groups
295                        .iter()
296                        .filter(|g| g.stop_entities().contains(&self.destination))
297                        .map(ElevatorGroup::id)
298                        .collect();
299                    return Err(SimError::NoRoute {
300                        origin: self.origin,
301                        destination: self.destination,
302                        origin_groups,
303                        destination_groups,
304                    });
305                }
306                1 => Route::direct(self.origin, self.destination, matching[0]),
307                _ => {
308                    return Err(SimError::AmbiguousRoute {
309                        origin: self.origin,
310                        destination: self.destination,
311                        groups: matching,
312                    });
313                }
314            }
315        };
316
317        let eid = self
318            .sim
319            .spawn_rider_inner(self.origin, self.destination, self.weight, route);
320
321        // Apply optional components.
322        if let Some(max_wait) = self.patience {
323            self.sim.world.set_patience(
324                eid,
325                Patience {
326                    max_wait_ticks: max_wait,
327                    waited_ticks: 0,
328                },
329            );
330        }
331        if let Some(prefs) = self.preferences {
332            self.sim.world.set_preferences(eid, prefs);
333        }
334        if let Some(ac) = self.access_control {
335            self.sim.world.set_access_control(eid, ac);
336        }
337
338        Ok(RiderId::from(eid))
339    }
340}
341
342/// The core simulation state, advanced by calling `step()`.
343pub struct Simulation {
344    /// The ECS world containing all entity data.
345    world: World,
346    /// Internal event bus — only holds events from the current tick.
347    events: EventBus,
348    /// Events from completed ticks, available to consumers via `drain_events()`.
349    pending_output: Vec<Event>,
350    /// Current simulation tick.
351    tick: u64,
352    /// Time delta per tick (seconds).
353    dt: f64,
354    /// Elevator groups in this simulation.
355    groups: Vec<ElevatorGroup>,
356    /// Config `StopId` to `EntityId` mapping for spawn helpers.
357    stop_lookup: HashMap<StopId, EntityId>,
358    /// Dispatch strategies keyed by group.
359    dispatchers: BTreeMap<GroupId, Box<dyn DispatchStrategy>>,
360    /// Serializable strategy identifiers (for snapshot).
361    strategy_ids: BTreeMap<GroupId, crate::dispatch::BuiltinStrategy>,
362    /// Reposition strategies keyed by group (optional per group).
363    repositioners: BTreeMap<GroupId, Box<dyn RepositionStrategy>>,
364    /// Serializable reposition strategy identifiers (for snapshot).
365    reposition_ids: BTreeMap<GroupId, BuiltinReposition>,
366    /// Aggregated metrics.
367    metrics: Metrics,
368    /// Time conversion utility.
369    time: TimeAdapter,
370    /// Lifecycle hooks (before/after each phase).
371    hooks: PhaseHooks,
372    /// Reusable buffer for elevator IDs (avoids per-tick allocation).
373    elevator_ids_buf: Vec<EntityId>,
374    /// Reusable buffer for reposition decisions (avoids per-tick allocation).
375    reposition_buf: Vec<(EntityId, EntityId)>,
376    /// Lazy-rebuilt connectivity graph for cross-line topology queries.
377    topo_graph: Mutex<TopologyGraph>,
378    /// Phase-partitioned reverse index for O(1) population queries.
379    rider_index: RiderIndex,
380}
381
382impl Simulation {
383    // ── Accessors ────────────────────────────────────────────────────
384
385    /// Get a shared reference to the world.
386    //
387    // Intentionally non-`const`: a `const` qualifier on a runtime accessor
388    // signals "usable in const context", which these methods are not in
389    // practice (the `World` is heap-allocated and mutated). Marking them
390    // `const` misleads readers without unlocking any call sites.
391    #[must_use]
392    #[allow(clippy::missing_const_for_fn)]
393    pub fn world(&self) -> &World {
394        &self.world
395    }
396
397    /// Get a mutable reference to the world.
398    ///
399    /// Exposed for advanced use cases (manual rider management, custom
400    /// component attachment). Prefer `spawn_rider` / `build_rider`
401    /// for standard operations.
402    #[allow(clippy::missing_const_for_fn)]
403    pub fn world_mut(&mut self) -> &mut World {
404        &mut self.world
405    }
406
407    /// Current simulation tick.
408    #[must_use]
409    pub const fn current_tick(&self) -> u64 {
410        self.tick
411    }
412
413    /// Time delta per tick (seconds).
414    #[must_use]
415    pub const fn dt(&self) -> f64 {
416        self.dt
417    }
418
419    /// Interpolated position between the previous and current tick.
420    ///
421    /// `alpha` is clamped to `[0.0, 1.0]`, where `0.0` returns the entity's
422    /// position at the start of the last completed tick and `1.0` returns
423    /// the current position. Intended for smooth rendering when a render
424    /// frame falls between simulation ticks.
425    ///
426    /// Returns `None` if the entity has no position component. Returns the
427    /// current position unchanged if no previous snapshot exists (i.e. before
428    /// the first [`step`](Self::step)).
429    ///
430    /// [`step`]: Self::step
431    #[must_use]
432    pub fn position_at(&self, id: EntityId, alpha: f64) -> Option<f64> {
433        let current = self.world.position(id)?.value;
434        let alpha = if alpha.is_nan() {
435            0.0
436        } else {
437            alpha.clamp(0.0, 1.0)
438        };
439        let prev = self.world.prev_position(id).map_or(current, |p| p.value);
440        Some((current - prev).mul_add(alpha, prev))
441    }
442
443    /// Current velocity of an entity along the shaft axis (signed: +up, -down).
444    ///
445    /// Convenience wrapper over [`World::velocity`] that returns the raw
446    /// `f64` value. Returns `None` if the entity has no velocity component.
447    #[must_use]
448    pub fn velocity(&self, id: EntityId) -> Option<f64> {
449        self.world.velocity(id).map(Velocity::value)
450    }
451
452    /// Get current simulation metrics.
453    #[must_use]
454    pub const fn metrics(&self) -> &Metrics {
455        &self.metrics
456    }
457
458    /// The time adapter for tick↔wall-clock conversion.
459    #[must_use]
460    pub const fn time(&self) -> &TimeAdapter {
461        &self.time
462    }
463
464    /// Get the elevator groups.
465    #[must_use]
466    pub fn groups(&self) -> &[ElevatorGroup] {
467        &self.groups
468    }
469
470    /// Mutable access to the group collection. Use this to flip a group
471    /// into [`HallCallMode::Destination`](crate::dispatch::HallCallMode)
472    /// or tune its `ack_latency_ticks` after construction. Changing the
473    /// line/elevator structure here is not supported — use the dedicated
474    /// topology mutators for that.
475    pub fn groups_mut(&mut self) -> &mut [ElevatorGroup] {
476        &mut self.groups
477    }
478
479    /// Resolve a config `StopId` to its runtime `EntityId`.
480    #[must_use]
481    pub fn stop_entity(&self, id: StopId) -> Option<EntityId> {
482        self.stop_lookup.get(&id).copied()
483    }
484
485    /// Resolve a [`StopRef`] to its runtime [`EntityId`].
486    fn resolve_stop(&self, stop: StopRef) -> Result<EntityId, SimError> {
487        match stop {
488            StopRef::ByEntity(id) => Ok(id),
489            StopRef::ById(sid) => self.stop_entity(sid).ok_or(SimError::StopNotFound(sid)),
490        }
491    }
492
493    /// Get the strategy identifier for a group.
494    #[must_use]
495    pub fn strategy_id(&self, group: GroupId) -> Option<&crate::dispatch::BuiltinStrategy> {
496        self.strategy_ids.get(&group)
497    }
498
499    /// Iterate over the stop ID → entity ID mapping.
500    pub fn stop_lookup_iter(&self) -> impl Iterator<Item = (&StopId, &EntityId)> {
501        self.stop_lookup.iter()
502    }
503
504    /// Peek at events pending for consumer retrieval.
505    #[must_use]
506    pub fn pending_events(&self) -> &[Event] {
507        &self.pending_output
508    }
509
510    // ── Destination queue (imperative dispatch) ────────────────────
511
512    /// Read-only view of an elevator's destination queue (FIFO of target
513    /// stop `EntityId`s).
514    ///
515    /// Returns `None` if `elev` is not an elevator entity. Returns
516    /// `Some(&[])` for elevators with an empty queue.
517    #[must_use]
518    pub fn destination_queue(&self, elev: ElevatorId) -> Option<&[EntityId]> {
519        let elev = elev.entity();
520        self.world
521            .destination_queue(elev)
522            .map(crate::components::DestinationQueue::queue)
523    }
524
525    /// Push a stop onto the back of an elevator's destination queue.
526    ///
527    /// Adjacent duplicates are suppressed: if the last entry already equals
528    /// `stop`, the queue is unchanged and no event is emitted.
529    /// Otherwise emits [`Event::DestinationQueued`].
530    ///
531    /// # Errors
532    ///
533    /// - [`SimError::NotAnElevator`] if `elev` is not an elevator.
534    /// - [`SimError::NotAStop`] if `stop` is not a stop.
535    pub fn push_destination(
536        &mut self,
537        elev: ElevatorId,
538        stop: impl Into<StopRef>,
539    ) -> Result<(), SimError> {
540        let elev = elev.entity();
541        let stop = self.resolve_stop(stop.into())?;
542        self.validate_push_targets(elev, stop)?;
543        let appended = self
544            .world
545            .destination_queue_mut(elev)
546            .is_some_and(|q| q.push_back(stop));
547        if appended {
548            self.events.emit(Event::DestinationQueued {
549                elevator: elev,
550                stop,
551                tick: self.tick,
552            });
553        }
554        Ok(())
555    }
556
557    /// Insert a stop at the front of an elevator's destination queue —
558    /// "go here next, before anything else in the queue".
559    ///
560    /// On the next `AdvanceQueue` phase (between Dispatch and Movement),
561    /// the elevator redirects to this new front if it differs from the
562    /// current target.
563    ///
564    /// Adjacent duplicates are suppressed: if the first entry already equals
565    /// `stop`, the queue is unchanged and no event is emitted.
566    ///
567    /// # Errors
568    ///
569    /// - [`SimError::NotAnElevator`] if `elev` is not an elevator.
570    /// - [`SimError::NotAStop`] if `stop` is not a stop.
571    pub fn push_destination_front(
572        &mut self,
573        elev: ElevatorId,
574        stop: impl Into<StopRef>,
575    ) -> Result<(), SimError> {
576        let elev = elev.entity();
577        let stop = self.resolve_stop(stop.into())?;
578        self.validate_push_targets(elev, stop)?;
579        let inserted = self
580            .world
581            .destination_queue_mut(elev)
582            .is_some_and(|q| q.push_front(stop));
583        if inserted {
584            self.events.emit(Event::DestinationQueued {
585                elevator: elev,
586                stop,
587                tick: self.tick,
588            });
589        }
590        Ok(())
591    }
592
593    /// Clear an elevator's destination queue.
594    ///
595    /// Does **not** affect an in-flight movement — the elevator will
596    /// finish its current leg and then go idle (since the queue is empty).
597    /// To stop a moving car immediately, use
598    /// [`abort_movement`](Self::abort_movement), which brakes the car to
599    /// the nearest reachable stop and also clears the queue.
600    ///
601    /// # Errors
602    ///
603    /// Returns [`SimError::NotAnElevator`] if `elev` is not an elevator.
604    pub fn clear_destinations(&mut self, elev: ElevatorId) -> Result<(), SimError> {
605        let elev = elev.entity();
606        if self.world.elevator(elev).is_none() {
607            return Err(SimError::NotAnElevator(elev));
608        }
609        if let Some(q) = self.world.destination_queue_mut(elev) {
610            q.clear();
611        }
612        Ok(())
613    }
614
615    /// Abort the elevator's in-flight movement and park at the nearest
616    /// reachable stop.
617    ///
618    /// Computes the minimum stopping position under the car's normal
619    /// deceleration profile (see
620    /// [`future_stop_position`](Self::future_stop_position)), picks the
621    /// closest stop at or past that position in the current direction of
622    /// travel, re-targets there via
623    /// [`ElevatorPhase::Repositioning`](crate::components::ElevatorPhase::Repositioning)
624    /// so the car arrives **without opening doors**, and clears any queued
625    /// destinations. Onboard riders stay aboard.
626    ///
627    /// Emits [`Event::MovementAborted`](crate::events::Event::MovementAborted)
628    /// when an abort occurs.
629    ///
630    /// # No-op conditions
631    ///
632    /// Returns `Ok(())` without changes if the car is not currently moving
633    /// (any phase other than
634    /// [`MovingToStop`](crate::components::ElevatorPhase::MovingToStop) or
635    /// [`Repositioning`](crate::components::ElevatorPhase::Repositioning)),
636    /// or if the simulation has no stops.
637    ///
638    /// # Errors
639    ///
640    /// Returns [`SimError::NotAnElevator`] if `elev` is not an elevator.
641    pub fn abort_movement(&mut self, elev: ElevatorId) -> Result<(), SimError> {
642        let eid = elev.entity();
643        let Some(car) = self.world.elevator(eid) else {
644            return Err(SimError::NotAnElevator(eid));
645        };
646        if !car.phase().is_moving() {
647            return Ok(());
648        }
649
650        let pos = self.world.position(eid).map_or(0.0, |p| p.value);
651        let vel = self.world.velocity(eid).map_or(0.0, |v| v.value);
652        let Some(brake_pos) = self.future_stop_position(eid) else {
653            return Ok(());
654        };
655
656        let Some(brake_stop) = brake_target_stop(&self.world, pos, vel, brake_pos) else {
657            return Ok(());
658        };
659
660        if let Some(car) = self.world.elevator_mut(eid) {
661            car.phase = ElevatorPhase::Repositioning(brake_stop);
662            car.target_stop = Some(brake_stop);
663            car.repositioning = true;
664        }
665        if let Some(q) = self.world.destination_queue_mut(eid) {
666            q.clear();
667        }
668
669        self.events.emit(Event::MovementAborted {
670            elevator: eid,
671            brake_target: brake_stop,
672            tick: self.tick,
673        });
674
675        Ok(())
676    }
677
678    /// Validate that `elev` is an elevator and `stop` is a stop.
679    fn validate_push_targets(&self, elev: EntityId, stop: EntityId) -> Result<(), SimError> {
680        if self.world.elevator(elev).is_none() {
681            return Err(SimError::NotAnElevator(elev));
682        }
683        if self.world.stop(stop).is_none() {
684            return Err(SimError::NotAStop(stop));
685        }
686        Ok(())
687    }
688
689    // ── ETA queries ─────────────────────────────────────────────────
690
691    /// Estimated time until `elev` arrives at `stop`, summing closed-form
692    /// trapezoidal travel time for every leg up to (and including) the leg
693    /// that ends at `stop`, plus the door dwell at every *intermediate* stop.
694    ///
695    /// "Arrival" is the moment the door cycle begins at `stop` — door time
696    /// at `stop` itself is **not** added; door time at earlier stops along
697    /// the route **is**.
698    ///
699    /// # Errors
700    ///
701    /// - [`EtaError::NotAnElevator`] if `elev` is not an elevator entity.
702    /// - [`EtaError::NotAStop`] if `stop` is not a stop entity.
703    /// - [`EtaError::ServiceModeExcluded`] if the elevator's
704    ///   [`ServiceMode`](crate::components::ServiceMode) is dispatch-excluded
705    ///   (`Manual` / `Independent`).
706    /// - [`EtaError::StopNotQueued`] if `stop` is neither the elevator's
707    ///   current movement target nor anywhere in its
708    ///   [`destination_queue`](Self::destination_queue).
709    /// - [`EtaError::StopVanished`] if a stop in the route lost its position
710    ///   during calculation.
711    ///
712    /// The estimate is best-effort. It assumes the queue is served in order
713    /// with no mid-trip insertions; dispatch decisions, manual door commands,
714    /// and rider boarding/exiting beyond the configured dwell will perturb
715    /// the actual arrival.
716    pub fn eta(&self, elev: ElevatorId, stop: EntityId) -> Result<Duration, EtaError> {
717        let elev = elev.entity();
718        let elevator = self
719            .world
720            .elevator(elev)
721            .ok_or(EtaError::NotAnElevator(elev))?;
722        self.world.stop(stop).ok_or(EtaError::NotAStop(stop))?;
723        let svc = self.world.service_mode(elev).copied().unwrap_or_default();
724        if svc.is_dispatch_excluded() {
725            return Err(EtaError::ServiceModeExcluded(elev));
726        }
727
728        // Build the route in service order: current target first (if any),
729        // then queue entries, with adjacent duplicates collapsed.
730        let mut route: Vec<EntityId> = Vec::new();
731        if let Some(t) = elevator.phase().moving_target() {
732            route.push(t);
733        }
734        if let Some(q) = self.world.destination_queue(elev) {
735            for &s in q.queue() {
736                if route.last() != Some(&s) {
737                    route.push(s);
738                }
739            }
740        }
741        if !route.contains(&stop) {
742            return Err(EtaError::StopNotQueued {
743                elevator: elev,
744                stop,
745            });
746        }
747
748        let max_speed = elevator.max_speed().value();
749        let accel = elevator.acceleration().value();
750        let decel = elevator.deceleration().value();
751        let door_cycle_ticks =
752            u64::from(elevator.door_transition_ticks()) * 2 + u64::from(elevator.door_open_ticks());
753        let door_cycle_secs = (door_cycle_ticks as f64) * self.dt;
754
755        // Account for any in-progress door cycle before the first travel leg:
756        // the elevator is parked at its current stop and won't move until the
757        // door FSM returns to Closed.
758        let mut total = match elevator.door() {
759            crate::door::DoorState::Opening {
760                ticks_remaining,
761                open_duration,
762                close_duration,
763            } => f64::from(*ticks_remaining + *open_duration + *close_duration) * self.dt,
764            crate::door::DoorState::Open {
765                ticks_remaining,
766                close_duration,
767            } => f64::from(*ticks_remaining + *close_duration) * self.dt,
768            crate::door::DoorState::Closing { ticks_remaining } => {
769                f64::from(*ticks_remaining) * self.dt
770            }
771            crate::door::DoorState::Closed => 0.0,
772        };
773
774        let in_door_cycle = !matches!(elevator.door(), crate::door::DoorState::Closed);
775        let mut pos = self
776            .world
777            .position(elev)
778            .ok_or(EtaError::NotAnElevator(elev))?
779            .value;
780        let vel_signed = self.world.velocity(elev).map_or(0.0, Velocity::value);
781
782        for (idx, &s) in route.iter().enumerate() {
783            let s_pos = self
784                .world
785                .stop_position(s)
786                .ok_or(EtaError::StopVanished(s))?;
787            let dist = (s_pos - pos).abs();
788            // Only the first leg can carry initial velocity, and only if
789            // the car is already moving toward this stop and not stuck in
790            // a door cycle (which forces it to stop first).
791            let v0 = if idx == 0 && !in_door_cycle && vel_signed.abs() > f64::EPSILON {
792                let dir = (s_pos - pos).signum();
793                if dir * vel_signed > 0.0 {
794                    vel_signed.abs()
795                } else {
796                    0.0
797                }
798            } else {
799                0.0
800            };
801            total += crate::eta::travel_time(dist, v0, max_speed, accel, decel);
802            if s == stop {
803                return Ok(Duration::from_secs_f64(total.max(0.0)));
804            }
805            total += door_cycle_secs;
806            pos = s_pos;
807        }
808        // `route.contains(&stop)` was true above, so the loop must hit `stop`.
809        // Fall through as a defensive backstop.
810        Err(EtaError::StopNotQueued {
811            elevator: elev,
812            stop,
813        })
814    }
815
816    /// Best ETA to `stop` across all dispatch-eligible elevators, optionally
817    /// filtered by indicator-lamp [`Direction`](crate::components::Direction).
818    ///
819    /// Pass [`Direction::Either`](crate::components::Direction::Either) to
820    /// consider every car. Otherwise, only cars whose committed direction is
821    /// `Either` or matches the requested direction are considered — useful
822    /// for hall-call assignment ("which up-going car arrives first?").
823    ///
824    /// Returns the entity ID of the winning elevator and its ETA, or `None`
825    /// if no eligible car has `stop` queued.
826    #[must_use]
827    pub fn best_eta(
828        &self,
829        stop: impl Into<StopRef>,
830        direction: crate::components::Direction,
831    ) -> Option<(EntityId, Duration)> {
832        use crate::components::Direction;
833        let stop = self.resolve_stop(stop.into()).ok()?;
834        self.world
835            .iter_elevators()
836            .filter_map(|(eid, _, elev)| {
837                let car_dir = elev.direction();
838                let direction_ok = match direction {
839                    Direction::Either => true,
840                    requested => car_dir == Direction::Either || car_dir == requested,
841                };
842                if !direction_ok {
843                    return None;
844                }
845                self.eta(ElevatorId::from(eid), stop).ok().map(|d| (eid, d))
846            })
847            .min_by_key(|(_, d)| *d)
848    }
849
850    // ── Runtime elevator upgrades ────────────────────────────────────
851    //
852    // Games that want to mutate elevator parameters at runtime (e.g.
853    // an RPG speed-upgrade purchase, a scripted capacity boost) go
854    // through these setters rather than poking `Elevator` directly via
855    // `world_mut()`. Each setter validates its input, updates the
856    // underlying component, and emits an [`Event::ElevatorUpgraded`]
857    // so game code can react without polling.
858    //
859    // ### Semantics
860    //
861    // - `max_speed`, `acceleration`, `deceleration`: applied on the next
862    //   movement integration step. The car's **current velocity is
863    //   preserved** — there is no instantaneous jerk. If `max_speed`
864    //   is lowered below the current velocity, the movement integrator
865    //   clamps velocity to the new cap on the next tick.
866    // - `weight_capacity`: applied immediately. If the new capacity is
867    //   below `current_load` the car ends up temporarily overweight —
868    //   no riders are ejected, but the next boarding pass will reject
869    //   any rider that would push the load further over the new cap.
870    // - `door_transition_ticks`, `door_open_ticks`: applied on the
871    //   **next** door cycle. An in-progress door transition keeps its
872    //   original timing, so setters never cause visual glitches.
873
874    /// Set the maximum travel speed for an elevator at runtime.
875    ///
876    /// The new value applies on the next movement integration step;
877    /// the car's current velocity is preserved (see the
878    /// [runtime upgrades section](crate#runtime-upgrades) of the crate
879    /// docs). If the new cap is below the current velocity, the movement
880    /// system clamps velocity down on the next tick.
881    ///
882    /// # Errors
883    ///
884    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
885    /// - [`SimError::InvalidConfig`] if `speed` is not a positive finite number.
886    ///
887    /// # Example
888    ///
889    /// ```
890    /// use elevator_core::prelude::*;
891    ///
892    /// let mut sim = SimulationBuilder::demo().build().unwrap();
893    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
894    /// sim.set_max_speed(elev, 4.0).unwrap();
895    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().max_speed().value(), 4.0);
896    /// ```
897    pub fn set_max_speed(&mut self, elevator: ElevatorId, speed: f64) -> Result<(), SimError> {
898        let elevator = elevator.entity();
899        Self::validate_positive_finite_f64(speed, "elevators.max_speed")?;
900        let old = self.require_elevator(elevator)?.max_speed.value();
901        let speed = Speed::from(speed);
902        if let Some(car) = self.world.elevator_mut(elevator) {
903            car.max_speed = speed;
904        }
905        self.emit_upgrade(
906            elevator,
907            crate::events::UpgradeField::MaxSpeed,
908            crate::events::UpgradeValue::float(old),
909            crate::events::UpgradeValue::float(speed.value()),
910        );
911        Ok(())
912    }
913
914    /// Set the acceleration rate for an elevator at runtime.
915    ///
916    /// See [`set_max_speed`](Self::set_max_speed) for the general
917    /// velocity-preservation rules that apply to kinematic setters.
918    ///
919    /// # Errors
920    ///
921    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
922    /// - [`SimError::InvalidConfig`] if `accel` is not a positive finite number.
923    ///
924    /// # Example
925    ///
926    /// ```
927    /// use elevator_core::prelude::*;
928    ///
929    /// let mut sim = SimulationBuilder::demo().build().unwrap();
930    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
931    /// sim.set_acceleration(elev, 3.0).unwrap();
932    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().acceleration().value(), 3.0);
933    /// ```
934    pub fn set_acceleration(&mut self, elevator: ElevatorId, accel: f64) -> Result<(), SimError> {
935        let elevator = elevator.entity();
936        Self::validate_positive_finite_f64(accel, "elevators.acceleration")?;
937        let old = self.require_elevator(elevator)?.acceleration.value();
938        let accel = Accel::from(accel);
939        if let Some(car) = self.world.elevator_mut(elevator) {
940            car.acceleration = accel;
941        }
942        self.emit_upgrade(
943            elevator,
944            crate::events::UpgradeField::Acceleration,
945            crate::events::UpgradeValue::float(old),
946            crate::events::UpgradeValue::float(accel.value()),
947        );
948        Ok(())
949    }
950
951    /// Set the deceleration rate for an elevator at runtime.
952    ///
953    /// See [`set_max_speed`](Self::set_max_speed) for the general
954    /// velocity-preservation rules that apply to kinematic setters.
955    ///
956    /// # Errors
957    ///
958    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
959    /// - [`SimError::InvalidConfig`] if `decel` is not a positive finite number.
960    ///
961    /// # Example
962    ///
963    /// ```
964    /// use elevator_core::prelude::*;
965    ///
966    /// let mut sim = SimulationBuilder::demo().build().unwrap();
967    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
968    /// sim.set_deceleration(elev, 3.5).unwrap();
969    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().deceleration().value(), 3.5);
970    /// ```
971    pub fn set_deceleration(&mut self, elevator: ElevatorId, decel: f64) -> Result<(), SimError> {
972        let elevator = elevator.entity();
973        Self::validate_positive_finite_f64(decel, "elevators.deceleration")?;
974        let old = self.require_elevator(elevator)?.deceleration.value();
975        let decel = Accel::from(decel);
976        if let Some(car) = self.world.elevator_mut(elevator) {
977            car.deceleration = decel;
978        }
979        self.emit_upgrade(
980            elevator,
981            crate::events::UpgradeField::Deceleration,
982            crate::events::UpgradeValue::float(old),
983            crate::events::UpgradeValue::float(decel.value()),
984        );
985        Ok(())
986    }
987
988    /// Set the weight capacity for an elevator at runtime.
989    ///
990    /// Applied immediately. If the new capacity is below the car's
991    /// current load the car is temporarily overweight; no riders are
992    /// ejected, but subsequent boarding attempts that would push load
993    /// further over the cap will be rejected as
994    /// [`RejectionReason::OverCapacity`](crate::error::RejectionReason::OverCapacity).
995    ///
996    /// # Errors
997    ///
998    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
999    /// - [`SimError::InvalidConfig`] if `capacity` is not a positive finite number.
1000    ///
1001    /// # Example
1002    ///
1003    /// ```
1004    /// use elevator_core::prelude::*;
1005    ///
1006    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1007    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1008    /// sim.set_weight_capacity(elev, 1200.0).unwrap();
1009    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().weight_capacity().value(), 1200.0);
1010    /// ```
1011    pub fn set_weight_capacity(
1012        &mut self,
1013        elevator: ElevatorId,
1014        capacity: f64,
1015    ) -> Result<(), SimError> {
1016        let elevator = elevator.entity();
1017        Self::validate_positive_finite_f64(capacity, "elevators.weight_capacity")?;
1018        let old = self.require_elevator(elevator)?.weight_capacity.value();
1019        let capacity = Weight::from(capacity);
1020        if let Some(car) = self.world.elevator_mut(elevator) {
1021            car.weight_capacity = capacity;
1022        }
1023        self.emit_upgrade(
1024            elevator,
1025            crate::events::UpgradeField::WeightCapacity,
1026            crate::events::UpgradeValue::float(old),
1027            crate::events::UpgradeValue::float(capacity.value()),
1028        );
1029        Ok(())
1030    }
1031
1032    /// Set the door open/close transition duration for an elevator.
1033    ///
1034    /// Applied on the **next** door cycle — an in-progress transition
1035    /// keeps its original timing to avoid visual glitches.
1036    ///
1037    /// # Errors
1038    ///
1039    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1040    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
1041    ///
1042    /// # Example
1043    ///
1044    /// ```
1045    /// use elevator_core::prelude::*;
1046    ///
1047    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1048    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1049    /// sim.set_door_transition_ticks(elev, 3).unwrap();
1050    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().door_transition_ticks(), 3);
1051    /// ```
1052    pub fn set_door_transition_ticks(
1053        &mut self,
1054        elevator: ElevatorId,
1055        ticks: u32,
1056    ) -> Result<(), SimError> {
1057        let elevator = elevator.entity();
1058        Self::validate_nonzero_u32(ticks, "elevators.door_transition_ticks")?;
1059        let old = self.require_elevator(elevator)?.door_transition_ticks;
1060        if let Some(car) = self.world.elevator_mut(elevator) {
1061            car.door_transition_ticks = ticks;
1062        }
1063        self.emit_upgrade(
1064            elevator,
1065            crate::events::UpgradeField::DoorTransitionTicks,
1066            crate::events::UpgradeValue::ticks(old),
1067            crate::events::UpgradeValue::ticks(ticks),
1068        );
1069        Ok(())
1070    }
1071
1072    /// Set how long doors hold fully open for an elevator.
1073    ///
1074    /// Applied on the **next** door cycle — a door that is currently
1075    /// holding open will complete its original dwell before the new
1076    /// value takes effect.
1077    ///
1078    /// # Errors
1079    ///
1080    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1081    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
1082    ///
1083    /// # Example
1084    ///
1085    /// ```
1086    /// use elevator_core::prelude::*;
1087    ///
1088    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1089    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1090    /// sim.set_door_open_ticks(elev, 20).unwrap();
1091    /// assert_eq!(sim.world().elevator(elev.entity()).unwrap().door_open_ticks(), 20);
1092    /// ```
1093    pub fn set_door_open_ticks(
1094        &mut self,
1095        elevator: ElevatorId,
1096        ticks: u32,
1097    ) -> Result<(), SimError> {
1098        let elevator = elevator.entity();
1099        Self::validate_nonzero_u32(ticks, "elevators.door_open_ticks")?;
1100        let old = self.require_elevator(elevator)?.door_open_ticks;
1101        if let Some(car) = self.world.elevator_mut(elevator) {
1102            car.door_open_ticks = ticks;
1103        }
1104        self.emit_upgrade(
1105            elevator,
1106            crate::events::UpgradeField::DoorOpenTicks,
1107            crate::events::UpgradeValue::ticks(old),
1108            crate::events::UpgradeValue::ticks(ticks),
1109        );
1110        Ok(())
1111    }
1112
1113    // ── Manual door control ──────────────────────────────────────────
1114    //
1115    // These methods let games drive door state directly — e.g. a
1116    // cab-panel open/close button in a first-person game, or an RPG
1117    // where the player *is* the elevator and decides when to cycle doors.
1118    //
1119    // Each method either applies the command immediately (if the car is
1120    // in a matching door-FSM state) or queues it on the elevator for
1121    // application at the next valid moment. This way games can call
1122    // these any time without worrying about FSM timing, and get a clean
1123    // success/failure split between "bad entity" and "bad moment".
1124
1125    /// Request the doors to open.
1126    ///
1127    /// Applied immediately if the car is stopped at a stop with closed
1128    /// or closing doors; otherwise queued until the car next arrives.
1129    /// A no-op if the doors are already open or opening.
1130    ///
1131    /// # Errors
1132    ///
1133    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1134    /// - [`SimError::ElevatorDisabled`] if the elevator is disabled.
1135    ///
1136    /// # Example
1137    ///
1138    /// ```
1139    /// use elevator_core::prelude::*;
1140    ///
1141    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1142    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1143    /// sim.open_door(elev).unwrap();
1144    /// ```
1145    pub fn open_door(&mut self, elevator: ElevatorId) -> Result<(), SimError> {
1146        let elevator = elevator.entity();
1147        self.require_enabled_elevator(elevator)?;
1148        self.enqueue_door_command(elevator, crate::door::DoorCommand::Open);
1149        Ok(())
1150    }
1151
1152    /// Request the doors to close now.
1153    ///
1154    /// Applied immediately if the doors are open or loading — forcing an
1155    /// early close — unless a rider is mid-boarding/exiting this car, in
1156    /// which case the close waits for the rider to finish. If doors are
1157    /// currently opening, the close queues and fires once fully open.
1158    ///
1159    /// # Errors
1160    ///
1161    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1162    /// - [`SimError::ElevatorDisabled`] if the elevator is disabled.
1163    ///
1164    /// # Example
1165    ///
1166    /// ```
1167    /// use elevator_core::prelude::*;
1168    ///
1169    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1170    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1171    /// sim.close_door(elev).unwrap();
1172    /// ```
1173    pub fn close_door(&mut self, elevator: ElevatorId) -> Result<(), SimError> {
1174        let elevator = elevator.entity();
1175        self.require_enabled_elevator(elevator)?;
1176        self.enqueue_door_command(elevator, crate::door::DoorCommand::Close);
1177        Ok(())
1178    }
1179
1180    /// Extend the doors' open dwell by `ticks`.
1181    ///
1182    /// Cumulative — two calls of 30 ticks each extend the dwell by 60
1183    /// ticks in total. If the doors aren't open yet, the hold is queued
1184    /// and applied when they next reach the fully-open state.
1185    ///
1186    /// # Errors
1187    ///
1188    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1189    /// - [`SimError::ElevatorDisabled`] if the elevator is disabled.
1190    /// - [`SimError::InvalidConfig`] if `ticks` is zero.
1191    ///
1192    /// # Example
1193    ///
1194    /// ```
1195    /// use elevator_core::prelude::*;
1196    ///
1197    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1198    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1199    /// sim.hold_door(elev, 30).unwrap();
1200    /// ```
1201    pub fn hold_door(&mut self, elevator: ElevatorId, ticks: u32) -> Result<(), SimError> {
1202        let elevator = elevator.entity();
1203        Self::validate_nonzero_u32(ticks, "hold_door.ticks")?;
1204        self.require_enabled_elevator(elevator)?;
1205        self.enqueue_door_command(elevator, crate::door::DoorCommand::HoldOpen { ticks });
1206        Ok(())
1207    }
1208
1209    /// Cancel any pending hold extension.
1210    ///
1211    /// If the base open timer has already elapsed the doors close on
1212    /// the next doors-phase tick.
1213    ///
1214    /// # Errors
1215    ///
1216    /// - [`SimError::NotAnElevator`] if `elevator` is not an elevator entity.
1217    /// - [`SimError::ElevatorDisabled`] if the elevator is disabled.
1218    ///
1219    /// # Example
1220    ///
1221    /// ```
1222    /// use elevator_core::prelude::*;
1223    ///
1224    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1225    /// let elev = ElevatorId::from(sim.world().iter_elevators().next().unwrap().0);
1226    /// sim.hold_door(elev, 100).unwrap();
1227    /// sim.cancel_door_hold(elev).unwrap();
1228    /// ```
1229    pub fn cancel_door_hold(&mut self, elevator: ElevatorId) -> Result<(), SimError> {
1230        let elevator = elevator.entity();
1231        self.require_enabled_elevator(elevator)?;
1232        self.enqueue_door_command(elevator, crate::door::DoorCommand::CancelHold);
1233        Ok(())
1234    }
1235
1236    /// Set the target velocity for a manual-mode elevator.
1237    ///
1238    /// The velocity is clamped to the elevator's `[-max_speed, max_speed]`
1239    /// range after validation. The car ramps toward the target each tick
1240    /// using `acceleration` (speeding up, or starting from rest) or
1241    /// `deceleration` (slowing down, or reversing direction). Positive
1242    /// values command upward travel, negative values command downward travel.
1243    ///
1244    /// # Errors
1245    /// - [`SimError::NotAnElevator`] if the entity is not an elevator.
1246    /// - [`SimError::ElevatorDisabled`] if the elevator is disabled.
1247    /// - [`SimError::WrongServiceMode`] if the elevator is not in [`ServiceMode::Manual`].
1248    /// - [`SimError::InvalidConfig`] if `velocity` is not finite (NaN or infinite).
1249    ///
1250    /// [`ServiceMode::Manual`]: crate::components::ServiceMode::Manual
1251    pub fn set_target_velocity(
1252        &mut self,
1253        elevator: ElevatorId,
1254        velocity: f64,
1255    ) -> Result<(), SimError> {
1256        let elevator = elevator.entity();
1257        self.require_enabled_elevator(elevator)?;
1258        self.require_manual_mode(elevator)?;
1259        if !velocity.is_finite() {
1260            return Err(SimError::InvalidConfig {
1261                field: "target_velocity",
1262                reason: format!("must be finite, got {velocity}"),
1263            });
1264        }
1265        let max = self
1266            .world
1267            .elevator(elevator)
1268            .map_or(f64::INFINITY, |c| c.max_speed.value());
1269        let clamped = velocity.clamp(-max, max);
1270        if let Some(car) = self.world.elevator_mut(elevator) {
1271            car.manual_target_velocity = Some(clamped);
1272        }
1273        self.events.emit(Event::ManualVelocityCommanded {
1274            elevator,
1275            target_velocity: Some(ordered_float::OrderedFloat(clamped)),
1276            tick: self.tick,
1277        });
1278        Ok(())
1279    }
1280
1281    /// Command an immediate stop on a manual-mode elevator.
1282    ///
1283    /// Sets the target velocity to zero; the car decelerates at its
1284    /// configured `deceleration` rate. Equivalent to
1285    /// `set_target_velocity(elevator, 0.0)` but emits a distinct
1286    /// [`Event::ManualVelocityCommanded`] with `None` payload so games can
1287    /// distinguish an emergency stop from a deliberate hold.
1288    ///
1289    /// # Errors
1290    /// Same as [`set_target_velocity`](Self::set_target_velocity), minus
1291    /// the finite-velocity check.
1292    pub fn emergency_stop(&mut self, elevator: ElevatorId) -> Result<(), SimError> {
1293        let elevator = elevator.entity();
1294        self.require_enabled_elevator(elevator)?;
1295        self.require_manual_mode(elevator)?;
1296        if let Some(car) = self.world.elevator_mut(elevator) {
1297            car.manual_target_velocity = Some(0.0);
1298        }
1299        self.events.emit(Event::ManualVelocityCommanded {
1300            elevator,
1301            target_velocity: None,
1302            tick: self.tick,
1303        });
1304        Ok(())
1305    }
1306
1307    /// Internal: require an elevator be in `ServiceMode::Manual`.
1308    fn require_manual_mode(&self, elevator: EntityId) -> Result<(), SimError> {
1309        let actual = self
1310            .world
1311            .service_mode(elevator)
1312            .copied()
1313            .unwrap_or_default();
1314        if actual != crate::components::ServiceMode::Manual {
1315            return Err(SimError::WrongServiceMode {
1316                entity: elevator,
1317                expected: crate::components::ServiceMode::Manual,
1318                actual,
1319            });
1320        }
1321        Ok(())
1322    }
1323
1324    /// Internal: push a command onto the queue, collapsing adjacent
1325    /// duplicates, capping length, and emitting `DoorCommandQueued`.
1326    fn enqueue_door_command(&mut self, elevator: EntityId, command: crate::door::DoorCommand) {
1327        if let Some(car) = self.world.elevator_mut(elevator) {
1328            let q = &mut car.door_command_queue;
1329            // Collapse adjacent duplicates for idempotent commands
1330            // (Open/Close/CancelHold) — repeating them adds nothing.
1331            // HoldOpen is explicitly cumulative, so never collapsed.
1332            let collapse = matches!(
1333                command,
1334                crate::door::DoorCommand::Open
1335                    | crate::door::DoorCommand::Close
1336                    | crate::door::DoorCommand::CancelHold
1337            ) && q.last().copied() == Some(command);
1338            if !collapse {
1339                q.push(command);
1340                if q.len() > crate::components::DOOR_COMMAND_QUEUE_CAP {
1341                    q.remove(0);
1342                }
1343            }
1344        }
1345        self.events.emit(Event::DoorCommandQueued {
1346            elevator,
1347            command,
1348            tick: self.tick,
1349        });
1350    }
1351
1352    /// Internal: resolve an elevator entity that is not disabled.
1353    fn require_enabled_elevator(&self, elevator: EntityId) -> Result<(), SimError> {
1354        if self.world.elevator(elevator).is_none() {
1355            return Err(SimError::NotAnElevator(elevator));
1356        }
1357        if self.world.is_disabled(elevator) {
1358            return Err(SimError::ElevatorDisabled(elevator));
1359        }
1360        Ok(())
1361    }
1362
1363    /// Internal: resolve an elevator entity or return a clear error.
1364    fn require_elevator(
1365        &self,
1366        elevator: EntityId,
1367    ) -> Result<&crate::components::Elevator, SimError> {
1368        self.world
1369            .elevator(elevator)
1370            .ok_or(SimError::NotAnElevator(elevator))
1371    }
1372
1373    /// Internal: positive-finite validator matching the construction-time
1374    /// error shape in `sim/construction.rs::validate_elevator_config`.
1375    fn validate_positive_finite_f64(value: f64, field: &'static str) -> Result<(), SimError> {
1376        if !value.is_finite() {
1377            return Err(SimError::InvalidConfig {
1378                field,
1379                reason: format!("must be finite, got {value}"),
1380            });
1381        }
1382        if value <= 0.0 {
1383            return Err(SimError::InvalidConfig {
1384                field,
1385                reason: format!("must be positive, got {value}"),
1386            });
1387        }
1388        Ok(())
1389    }
1390
1391    /// Internal: reject zero-tick timings.
1392    fn validate_nonzero_u32(value: u32, field: &'static str) -> Result<(), SimError> {
1393        if value == 0 {
1394            return Err(SimError::InvalidConfig {
1395                field,
1396                reason: "must be > 0".into(),
1397            });
1398        }
1399        Ok(())
1400    }
1401
1402    /// Internal: emit a single `ElevatorUpgraded` event for the current tick.
1403    fn emit_upgrade(
1404        &mut self,
1405        elevator: EntityId,
1406        field: crate::events::UpgradeField,
1407        old: crate::events::UpgradeValue,
1408        new: crate::events::UpgradeValue,
1409    ) {
1410        self.events.emit(Event::ElevatorUpgraded {
1411            elevator,
1412            field,
1413            old,
1414            new,
1415            tick: self.tick,
1416        });
1417    }
1418
1419    // Dispatch & reposition management live in `sim/construction.rs`.
1420
1421    // ── Tagging ──────────────────────────────────────────────────────
1422
1423    /// Attach a metric tag to an entity (rider, stop, elevator, etc.).
1424    ///
1425    /// Tags enable per-tag metric breakdowns. An entity can have multiple tags.
1426    /// Riders automatically inherit tags from their origin stop when spawned.
1427    ///
1428    /// # Errors
1429    ///
1430    /// Returns [`SimError::EntityNotFound`] if the entity does not exist in
1431    /// the world.
1432    pub fn tag_entity(&mut self, id: EntityId, tag: impl Into<String>) -> Result<(), SimError> {
1433        if !self.world.is_alive(id) {
1434            return Err(SimError::EntityNotFound(id));
1435        }
1436        if let Some(tags) = self
1437            .world
1438            .resource_mut::<crate::tagged_metrics::MetricTags>()
1439        {
1440            tags.tag(id, tag);
1441        }
1442        Ok(())
1443    }
1444
1445    /// Remove a metric tag from an entity.
1446    pub fn untag_entity(&mut self, id: EntityId, tag: &str) {
1447        if let Some(tags) = self
1448            .world
1449            .resource_mut::<crate::tagged_metrics::MetricTags>()
1450        {
1451            tags.untag(id, tag);
1452        }
1453    }
1454
1455    /// Query the metric accumulator for a specific tag.
1456    #[must_use]
1457    pub fn metrics_for_tag(&self, tag: &str) -> Option<&crate::tagged_metrics::TaggedMetric> {
1458        self.world
1459            .resource::<crate::tagged_metrics::MetricTags>()
1460            .and_then(|tags| tags.metric(tag))
1461    }
1462
1463    /// List all registered metric tags.
1464    pub fn all_tags(&self) -> Vec<&str> {
1465        self.world
1466            .resource::<crate::tagged_metrics::MetricTags>()
1467            .map_or_else(Vec::new, |tags| tags.all_tags().collect())
1468    }
1469
1470    // ── Rider spawning ───────────────────────────────────────────────
1471
1472    /// Create a rider builder for fluent rider spawning.
1473    ///
1474    /// Accepts [`EntityId`] or [`StopId`] for origin and destination
1475    /// (anything that implements `Into<StopRef>`).
1476    ///
1477    /// # Errors
1478    ///
1479    /// Returns [`SimError::StopNotFound`] if a [`StopId`] does not exist
1480    /// in the building configuration.
1481    ///
1482    /// ```
1483    /// use elevator_core::prelude::*;
1484    ///
1485    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1486    /// let rider = sim.build_rider(StopId(0), StopId(1))
1487    ///     .unwrap()
1488    ///     .weight(80.0)
1489    ///     .spawn()
1490    ///     .unwrap();
1491    /// ```
1492    pub fn build_rider(
1493        &mut self,
1494        origin: impl Into<StopRef>,
1495        destination: impl Into<StopRef>,
1496    ) -> Result<RiderBuilder<'_>, SimError> {
1497        let origin = self.resolve_stop(origin.into())?;
1498        let destination = self.resolve_stop(destination.into())?;
1499        Ok(RiderBuilder {
1500            sim: self,
1501            origin,
1502            destination,
1503            weight: Weight::from(75.0),
1504            group: None,
1505            route: None,
1506            patience: None,
1507            preferences: None,
1508            access_control: None,
1509        })
1510    }
1511
1512    /// Spawn a rider with default preferences (convenience shorthand).
1513    ///
1514    /// Equivalent to `build_rider(origin, destination)?.weight(weight).spawn()`.
1515    /// Use [`build_rider`](Self::build_rider) instead when you need to set
1516    /// patience, preferences, access control, or an explicit route.
1517    ///
1518    /// Auto-detects the elevator group by finding groups that serve both origin
1519    /// and destination stops.
1520    ///
1521    /// # Errors
1522    ///
1523    /// Returns [`SimError::NoRoute`] if no group serves both stops.
1524    /// Returns [`SimError::AmbiguousRoute`] if multiple groups serve both stops.
1525    pub fn spawn_rider(
1526        &mut self,
1527        origin: impl Into<StopRef>,
1528        destination: impl Into<StopRef>,
1529        weight: impl Into<Weight>,
1530    ) -> Result<RiderId, SimError> {
1531        let origin = self.resolve_stop(origin.into())?;
1532        let destination = self.resolve_stop(destination.into())?;
1533        let weight: Weight = weight.into();
1534        let matching: Vec<GroupId> = self
1535            .groups
1536            .iter()
1537            .filter(|g| {
1538                g.stop_entities().contains(&origin) && g.stop_entities().contains(&destination)
1539            })
1540            .map(ElevatorGroup::id)
1541            .collect();
1542
1543        let group = match matching.len() {
1544            0 => {
1545                let origin_groups: Vec<GroupId> = self
1546                    .groups
1547                    .iter()
1548                    .filter(|g| g.stop_entities().contains(&origin))
1549                    .map(ElevatorGroup::id)
1550                    .collect();
1551                let destination_groups: Vec<GroupId> = self
1552                    .groups
1553                    .iter()
1554                    .filter(|g| g.stop_entities().contains(&destination))
1555                    .map(ElevatorGroup::id)
1556                    .collect();
1557                return Err(SimError::NoRoute {
1558                    origin,
1559                    destination,
1560                    origin_groups,
1561                    destination_groups,
1562                });
1563            }
1564            1 => matching[0],
1565            _ => {
1566                return Err(SimError::AmbiguousRoute {
1567                    origin,
1568                    destination,
1569                    groups: matching,
1570                });
1571            }
1572        };
1573
1574        let route = Route::direct(origin, destination, group);
1575        Ok(RiderId::from(self.spawn_rider_inner(
1576            origin,
1577            destination,
1578            weight,
1579            route,
1580        )))
1581    }
1582
1583    /// Internal helper: spawn a rider entity with the given route.
1584    fn spawn_rider_inner(
1585        &mut self,
1586        origin: EntityId,
1587        destination: EntityId,
1588        weight: Weight,
1589        route: Route,
1590    ) -> EntityId {
1591        let eid = self.world.spawn();
1592        self.world.set_rider(
1593            eid,
1594            Rider {
1595                weight,
1596                phase: RiderPhase::Waiting,
1597                current_stop: Some(origin),
1598                spawn_tick: self.tick,
1599                board_tick: None,
1600            },
1601        );
1602        self.world.set_route(eid, route);
1603        self.rider_index.insert_waiting(origin, eid);
1604        self.events.emit(Event::RiderSpawned {
1605            rider: eid,
1606            origin,
1607            destination,
1608            tick: self.tick,
1609        });
1610
1611        // Auto-press the hall button for this rider. Direction is the
1612        // sign of `dest_pos - origin_pos`; if the two coincide (walk
1613        // leg, identity trip) no call is registered.
1614        if let (Some(op), Some(dp)) = (
1615            self.world.stop_position(origin),
1616            self.world.stop_position(destination),
1617        ) && let Some(direction) = crate::components::CallDirection::between(op, dp)
1618        {
1619            self.register_hall_call_for_rider(origin, direction, eid, destination);
1620        }
1621
1622        // Auto-tag the rider with "stop:{name}" for per-stop wait time tracking.
1623        let stop_tag = self
1624            .world
1625            .stop(origin)
1626            .map(|s| format!("stop:{}", s.name()));
1627
1628        // Inherit metric tags from the origin stop.
1629        if let Some(tags_res) = self
1630            .world
1631            .resource_mut::<crate::tagged_metrics::MetricTags>()
1632        {
1633            let origin_tags: Vec<String> = tags_res.tags_for(origin).to_vec();
1634            for tag in origin_tags {
1635                tags_res.tag(eid, tag);
1636            }
1637            // Apply the origin stop tag.
1638            if let Some(tag) = stop_tag {
1639                tags_res.tag(eid, tag);
1640            }
1641        }
1642
1643        eid
1644    }
1645
1646    /// Drain all pending events from completed ticks.
1647    ///
1648    /// Events emitted during `step()` (or per-phase methods) are buffered
1649    /// and made available here after `advance_tick()` is called.
1650    /// Events emitted outside the tick loop (e.g., `spawn_rider`, `disable`)
1651    /// are also included.
1652    ///
1653    /// ```
1654    /// use elevator_core::prelude::*;
1655    ///
1656    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1657    ///
1658    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
1659    /// sim.step();
1660    ///
1661    /// let events = sim.drain_events();
1662    /// assert!(!events.is_empty());
1663    /// ```
1664    pub fn drain_events(&mut self) -> Vec<Event> {
1665        // Flush any events still in the bus (from spawn_rider, disable, etc.)
1666        self.pending_output.extend(self.events.drain());
1667        std::mem::take(&mut self.pending_output)
1668    }
1669
1670    /// Push an event into the pending output buffer (crate-internal).
1671    pub(crate) fn push_event(&mut self, event: Event) {
1672        self.pending_output.push(event);
1673    }
1674
1675    /// Drain only events matching a predicate.
1676    ///
1677    /// Events that don't match the predicate remain in the buffer
1678    /// and will be returned by future `drain_events` or
1679    /// `drain_events_where` calls.
1680    ///
1681    /// ```
1682    /// use elevator_core::prelude::*;
1683    ///
1684    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1685    /// sim.spawn_rider(StopId(0), StopId(1), 70.0).unwrap();
1686    /// sim.step();
1687    ///
1688    /// let spawns: Vec<Event> = sim.drain_events_where(|e| {
1689    ///     matches!(e, Event::RiderSpawned { .. })
1690    /// });
1691    /// ```
1692    pub fn drain_events_where(&mut self, predicate: impl Fn(&Event) -> bool) -> Vec<Event> {
1693        // Flush bus into pending_output first.
1694        self.pending_output.extend(self.events.drain());
1695
1696        let mut matched = Vec::new();
1697        let mut remaining = Vec::new();
1698        for event in std::mem::take(&mut self.pending_output) {
1699            if predicate(&event) {
1700                matched.push(event);
1701            } else {
1702                remaining.push(event);
1703            }
1704        }
1705        self.pending_output = remaining;
1706        matched
1707    }
1708
1709    // ── Sub-stepping ────────────────────────────────────────────────
1710
1711    /// Get the dispatch strategies map (for advanced sub-stepping).
1712    #[must_use]
1713    pub fn dispatchers(&self) -> &BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1714        &self.dispatchers
1715    }
1716
1717    /// Get the dispatch strategies map mutably (for advanced sub-stepping).
1718    pub fn dispatchers_mut(&mut self) -> &mut BTreeMap<GroupId, Box<dyn DispatchStrategy>> {
1719        &mut self.dispatchers
1720    }
1721
1722    /// Get a mutable reference to the event bus.
1723    pub const fn events_mut(&mut self) -> &mut EventBus {
1724        &mut self.events
1725    }
1726
1727    /// Get a mutable reference to the metrics.
1728    pub const fn metrics_mut(&mut self) -> &mut Metrics {
1729        &mut self.metrics
1730    }
1731
1732    /// Build the `PhaseContext` for the current tick.
1733    #[must_use]
1734    pub const fn phase_context(&self) -> PhaseContext {
1735        PhaseContext {
1736            tick: self.tick,
1737            dt: self.dt,
1738        }
1739    }
1740
1741    /// Run only the `advance_transient` phase (with hooks).
1742    ///
1743    /// # Phase ordering
1744    ///
1745    /// When calling individual phase methods instead of [`step()`](Self::step),
1746    /// phases **must** be called in this order each tick:
1747    ///
1748    /// 1. `run_advance_transient`
1749    /// 2. `run_dispatch`
1750    /// 3. `run_reposition`
1751    /// 4. `run_advance_queue`
1752    /// 5. `run_movement`
1753    /// 6. `run_doors`
1754    /// 7. `run_loading`
1755    /// 8. `run_metrics`
1756    ///
1757    /// Out-of-order execution may cause riders to board with closed doors,
1758    /// elevators to move before dispatch, or transient states to persist
1759    /// across tick boundaries.
1760    pub fn run_advance_transient(&mut self) {
1761        self.hooks
1762            .run_before(Phase::AdvanceTransient, &mut self.world);
1763        for group in &self.groups {
1764            self.hooks
1765                .run_before_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1766        }
1767        let ctx = self.phase_context();
1768        crate::systems::advance_transient::run(
1769            &mut self.world,
1770            &mut self.events,
1771            &ctx,
1772            &mut self.rider_index,
1773        );
1774        for group in &self.groups {
1775            self.hooks
1776                .run_after_group(Phase::AdvanceTransient, group.id(), &mut self.world);
1777        }
1778        self.hooks
1779            .run_after(Phase::AdvanceTransient, &mut self.world);
1780    }
1781
1782    /// Run only the dispatch phase (with hooks).
1783    pub fn run_dispatch(&mut self) {
1784        self.hooks.run_before(Phase::Dispatch, &mut self.world);
1785        for group in &self.groups {
1786            self.hooks
1787                .run_before_group(Phase::Dispatch, group.id(), &mut self.world);
1788        }
1789        let ctx = self.phase_context();
1790        crate::systems::dispatch::run(
1791            &mut self.world,
1792            &mut self.events,
1793            &ctx,
1794            &self.groups,
1795            &mut self.dispatchers,
1796            &self.rider_index,
1797        );
1798        for group in &self.groups {
1799            self.hooks
1800                .run_after_group(Phase::Dispatch, group.id(), &mut self.world);
1801        }
1802        self.hooks.run_after(Phase::Dispatch, &mut self.world);
1803    }
1804
1805    /// Run only the movement phase (with hooks).
1806    pub fn run_movement(&mut self) {
1807        self.hooks.run_before(Phase::Movement, &mut self.world);
1808        for group in &self.groups {
1809            self.hooks
1810                .run_before_group(Phase::Movement, group.id(), &mut self.world);
1811        }
1812        let ctx = self.phase_context();
1813        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1814        crate::systems::movement::run(
1815            &mut self.world,
1816            &mut self.events,
1817            &ctx,
1818            &self.elevator_ids_buf,
1819            &mut self.metrics,
1820        );
1821        for group in &self.groups {
1822            self.hooks
1823                .run_after_group(Phase::Movement, group.id(), &mut self.world);
1824        }
1825        self.hooks.run_after(Phase::Movement, &mut self.world);
1826    }
1827
1828    /// Run only the doors phase (with hooks).
1829    pub fn run_doors(&mut self) {
1830        self.hooks.run_before(Phase::Doors, &mut self.world);
1831        for group in &self.groups {
1832            self.hooks
1833                .run_before_group(Phase::Doors, group.id(), &mut self.world);
1834        }
1835        let ctx = self.phase_context();
1836        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1837        crate::systems::doors::run(
1838            &mut self.world,
1839            &mut self.events,
1840            &ctx,
1841            &self.elevator_ids_buf,
1842        );
1843        for group in &self.groups {
1844            self.hooks
1845                .run_after_group(Phase::Doors, group.id(), &mut self.world);
1846        }
1847        self.hooks.run_after(Phase::Doors, &mut self.world);
1848    }
1849
1850    /// Run only the loading phase (with hooks).
1851    pub fn run_loading(&mut self) {
1852        self.hooks.run_before(Phase::Loading, &mut self.world);
1853        for group in &self.groups {
1854            self.hooks
1855                .run_before_group(Phase::Loading, group.id(), &mut self.world);
1856        }
1857        let ctx = self.phase_context();
1858        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1859        crate::systems::loading::run(
1860            &mut self.world,
1861            &mut self.events,
1862            &ctx,
1863            &self.elevator_ids_buf,
1864            &mut self.rider_index,
1865        );
1866        for group in &self.groups {
1867            self.hooks
1868                .run_after_group(Phase::Loading, group.id(), &mut self.world);
1869        }
1870        self.hooks.run_after(Phase::Loading, &mut self.world);
1871    }
1872
1873    /// Run only the advance-queue phase (with hooks).
1874    ///
1875    /// Reconciles each elevator's phase/target with the front of its
1876    /// [`DestinationQueue`](crate::components::DestinationQueue). Runs
1877    /// between Reposition and Movement.
1878    pub fn run_advance_queue(&mut self) {
1879        self.hooks.run_before(Phase::AdvanceQueue, &mut self.world);
1880        for group in &self.groups {
1881            self.hooks
1882                .run_before_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1883        }
1884        let ctx = self.phase_context();
1885        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1886        crate::systems::advance_queue::run(
1887            &mut self.world,
1888            &mut self.events,
1889            &ctx,
1890            &self.elevator_ids_buf,
1891        );
1892        for group in &self.groups {
1893            self.hooks
1894                .run_after_group(Phase::AdvanceQueue, group.id(), &mut self.world);
1895        }
1896        self.hooks.run_after(Phase::AdvanceQueue, &mut self.world);
1897    }
1898
1899    /// Run only the reposition phase (with hooks).
1900    ///
1901    /// Global before/after hooks always fire even when no [`RepositionStrategy`]
1902    /// is configured. Per-group hooks only fire for groups that have a
1903    /// repositioner — this differs from other phases where per-group hooks
1904    /// fire unconditionally.
1905    pub fn run_reposition(&mut self) {
1906        self.hooks.run_before(Phase::Reposition, &mut self.world);
1907        if !self.repositioners.is_empty() {
1908            // Only run per-group hooks for groups that have a repositioner.
1909            for group in &self.groups {
1910                if self.repositioners.contains_key(&group.id()) {
1911                    self.hooks
1912                        .run_before_group(Phase::Reposition, group.id(), &mut self.world);
1913                }
1914            }
1915            let ctx = self.phase_context();
1916            crate::systems::reposition::run(
1917                &mut self.world,
1918                &mut self.events,
1919                &ctx,
1920                &self.groups,
1921                &mut self.repositioners,
1922                &mut self.reposition_buf,
1923            );
1924            for group in &self.groups {
1925                if self.repositioners.contains_key(&group.id()) {
1926                    self.hooks
1927                        .run_after_group(Phase::Reposition, group.id(), &mut self.world);
1928                }
1929            }
1930        }
1931        self.hooks.run_after(Phase::Reposition, &mut self.world);
1932    }
1933
1934    /// Run the energy system (no hooks — inline phase).
1935    #[cfg(feature = "energy")]
1936    fn run_energy(&mut self) {
1937        let ctx = self.phase_context();
1938        self.world.elevator_ids_into(&mut self.elevator_ids_buf);
1939        crate::systems::energy::run(
1940            &mut self.world,
1941            &mut self.events,
1942            &ctx,
1943            &self.elevator_ids_buf,
1944        );
1945    }
1946
1947    /// Run only the metrics phase (with hooks).
1948    pub fn run_metrics(&mut self) {
1949        self.hooks.run_before(Phase::Metrics, &mut self.world);
1950        for group in &self.groups {
1951            self.hooks
1952                .run_before_group(Phase::Metrics, group.id(), &mut self.world);
1953        }
1954        let ctx = self.phase_context();
1955        crate::systems::metrics::run(
1956            &mut self.world,
1957            &self.events,
1958            &mut self.metrics,
1959            &ctx,
1960            &self.groups,
1961        );
1962        for group in &self.groups {
1963            self.hooks
1964                .run_after_group(Phase::Metrics, group.id(), &mut self.world);
1965        }
1966        self.hooks.run_after(Phase::Metrics, &mut self.world);
1967    }
1968
1969    // Phase-hook registration lives in `sim/construction.rs`.
1970
1971    /// Increment the tick counter and flush events to the output buffer.
1972    ///
1973    /// Call after running all desired phases. Events emitted during this tick
1974    /// are moved to the output buffer and available via `drain_events()`.
1975    pub fn advance_tick(&mut self) {
1976        self.pending_output.extend(self.events.drain());
1977        self.tick += 1;
1978    }
1979
1980    /// Advance the simulation by one tick.
1981    ///
1982    /// Events from this tick are buffered internally and available via
1983    /// `drain_events()`. The metrics system only processes events from
1984    /// the current tick, regardless of whether the consumer drains them.
1985    ///
1986    /// ```
1987    /// use elevator_core::prelude::*;
1988    ///
1989    /// let mut sim = SimulationBuilder::demo().build().unwrap();
1990    /// sim.step();
1991    /// assert_eq!(sim.current_tick(), 1);
1992    /// ```
1993    pub fn step(&mut self) {
1994        self.world.snapshot_prev_positions();
1995        self.run_advance_transient();
1996        self.run_dispatch();
1997        self.run_reposition();
1998        self.run_advance_queue();
1999        self.run_movement();
2000        self.run_doors();
2001        self.run_loading();
2002        #[cfg(feature = "energy")]
2003        self.run_energy();
2004        self.run_metrics();
2005        self.advance_tick();
2006    }
2007
2008    // ── Hall / car call API ─────────────────────────────────────────
2009
2010    /// Press an up/down hall button at `stop` without associating it
2011    /// with any particular rider. Useful for scripted NPCs, player
2012    /// input, or cutscene cues.
2013    ///
2014    /// If a call in this direction already exists at `stop`, the press
2015    /// tick is left untouched (first press wins for latency purposes).
2016    ///
2017    /// # Errors
2018    /// Returns [`SimError::EntityNotFound`] if `stop` is not a valid
2019    /// stop entity.
2020    pub fn press_hall_button(
2021        &mut self,
2022        stop: impl Into<StopRef>,
2023        direction: crate::components::CallDirection,
2024    ) -> Result<(), SimError> {
2025        let stop = self.resolve_stop(stop.into())?;
2026        if self.world.stop(stop).is_none() {
2027            return Err(SimError::EntityNotFound(stop));
2028        }
2029        self.ensure_hall_call(stop, direction, None, None);
2030        Ok(())
2031    }
2032
2033    /// Press a floor button from inside `car`. No-op if the car already
2034    /// has a pending call for `floor`.
2035    ///
2036    /// # Errors
2037    /// Returns [`SimError::EntityNotFound`] if `car` or `floor` is invalid.
2038    pub fn press_car_button(
2039        &mut self,
2040        car: ElevatorId,
2041        floor: impl Into<StopRef>,
2042    ) -> Result<(), SimError> {
2043        let car = car.entity();
2044        let floor = self.resolve_stop(floor.into())?;
2045        if self.world.elevator(car).is_none() {
2046            return Err(SimError::EntityNotFound(car));
2047        }
2048        if self.world.stop(floor).is_none() {
2049            return Err(SimError::EntityNotFound(floor));
2050        }
2051        self.ensure_car_call(car, floor, None);
2052        Ok(())
2053    }
2054
2055    /// Pin the hall call at `(stop, direction)` to `car`. Dispatch is
2056    /// forbidden from reassigning the call to a different car until
2057    /// [`unpin_assignment`](Self::unpin_assignment) is called or the
2058    /// call is cleared.
2059    ///
2060    /// # Errors
2061    /// - [`SimError::EntityNotFound`] — `car` is not a valid elevator.
2062    /// - [`SimError::HallCallNotFound`] — no hall call exists at that
2063    ///   `(stop, direction)` pair yet.
2064    /// - [`SimError::LineDoesNotServeStop`] — the car's line does not
2065    ///   serve `stop`. Without this check a cross-line pin would be
2066    ///   silently dropped at dispatch time yet leave the call `pinned`,
2067    ///   blocking every other car.
2068    pub fn pin_assignment(
2069        &mut self,
2070        car: ElevatorId,
2071        stop: EntityId,
2072        direction: crate::components::CallDirection,
2073    ) -> Result<(), SimError> {
2074        let car = car.entity();
2075        let Some(elev) = self.world.elevator(car) else {
2076            return Err(SimError::EntityNotFound(car));
2077        };
2078        let car_line = elev.line;
2079        // Validate the car's line can reach the stop. If the line has
2080        // an entry in any group, we consult its `serves` list. A car
2081        // whose line entity doesn't match any line in any group falls
2082        // through — older test fixtures create elevators without a
2083        // line entity, and we don't want to regress them.
2084        let line_serves_stop = self
2085            .groups
2086            .iter()
2087            .flat_map(|g| g.lines().iter())
2088            .find(|li| li.entity() == car_line)
2089            .map(|li| li.serves().contains(&stop));
2090        if line_serves_stop == Some(false) {
2091            return Err(SimError::LineDoesNotServeStop {
2092                line_or_car: car,
2093                stop,
2094            });
2095        }
2096        let Some(call) = self.world.hall_call_mut(stop, direction) else {
2097            return Err(SimError::HallCallNotFound { stop, direction });
2098        };
2099        call.assigned_car = Some(car);
2100        call.pinned = true;
2101        Ok(())
2102    }
2103
2104    /// Release a previous pin at `(stop, direction)`. No-op if the call
2105    /// doesn't exist or wasn't pinned.
2106    pub fn unpin_assignment(
2107        &mut self,
2108        stop: EntityId,
2109        direction: crate::components::CallDirection,
2110    ) {
2111        if let Some(call) = self.world.hall_call_mut(stop, direction) {
2112            call.pinned = false;
2113        }
2114    }
2115
2116    /// Iterate every active hall call across the simulation. Yields a
2117    /// reference per live `(stop, direction)` press; games use this to
2118    /// render lobby lamp states, pending-rider counts, or per-floor
2119    /// button animations.
2120    pub fn hall_calls(&self) -> impl Iterator<Item = &crate::components::HallCall> {
2121        self.world.iter_hall_calls()
2122    }
2123
2124    /// Floor buttons currently pressed inside `car`. Returns an empty
2125    /// slice when the car has no aboard riders or hasn't been used.
2126    #[must_use]
2127    pub fn car_calls(&self, car: ElevatorId) -> &[crate::components::CarCall] {
2128        let car = car.entity();
2129        self.world.car_calls(car)
2130    }
2131
2132    /// Car currently assigned to serve the call at `(stop, direction)`,
2133    /// if dispatch has made an assignment yet.
2134    #[must_use]
2135    pub fn assigned_car(
2136        &self,
2137        stop: EntityId,
2138        direction: crate::components::CallDirection,
2139    ) -> Option<EntityId> {
2140        self.world
2141            .hall_call(stop, direction)
2142            .and_then(|c| c.assigned_car)
2143    }
2144
2145    /// Estimated ticks remaining before the assigned car reaches the
2146    /// call at `(stop, direction)`.
2147    ///
2148    /// # Errors
2149    ///
2150    /// - [`EtaError::NotAStop`] if no hall call exists at `(stop, direction)`.
2151    /// - [`EtaError::StopNotQueued`] if no car is assigned to the call.
2152    /// - [`EtaError::NotAnElevator`] if the assigned car has no positional
2153    ///   data or is not a valid elevator.
2154    pub fn eta_for_call(
2155        &self,
2156        stop: EntityId,
2157        direction: crate::components::CallDirection,
2158    ) -> Result<u64, EtaError> {
2159        let call = self
2160            .world
2161            .hall_call(stop, direction)
2162            .ok_or(EtaError::NotAStop(stop))?;
2163        let car = call.assigned_car.ok_or(EtaError::NoCarAssigned(stop))?;
2164        let car_pos = self
2165            .world
2166            .position(car)
2167            .ok_or(EtaError::NotAnElevator(car))?
2168            .value;
2169        let stop_pos = self
2170            .world
2171            .stop_position(stop)
2172            .ok_or(EtaError::StopVanished(stop))?;
2173        let max_speed = self
2174            .world
2175            .elevator(car)
2176            .ok_or(EtaError::NotAnElevator(car))?
2177            .max_speed()
2178            .value();
2179        if max_speed <= 0.0 {
2180            return Err(EtaError::NotAnElevator(car));
2181        }
2182        let distance = (car_pos - stop_pos).abs();
2183        // Simple kinematic estimate. The `eta` module has a richer
2184        // trapezoidal model; the one-liner suits most hall-display use.
2185        Ok((distance / max_speed).ceil() as u64)
2186    }
2187
2188    // ── Internal helpers ────────────────────────────────────────────
2189
2190    /// Register (or aggregate) a hall call on behalf of a specific
2191    /// rider, including their destination in DCS mode.
2192    fn register_hall_call_for_rider(
2193        &mut self,
2194        stop: EntityId,
2195        direction: crate::components::CallDirection,
2196        rider: EntityId,
2197        destination: EntityId,
2198    ) {
2199        let mode = self
2200            .groups
2201            .iter()
2202            .find(|g| g.stop_entities().contains(&stop))
2203            .map(crate::dispatch::ElevatorGroup::hall_call_mode);
2204        let dest = match mode {
2205            Some(crate::dispatch::HallCallMode::Destination) => Some(destination),
2206            _ => None,
2207        };
2208        self.ensure_hall_call(stop, direction, Some(rider), dest);
2209    }
2210
2211    /// Create or aggregate into the hall call at `(stop, direction)`.
2212    /// Emits [`Event::HallButtonPressed`] only on the *first* press.
2213    fn ensure_hall_call(
2214        &mut self,
2215        stop: EntityId,
2216        direction: crate::components::CallDirection,
2217        rider: Option<EntityId>,
2218        destination: Option<EntityId>,
2219    ) {
2220        let mut fresh_press = false;
2221        if self.world.hall_call(stop, direction).is_none() {
2222            let mut call = crate::components::HallCall::new(stop, direction, self.tick);
2223            call.destination = destination;
2224            call.ack_latency_ticks = self.ack_latency_for_stop(stop);
2225            if call.ack_latency_ticks == 0 {
2226                // Controller has zero-tick latency — mark acknowledged
2227                // immediately so dispatch sees the call this same tick.
2228                call.acknowledged_at = Some(self.tick);
2229            }
2230            if let Some(rid) = rider {
2231                call.pending_riders.push(rid);
2232            }
2233            self.world.set_hall_call(call);
2234            fresh_press = true;
2235        } else if let Some(existing) = self.world.hall_call_mut(stop, direction) {
2236            if let Some(rid) = rider
2237                && !existing.pending_riders.contains(&rid)
2238            {
2239                existing.pending_riders.push(rid);
2240            }
2241            // Prefer a populated destination over None; don't overwrite
2242            // an existing destination even if a later press omits it.
2243            if existing.destination.is_none() {
2244                existing.destination = destination;
2245            }
2246        }
2247        if fresh_press {
2248            self.events.emit(Event::HallButtonPressed {
2249                stop,
2250                direction,
2251                tick: self.tick,
2252            });
2253            // Zero-latency controllers acknowledge on the press tick.
2254            if let Some(call) = self.world.hall_call(stop, direction)
2255                && call.acknowledged_at == Some(self.tick)
2256            {
2257                self.events.emit(Event::HallCallAcknowledged {
2258                    stop,
2259                    direction,
2260                    tick: self.tick,
2261                });
2262            }
2263        }
2264    }
2265
2266    /// Ack latency for the group whose `members` slice contains `entity`.
2267    /// Defaults to 0 if no group matches (unreachable in normal builds).
2268    fn ack_latency_for(
2269        &self,
2270        entity: EntityId,
2271        members: impl Fn(&crate::dispatch::ElevatorGroup) -> &[EntityId],
2272    ) -> u32 {
2273        self.groups
2274            .iter()
2275            .find(|g| members(g).contains(&entity))
2276            .map_or(0, crate::dispatch::ElevatorGroup::ack_latency_ticks)
2277    }
2278
2279    /// Ack latency for the group that owns `stop` (0 if no group).
2280    fn ack_latency_for_stop(&self, stop: EntityId) -> u32 {
2281        self.ack_latency_for(stop, crate::dispatch::ElevatorGroup::stop_entities)
2282    }
2283
2284    /// Ack latency for the group that owns `car` (0 if no group).
2285    fn ack_latency_for_car(&self, car: EntityId) -> u32 {
2286        self.ack_latency_for(car, crate::dispatch::ElevatorGroup::elevator_entities)
2287    }
2288
2289    /// Create or aggregate into a car call for `(car, floor)`.
2290    /// Emits [`Event::CarButtonPressed`] on first press; repeat presses
2291    /// by other riders append to `pending_riders` without re-emitting.
2292    fn ensure_car_call(&mut self, car: EntityId, floor: EntityId, rider: Option<EntityId>) {
2293        let press_tick = self.tick;
2294        let ack_latency = self.ack_latency_for_car(car);
2295        let Some(queue) = self.world.car_calls_mut(car) else {
2296            debug_assert!(
2297                false,
2298                "ensure_car_call: car {car:?} has no car_calls component"
2299            );
2300            return;
2301        };
2302        let existing_idx = queue.iter().position(|c| c.floor == floor);
2303        let fresh = existing_idx.is_none();
2304        if let Some(idx) = existing_idx {
2305            if let Some(rid) = rider
2306                && !queue[idx].pending_riders.contains(&rid)
2307            {
2308                queue[idx].pending_riders.push(rid);
2309            }
2310        } else {
2311            let mut call = crate::components::CarCall::new(car, floor, press_tick);
2312            call.ack_latency_ticks = ack_latency;
2313            if ack_latency == 0 {
2314                call.acknowledged_at = Some(press_tick);
2315            }
2316            if let Some(rid) = rider {
2317                call.pending_riders.push(rid);
2318            }
2319            queue.push(call);
2320        }
2321        if fresh {
2322            self.events.emit(Event::CarButtonPressed {
2323                car,
2324                floor,
2325                rider,
2326                tick: press_tick,
2327            });
2328        }
2329    }
2330}
2331
2332impl fmt::Debug for Simulation {
2333    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2334        f.debug_struct("Simulation")
2335            .field("tick", &self.tick)
2336            .field("dt", &self.dt)
2337            .field("groups", &self.groups.len())
2338            .field("entities", &self.world.entity_count())
2339            .finish_non_exhaustive()
2340    }
2341}
2342
2343/// Pick the stop to park at when aborting an in-flight movement.
2344///
2345/// Tries three strategies in order:
2346///
2347/// 1. **Closest stop at or past `brake_pos` in the direction of travel.**
2348///    The car can decelerate into it naturally without overshoot.
2349/// 2. **Farthest stop still in the direction of travel (end-of-line).**
2350///    If the car is too close to the end of the line to fit a full
2351///    deceleration, pick the terminal stop ahead of it; the movement
2352///    system's overshoot-snap will absorb the small residual distance.
2353/// 3. **Nearest stop overall.** Only reachable when the car has no
2354///    stops ahead of it at all (e.g., single-stop worlds or the car is
2355///    already past the final stop); also handles the `vel == 0` case.
2356///
2357/// Returns `None` only if the world has no stops at all.
2358fn brake_target_stop(world: &World, pos: f64, vel: f64, brake_pos: f64) -> Option<EntityId> {
2359    let dir = vel.signum();
2360    if dir != 0.0 {
2361        let ahead_of_brake = world
2362            .iter_stops()
2363            .filter(|(_, stop)| (stop.position() - brake_pos) * dir >= 0.0)
2364            .min_by(|(_, a), (_, b)| {
2365                (a.position() - pos)
2366                    .abs()
2367                    .total_cmp(&(b.position() - pos).abs())
2368            })
2369            .map(|(id, _)| id);
2370        if ahead_of_brake.is_some() {
2371            return ahead_of_brake;
2372        }
2373        let ahead_of_car = world
2374            .iter_stops()
2375            .filter(|(_, stop)| (stop.position() - pos) * dir >= 0.0)
2376            .max_by(|(_, a), (_, b)| {
2377                ((a.position() - pos) * dir).total_cmp(&((b.position() - pos) * dir))
2378            })
2379            .map(|(id, _)| id);
2380        if ahead_of_car.is_some() {
2381            return ahead_of_car;
2382        }
2383    }
2384    world.find_nearest_stop(brake_pos)
2385}