Skip to main content

elevator_core/sim/
lifecycle.rs

1//! Rider lifecycle, population queries, and entity state control.
2//!
3//! Covers reroute/settle/despawn/disable/enable, population queries,
4//! per-entity metrics, service mode, and route invalidation. Split out
5//! from `sim.rs` to keep each concern readable.
6
7use std::collections::HashSet;
8
9use crate::components::{
10    CallDirection, Elevator, ElevatorPhase, RiderPhase, RiderPhaseKind, Route,
11};
12use crate::dispatch::ElevatorGroup;
13use crate::entity::{ElevatorId, EntityId, RiderId};
14use crate::error::SimError;
15use crate::events::Event;
16use crate::ids::GroupId;
17
18use super::Simulation;
19
20impl Simulation {
21    // ── Extension restore ────────────────────────────────────────────
22
23    /// Deserialize extension components from a snapshot.
24    ///
25    /// Call this after restoring from a snapshot and registering all
26    /// extension types via `world.register_ext::<T>(key)`.
27    ///
28    /// Returns the names of any extension types present in the snapshot
29    /// that were not registered. An empty vec means all extensions were
30    /// deserialized successfully.
31    ///
32    /// Prefer [`load_extensions_with`](Self::load_extensions_with) which
33    /// combines registration and loading in one call.
34    #[must_use]
35    pub fn load_extensions(&mut self) -> Vec<String> {
36        let Some(pending) = self
37            .world
38            .remove_resource::<crate::snapshot::PendingExtensions>()
39        else {
40            return Vec::new();
41        };
42        let unregistered = self.world.unregistered_ext_names(pending.0.keys());
43        self.world.deserialize_extensions(&pending.0);
44        unregistered
45    }
46
47    /// Register extension types and load their data from a snapshot
48    /// in one step.
49    ///
50    /// This is the recommended way to restore extensions. It replaces the
51    /// manual 3-step ceremony of `register_ext` → `load_extensions`:
52    ///
53    /// ```no_run
54    /// # use elevator_core::prelude::*;
55    /// # use elevator_core::register_extensions;
56    /// # use elevator_core::snapshot::WorldSnapshot;
57    /// # use serde::{Serialize, Deserialize};
58    /// # #[derive(Clone, Serialize, Deserialize)] struct VipTag;
59    /// # #[derive(Clone, Serialize, Deserialize)] struct TeamId;
60    /// # fn before(snapshot: WorldSnapshot) -> Result<(), SimError> {
61    /// // Before (3-step ceremony):
62    /// let mut sim = snapshot.restore(None)?;
63    /// sim.world_mut().register_ext::<VipTag>(ExtKey::from_type_name());
64    /// sim.world_mut().register_ext::<TeamId>(ExtKey::from_type_name());
65    /// sim.load_extensions();
66    /// # Ok(()) }
67    /// # fn after(snapshot: WorldSnapshot) -> Result<(), SimError> {
68    ///
69    /// // After:
70    /// let mut sim = snapshot.restore(None)?;
71    /// let unregistered = sim.load_extensions_with(|world| {
72    ///     register_extensions!(world, VipTag, TeamId);
73    /// });
74    /// assert!(unregistered.is_empty(), "missing: {unregistered:?}");
75    /// # Ok(()) }
76    /// ```
77    ///
78    /// Returns the names of any extension types in the snapshot that were
79    /// not registered. This catches "forgot to register" bugs at load time.
80    #[must_use]
81    pub fn load_extensions_with<F>(&mut self, register: F) -> Vec<String>
82    where
83        F: FnOnce(&mut crate::world::World),
84    {
85        register(&mut self.world);
86        self.load_extensions()
87    }
88
89    // ── Helpers ──────────────────────────────────────────────────────
90
91    /// Extract the `GroupId` from the current leg of a route.
92    ///
93    /// For Walk legs, looks ahead to the next leg to find the group.
94    /// Falls back to `GroupId(0)` when no route exists or no group leg is found.
95    pub(super) fn group_from_route(&self, route: Option<&Route>) -> GroupId {
96        if let Some(route) = route {
97            // Scan forward from current_leg looking for a Group or Line transport mode.
98            for leg in route.legs.iter().skip(route.current_leg) {
99                match leg.via {
100                    crate::components::TransportMode::Group(g) => return g,
101                    crate::components::TransportMode::Line(l) => {
102                        if let Some(line) = self.world.line(l) {
103                            return line.group();
104                        }
105                    }
106                    crate::components::TransportMode::Walk => {}
107                }
108            }
109        }
110        GroupId(0)
111    }
112
113    // ── Re-routing ───────────────────────────────────────────────────
114
115    /// Change a rider's destination mid-route.
116    ///
117    /// Replaces remaining route legs with a single direct leg to `new_destination`,
118    /// keeping the rider's current stop as origin.
119    ///
120    /// Returns `Err` if the rider does not exist or is not in `Waiting` phase
121    /// (riding/boarding riders cannot be rerouted until they exit).
122    ///
123    /// # Errors
124    ///
125    /// Returns [`SimError::EntityNotFound`] if `rider` does not exist.
126    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in
127    /// [`RiderPhase::Waiting`], or [`SimError::RiderHasNoStop`] if the
128    /// rider has no current stop.
129    pub fn reroute(&mut self, rider: RiderId, new_destination: EntityId) -> Result<(), SimError> {
130        let rider = rider.entity();
131        let r = self
132            .world
133            .rider(rider)
134            .ok_or(SimError::EntityNotFound(rider))?;
135
136        if r.phase != RiderPhase::Waiting {
137            return Err(SimError::WrongRiderPhase {
138                rider,
139                expected: RiderPhaseKind::Waiting,
140                actual: r.phase.kind(),
141            });
142        }
143
144        let origin = r.current_stop.ok_or(SimError::RiderHasNoStop(rider))?;
145
146        let group = self.group_from_route(self.world.route(rider));
147        self.world
148            .set_route(rider, Route::direct(origin, new_destination, group));
149
150        self.events.emit(Event::RiderRerouted {
151            rider,
152            new_destination,
153            tick: self.tick,
154        });
155
156        Ok(())
157    }
158
159    /// Replace a rider's entire remaining route.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`SimError::EntityNotFound`] if `rider` does not exist.
164    pub fn set_rider_route(&mut self, rider: EntityId, route: Route) -> Result<(), SimError> {
165        if self.world.rider(rider).is_none() {
166            return Err(SimError::EntityNotFound(rider));
167        }
168        self.world.set_route(rider, route);
169        Ok(())
170    }
171
172    // ── Rider settlement & population ─────────────────────────────
173
174    /// Transition an `Arrived` or `Abandoned` rider to `Resident` at their
175    /// current stop.
176    ///
177    /// Resident riders are parked — invisible to dispatch and loading, but
178    /// queryable via [`residents_at()`](Self::residents_at). They can later
179    /// be given a new route via [`reroute_rider()`](Self::reroute_rider).
180    ///
181    /// # Errors
182    ///
183    /// Returns [`SimError::EntityNotFound`] if `id` does not exist.
184    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in
185    /// `Arrived` or `Abandoned` phase, or [`SimError::RiderHasNoStop`]
186    /// if the rider has no current stop.
187    pub fn settle_rider(&mut self, id: RiderId) -> Result<(), SimError> {
188        let id = id.entity();
189        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
190
191        let old_phase = rider.phase;
192        match old_phase {
193            RiderPhase::Arrived | RiderPhase::Abandoned => {}
194            _ => {
195                return Err(SimError::WrongRiderPhase {
196                    rider: id,
197                    expected: RiderPhaseKind::Arrived,
198                    actual: old_phase.kind(),
199                });
200            }
201        }
202
203        let stop = rider.current_stop.ok_or(SimError::RiderHasNoStop(id))?;
204
205        // Update index: remove from old partition (only Abandoned is indexed).
206        if old_phase == RiderPhase::Abandoned {
207            self.rider_index.remove_abandoned(stop, id);
208        }
209        self.rider_index.insert_resident(stop, id);
210
211        if let Some(r) = self.world.rider_mut(id) {
212            r.phase = RiderPhase::Resident;
213        }
214
215        self.metrics.record_settle();
216        self.events.emit(Event::RiderSettled {
217            rider: id,
218            stop,
219            tick: self.tick,
220        });
221        Ok(())
222    }
223
224    /// Give a `Resident` rider a new route, transitioning them to `Waiting`.
225    ///
226    /// The rider begins waiting at their current stop for an elevator
227    /// matching the route's transport mode. If the rider has a
228    /// [`Patience`](crate::components::Patience) component, its
229    /// `waited_ticks` is reset to zero.
230    ///
231    /// # Errors
232    ///
233    /// Returns [`SimError::EntityNotFound`] if `id` does not exist.
234    /// Returns [`SimError::WrongRiderPhase`] if the rider is not in `Resident`
235    /// phase, [`SimError::EmptyRoute`] if the route has no legs, or
236    /// [`SimError::RouteOriginMismatch`] if the route's first leg origin does
237    /// not match the rider's current stop.
238    pub fn reroute_rider(&mut self, id: EntityId, route: Route) -> Result<(), SimError> {
239        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
240
241        if rider.phase != RiderPhase::Resident {
242            return Err(SimError::WrongRiderPhase {
243                rider: id,
244                expected: RiderPhaseKind::Resident,
245                actual: rider.phase.kind(),
246            });
247        }
248
249        let stop = rider.current_stop.ok_or(SimError::RiderHasNoStop(id))?;
250
251        let new_destination = route.final_destination().ok_or(SimError::EmptyRoute)?;
252
253        // Validate that the route departs from the rider's current stop.
254        if let Some(leg) = route.current()
255            && leg.from != stop
256        {
257            return Err(SimError::RouteOriginMismatch {
258                expected_origin: stop,
259                route_origin: leg.from,
260            });
261        }
262
263        self.rider_index.remove_resident(stop, id);
264        self.rider_index.insert_waiting(stop, id);
265
266        if let Some(r) = self.world.rider_mut(id) {
267            r.phase = RiderPhase::Waiting;
268            // Reset spawn_tick so manifest wait_ticks measures time since
269            // reroute, not time since the original spawn as a Resident.
270            r.spawn_tick = self.tick;
271        }
272        self.world.set_route(id, route);
273
274        // Reset patience if present.
275        if let Some(p) = self.world.patience_mut(id) {
276            p.waited_ticks = 0;
277        }
278
279        // A rerouted resident is indistinguishable from a fresh arrival —
280        // record it so predictive parking and `arrivals_at` see the demand.
281        if let Some(log) = self.world.resource_mut::<crate::arrival_log::ArrivalLog>() {
282            log.record(self.tick, stop);
283        }
284
285        self.metrics.record_reroute();
286        self.events.emit(Event::RiderRerouted {
287            rider: id,
288            new_destination,
289            tick: self.tick,
290        });
291        Ok(())
292    }
293
294    /// Remove a rider from the simulation entirely.
295    ///
296    /// Cleans up the population index, metric tags, and elevator cross-references
297    /// (if the rider is currently aboard). Emits [`Event::RiderDespawned`].
298    ///
299    /// All rider removal should go through this method rather than calling
300    /// `world.despawn()` directly, to keep the population index consistent.
301    ///
302    /// # Errors
303    ///
304    /// Returns [`SimError::EntityNotFound`] if `id` does not exist or is
305    /// not a rider.
306    pub fn despawn_rider(&mut self, id: RiderId) -> Result<(), SimError> {
307        let id = id.entity();
308        let rider = self.world.rider(id).ok_or(SimError::EntityNotFound(id))?;
309
310        // Targeted index removal based on current phase (O(1) vs O(n) scan).
311        if let Some(stop) = rider.current_stop {
312            match rider.phase {
313                RiderPhase::Waiting => self.rider_index.remove_waiting(stop, id),
314                RiderPhase::Resident => self.rider_index.remove_resident(stop, id),
315                RiderPhase::Abandoned => self.rider_index.remove_abandoned(stop, id),
316                _ => {} // Boarding/Riding/Exiting/Walking/Arrived — not indexed
317            }
318        }
319
320        if let Some(tags) = self
321            .world
322            .resource_mut::<crate::tagged_metrics::MetricTags>()
323        {
324            tags.remove_entity(id);
325        }
326
327        // Purge stale `pending_riders` entries before the entity slot
328        // is reused. `world.despawn` cleans ext storage keyed on this
329        // rider (e.g. `AssignedCar`) but not back-references living on
330        // stop/car entities.
331        self.world.scrub_rider_from_pending_calls(id);
332
333        self.world.despawn(id);
334
335        self.events.emit(Event::RiderDespawned {
336            rider: id,
337            tick: self.tick,
338        });
339        Ok(())
340    }
341
342    // ── Access control ──────────────────────────────────────────────
343
344    /// Set the allowed stops for a rider.
345    ///
346    /// When set, the rider will only be allowed to board elevators that
347    /// can take them to a stop in the allowed set. See
348    /// [`AccessControl`](crate::components::AccessControl) for details.
349    ///
350    /// # Errors
351    ///
352    /// Returns [`SimError::EntityNotFound`] if the rider does not exist.
353    pub fn set_rider_access(
354        &mut self,
355        rider: EntityId,
356        allowed_stops: HashSet<EntityId>,
357    ) -> Result<(), SimError> {
358        if self.world.rider(rider).is_none() {
359            return Err(SimError::EntityNotFound(rider));
360        }
361        self.world
362            .set_access_control(rider, crate::components::AccessControl::new(allowed_stops));
363        Ok(())
364    }
365
366    /// Set the restricted stops for an elevator.
367    ///
368    /// Riders whose current destination is in this set will be rejected
369    /// with [`RejectionReason::AccessDenied`](crate::error::RejectionReason::AccessDenied)
370    /// during the loading phase.
371    ///
372    /// # Errors
373    ///
374    /// Returns [`SimError::EntityNotFound`] if the elevator does not exist.
375    pub fn set_elevator_restricted_stops(
376        &mut self,
377        elevator: EntityId,
378        restricted_stops: HashSet<EntityId>,
379    ) -> Result<(), SimError> {
380        let car = self
381            .world
382            .elevator_mut(elevator)
383            .ok_or(SimError::EntityNotFound(elevator))?;
384        car.restricted_stops = restricted_stops;
385        Ok(())
386    }
387
388    // ── Population queries ──────────────────────────────────────────
389
390    /// Iterate over resident rider IDs at a stop (O(1) lookup).
391    pub fn residents_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
392        self.rider_index.residents_at(stop).iter().copied()
393    }
394
395    /// Count of residents at a stop (O(1)).
396    #[must_use]
397    pub fn resident_count_at(&self, stop: EntityId) -> usize {
398        self.rider_index.resident_count_at(stop)
399    }
400
401    /// Iterate over waiting rider IDs at a stop (O(1) lookup).
402    pub fn waiting_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
403        self.rider_index.waiting_at(stop).iter().copied()
404    }
405
406    /// Count of waiting riders at a stop (O(1)).
407    #[must_use]
408    pub fn waiting_count_at(&self, stop: EntityId) -> usize {
409        self.rider_index.waiting_count_at(stop)
410    }
411
412    /// Partition waiting riders at `stop` by their route direction.
413    ///
414    /// Returns `(up, down)` where `up` counts riders whose current route
415    /// destination lies above `stop` (they want to go up) and `down` counts
416    /// riders whose destination lies below. Riders without a [`Route`] or
417    /// whose current leg has no destination are excluded from both counts —
418    /// they have no intrinsic direction. The sum `up + down` may therefore
419    /// be less than [`waiting_count_at`](Self::waiting_count_at).
420    ///
421    /// Runs in `O(waiting riders at stop)`. Designed for per-frame rendering
422    /// code that wants to show up/down queues separately; dispatch strategies
423    /// should read [`HallCall`](crate::components::HallCall)s instead.
424    #[must_use]
425    pub fn waiting_direction_counts_at(&self, stop: EntityId) -> (usize, usize) {
426        let Some(origin_pos) = self.world.stop(stop).map(crate::components::Stop::position) else {
427            return (0, 0);
428        };
429        let mut up = 0usize;
430        let mut down = 0usize;
431        for rider in self.rider_index.waiting_at(stop) {
432            let Some(route) = self.world.route(*rider) else {
433                continue;
434            };
435            let Some(dest_entity) = route.current_destination() else {
436                continue;
437            };
438            let Some(dest_pos) = self
439                .world
440                .stop(dest_entity)
441                .map(crate::components::Stop::position)
442            else {
443                continue;
444            };
445            match CallDirection::between(origin_pos, dest_pos) {
446                Some(CallDirection::Up) => up += 1,
447                Some(CallDirection::Down) => down += 1,
448                None => {}
449            }
450        }
451        (up, down)
452    }
453
454    /// Iterate over abandoned rider IDs at a stop (O(1) lookup).
455    pub fn abandoned_at(&self, stop: EntityId) -> impl Iterator<Item = EntityId> + '_ {
456        self.rider_index.abandoned_at(stop).iter().copied()
457    }
458
459    /// Count of abandoned riders at a stop (O(1)).
460    #[must_use]
461    pub fn abandoned_count_at(&self, stop: EntityId) -> usize {
462        self.rider_index.abandoned_count_at(stop)
463    }
464
465    /// Get the rider entities currently aboard an elevator.
466    ///
467    /// Returns an empty slice if the elevator does not exist.
468    #[must_use]
469    pub fn riders_on(&self, elevator: EntityId) -> &[EntityId] {
470        self.world
471            .elevator(elevator)
472            .map_or(&[], |car| car.riders())
473    }
474
475    /// Get the number of riders aboard an elevator.
476    ///
477    /// Returns 0 if the elevator does not exist.
478    #[must_use]
479    pub fn occupancy(&self, elevator: EntityId) -> usize {
480        self.world
481            .elevator(elevator)
482            .map_or(0, |car| car.riders().len())
483    }
484
485    // ── Entity lifecycle ────────────────────────────────────────────
486
487    /// Disable an entity. Disabled entities are skipped by all systems.
488    ///
489    /// If the entity is an elevator in motion, it is reset to `Idle` with
490    /// zero velocity to prevent stale target references on re-enable.
491    ///
492    /// If the entity is a stop, any `Resident` riders parked there are
493    /// transitioned to `Abandoned` and appropriate events are emitted.
494    ///
495    /// Emits `EntityDisabled`. Returns `Err` if the entity does not exist.
496    ///
497    /// # Errors
498    ///
499    /// Returns [`SimError::EntityNotFound`] if `id` does not refer to a
500    /// living entity.
501    pub fn disable(&mut self, id: EntityId) -> Result<(), SimError> {
502        if !self.world.is_alive(id) {
503            return Err(SimError::EntityNotFound(id));
504        }
505        // If this is an elevator, eject all riders and reset state.
506        if let Some(car) = self.world.elevator(id) {
507            let rider_ids = car.riders.clone();
508            let pos = self.world.position(id).map_or(0.0, |p| p.value);
509            let nearest_stop = self.world.find_nearest_stop(pos);
510
511            // Drop any sticky DCS assignments pointing at this car so
512            // routed riders are not stranded behind a dead reference.
513            crate::dispatch::destination::clear_assignments_to(&mut self.world, id);
514            // Same for hall-call assignments — pre-fix, a pinned hall
515            // call to the disabled car was permanently stranded because
516            // dispatch kept committing the disabled car as the assignee
517            // and other cars couldn't take the call. (#292)
518            for hc in self.world.iter_hall_calls_mut() {
519                if hc.assigned_car == Some(id) {
520                    hc.assigned_car = None;
521                    hc.pinned = false;
522                }
523            }
524
525            for rid in &rider_ids {
526                if let Some(r) = self.world.rider_mut(*rid) {
527                    r.phase = RiderPhase::Waiting;
528                    r.current_stop = nearest_stop;
529                    r.board_tick = None;
530                }
531                if let Some(stop) = nearest_stop {
532                    self.rider_index.insert_waiting(stop, *rid);
533                    self.events.emit(Event::RiderEjected {
534                        rider: *rid,
535                        elevator: id,
536                        stop,
537                        tick: self.tick,
538                    });
539                }
540            }
541
542            let had_load = self
543                .world
544                .elevator(id)
545                .is_some_and(|c| c.current_load.value() > 0.0);
546            let capacity = self.world.elevator(id).map(|c| c.weight_capacity.value());
547            if let Some(car) = self.world.elevator_mut(id) {
548                car.riders.clear();
549                car.current_load = crate::components::Weight::ZERO;
550                car.phase = ElevatorPhase::Idle;
551                car.target_stop = None;
552            }
553            // Wipe any pressed floor buttons. On re-enable they'd
554            // otherwise resurface as active demand with stale press
555            // ticks, and dispatch would plan against a rider set that
556            // no longer exists.
557            if let Some(calls) = self.world.car_calls_mut(id) {
558                calls.clear();
559            }
560            // Tell the group's dispatcher the car left. SCAN/LOOK
561            // keep per-car direction state across ticks; without this
562            // a disabled-then-enabled car would re-enter service with
563            // whatever sweep direction it had before, potentially
564            // colliding with the new sweep state. Mirrors the
565            // `remove_elevator` / `reassign_elevator_to_line` paths in
566            // `topology.rs`, which already do this.
567            let group_id = self
568                .groups
569                .iter()
570                .find(|g| g.elevator_entities().contains(&id))
571                .map(ElevatorGroup::id);
572            if let Some(gid) = group_id
573                && let Some(dispatcher) = self.dispatchers.get_mut(&gid)
574            {
575                dispatcher.notify_removed(id);
576            }
577            if had_load && let Some(cap) = capacity {
578                self.events.emit(Event::CapacityChanged {
579                    elevator: id,
580                    current_load: ordered_float::OrderedFloat(0.0),
581                    capacity: ordered_float::OrderedFloat(cap),
582                    tick: self.tick,
583                });
584            }
585        }
586        if let Some(vel) = self.world.velocity_mut(id) {
587            vel.value = 0.0;
588        }
589
590        // If this is a stop, abandon resident riders and invalidate routes.
591        if self.world.stop(id).is_some() {
592            let resident_ids: Vec<EntityId> =
593                self.rider_index.residents_at(id).iter().copied().collect();
594            for rid in resident_ids {
595                self.rider_index.remove_resident(id, rid);
596                self.rider_index.insert_abandoned(id, rid);
597                if let Some(r) = self.world.rider_mut(rid) {
598                    r.phase = RiderPhase::Abandoned;
599                }
600                self.events.emit(Event::RiderAbandoned {
601                    rider: rid,
602                    stop: id,
603                    tick: self.tick,
604                });
605            }
606            self.invalidate_routes_for_stop(id);
607        }
608
609        self.world.disable(id);
610        self.events.emit(Event::EntityDisabled {
611            entity: id,
612            tick: self.tick,
613        });
614        Ok(())
615    }
616
617    /// Re-enable a disabled entity.
618    ///
619    /// Emits `EntityEnabled`. Returns `Err` if the entity does not exist.
620    ///
621    /// # Errors
622    ///
623    /// Returns [`SimError::EntityNotFound`] if `id` does not refer to a
624    /// living entity.
625    pub fn enable(&mut self, id: EntityId) -> Result<(), SimError> {
626        if !self.world.is_alive(id) {
627            return Err(SimError::EntityNotFound(id));
628        }
629        self.world.enable(id);
630        self.events.emit(Event::EntityEnabled {
631            entity: id,
632            tick: self.tick,
633        });
634        Ok(())
635    }
636
637    /// Invalidate routes for all riders referencing a disabled stop.
638    ///
639    /// Attempts to reroute riders to the nearest enabled alternative stop.
640    /// If no alternative exists, emits `RouteInvalidated` with `NoAlternative`.
641    fn invalidate_routes_for_stop(&mut self, disabled_stop: EntityId) {
642        use crate::events::RouteInvalidReason;
643
644        // Find the group this stop belongs to.
645        let group_stops: Vec<EntityId> = self
646            .groups
647            .iter()
648            .filter(|g| g.stop_entities().contains(&disabled_stop))
649            .flat_map(|g| g.stop_entities().iter().copied())
650            .filter(|&s| s != disabled_stop && !self.world.is_disabled(s))
651            .collect();
652
653        // Find all Waiting riders whose route references this stop.
654        // Riding riders are skipped — they'll be rerouted when they exit.
655        let rider_ids: Vec<EntityId> = self.world.rider_ids();
656        for rid in rider_ids {
657            let is_waiting = self
658                .world
659                .rider(rid)
660                .is_some_and(|r| r.phase == RiderPhase::Waiting);
661
662            if !is_waiting {
663                continue;
664            }
665
666            let references_stop = self.world.route(rid).is_some_and(|route| {
667                route
668                    .legs
669                    .iter()
670                    .skip(route.current_leg)
671                    .any(|leg| leg.to == disabled_stop || leg.from == disabled_stop)
672            });
673
674            if !references_stop {
675                continue;
676            }
677
678            // Try to find nearest alternative (excluding rider's current stop).
679            let rider_current_stop = self.world.rider(rid).and_then(|r| r.current_stop);
680
681            let disabled_stop_pos = self.world.stop(disabled_stop).map_or(0.0, |s| s.position);
682
683            let alternative = group_stops
684                .iter()
685                .filter(|&&s| Some(s) != rider_current_stop)
686                .filter_map(|&s| {
687                    self.world
688                        .stop(s)
689                        .map(|stop| (s, (stop.position - disabled_stop_pos).abs()))
690                })
691                .min_by(|a, b| a.1.total_cmp(&b.1))
692                .map(|(s, _)| s);
693
694            if let Some(alt_stop) = alternative {
695                // Reroute to nearest alternative.
696                let origin = rider_current_stop.unwrap_or(alt_stop);
697                let group = self.group_from_route(self.world.route(rid));
698                self.world
699                    .set_route(rid, Route::direct(origin, alt_stop, group));
700                self.events.emit(Event::RouteInvalidated {
701                    rider: rid,
702                    affected_stop: disabled_stop,
703                    reason: RouteInvalidReason::StopDisabled,
704                    tick: self.tick,
705                });
706            } else {
707                // No alternative — rider abandons immediately.
708                let abandon_stop = rider_current_stop.unwrap_or(disabled_stop);
709                self.events.emit(Event::RouteInvalidated {
710                    rider: rid,
711                    affected_stop: disabled_stop,
712                    reason: RouteInvalidReason::NoAlternative,
713                    tick: self.tick,
714                });
715                if let Some(r) = self.world.rider_mut(rid) {
716                    r.phase = RiderPhase::Abandoned;
717                }
718                // Fourth abandonment site (alongside the two in
719                // `advance_transient`); same stale-ID hazard. Scrub
720                // the rider from every hall/car-call pending list.
721                self.world.scrub_rider_from_pending_calls(rid);
722                if let Some(stop) = rider_current_stop {
723                    self.rider_index.remove_waiting(stop, rid);
724                    self.rider_index.insert_abandoned(stop, rid);
725                }
726                self.events.emit(Event::RiderAbandoned {
727                    rider: rid,
728                    stop: abandon_stop,
729                    tick: self.tick,
730                });
731            }
732        }
733    }
734
735    /// Check if an entity is disabled.
736    #[must_use]
737    pub fn is_disabled(&self, id: EntityId) -> bool {
738        self.world.is_disabled(id)
739    }
740
741    // ── Entity type queries ─────────────────────────────────────────
742
743    /// Check if an entity is an elevator.
744    ///
745    /// ```
746    /// use elevator_core::prelude::*;
747    ///
748    /// let sim = SimulationBuilder::demo().build().unwrap();
749    /// let stop = sim.stop_entity(StopId(0)).unwrap();
750    /// assert!(!sim.is_elevator(stop));
751    /// assert!(sim.is_stop(stop));
752    /// ```
753    #[must_use]
754    pub fn is_elevator(&self, id: EntityId) -> bool {
755        self.world.elevator(id).is_some()
756    }
757
758    /// Check if an entity is a rider.
759    #[must_use]
760    pub fn is_rider(&self, id: EntityId) -> bool {
761        self.world.rider(id).is_some()
762    }
763
764    /// Check if an entity is a stop.
765    #[must_use]
766    pub fn is_stop(&self, id: EntityId) -> bool {
767        self.world.stop(id).is_some()
768    }
769
770    // ── Aggregate queries ───────────────────────────────────────────
771
772    /// Count of elevators currently in the [`Idle`](ElevatorPhase::Idle) phase.
773    ///
774    /// Excludes disabled elevators (whose phase is reset to `Idle` on disable).
775    ///
776    /// ```
777    /// use elevator_core::prelude::*;
778    ///
779    /// let sim = SimulationBuilder::demo().build().unwrap();
780    /// assert_eq!(sim.idle_elevator_count(), 1);
781    /// ```
782    #[must_use]
783    pub fn idle_elevator_count(&self) -> usize {
784        self.world.iter_idle_elevators().count()
785    }
786
787    /// Current total weight aboard an elevator, or `None` if the entity is
788    /// not an elevator.
789    ///
790    /// ```
791    /// use elevator_core::prelude::*;
792    ///
793    /// let sim = SimulationBuilder::demo().build().unwrap();
794    /// let stop = sim.stop_entity(StopId(0)).unwrap();
795    /// assert_eq!(sim.elevator_load(ElevatorId::from(stop)), None); // not an elevator
796    /// ```
797    #[must_use]
798    pub fn elevator_load(&self, id: ElevatorId) -> Option<f64> {
799        let id = id.entity();
800        self.world.elevator(id).map(|e| e.current_load.value())
801    }
802
803    /// Whether the elevator's up-direction indicator lamp is lit.
804    ///
805    /// Returns `None` if the entity is not an elevator. See
806    /// [`Elevator::going_up`] for semantics.
807    #[must_use]
808    pub fn elevator_going_up(&self, id: EntityId) -> Option<bool> {
809        self.world.elevator(id).map(Elevator::going_up)
810    }
811
812    /// Whether the elevator's down-direction indicator lamp is lit.
813    ///
814    /// Returns `None` if the entity is not an elevator. See
815    /// [`Elevator::going_down`] for semantics.
816    #[must_use]
817    pub fn elevator_going_down(&self, id: EntityId) -> Option<bool> {
818        self.world.elevator(id).map(Elevator::going_down)
819    }
820
821    /// Direction the elevator is currently signalling, derived from the
822    /// indicator-lamp pair. Returns `None` if the entity is not an elevator.
823    #[must_use]
824    pub fn elevator_direction(&self, id: EntityId) -> Option<crate::components::Direction> {
825        self.world.elevator(id).map(Elevator::direction)
826    }
827
828    /// Count of rounded-floor transitions for an elevator (passing-floor
829    /// crossings plus arrivals). Returns `None` if the entity is not an
830    /// elevator.
831    #[must_use]
832    pub fn elevator_move_count(&self, id: EntityId) -> Option<u64> {
833        self.world.elevator(id).map(Elevator::move_count)
834    }
835
836    /// Distance the elevator would travel while braking to a stop from its
837    /// current velocity, at its configured deceleration rate.
838    ///
839    /// Uses the standard `v² / (2·a)` kinematic formula. A stationary
840    /// elevator returns `Some(0.0)`. Returns `None` if the entity is not
841    /// an elevator or lacks a velocity component.
842    ///
843    /// Useful for writing opportunistic dispatch strategies (e.g. "stop at
844    /// this floor if we can brake in time") without duplicating the physics
845    /// computation.
846    #[must_use]
847    pub fn braking_distance(&self, id: EntityId) -> Option<f64> {
848        let car = self.world.elevator(id)?;
849        let vel = self.world.velocity(id)?.value;
850        Some(crate::movement::braking_distance(
851            vel,
852            car.deceleration.value(),
853        ))
854    }
855
856    /// The position where the elevator would come to rest if it began braking
857    /// this instant. Current position plus a signed braking distance in the
858    /// direction of travel.
859    ///
860    /// Returns `None` if the entity is not an elevator or lacks the required
861    /// components.
862    #[must_use]
863    pub fn future_stop_position(&self, id: EntityId) -> Option<f64> {
864        let pos = self.world.position(id)?.value;
865        let vel = self.world.velocity(id)?.value;
866        let car = self.world.elevator(id)?;
867        let dist = crate::movement::braking_distance(vel, car.deceleration.value());
868        Some(vel.signum().mul_add(dist, pos))
869    }
870
871    /// Count of elevators currently in the given phase.
872    ///
873    /// Excludes disabled elevators (whose phase is reset to `Idle` on disable).
874    ///
875    /// ```
876    /// use elevator_core::prelude::*;
877    ///
878    /// let sim = SimulationBuilder::demo().build().unwrap();
879    /// assert_eq!(sim.elevators_in_phase(ElevatorPhase::Idle), 1);
880    /// assert_eq!(sim.elevators_in_phase(ElevatorPhase::Loading), 0);
881    /// ```
882    #[must_use]
883    pub fn elevators_in_phase(&self, phase: ElevatorPhase) -> usize {
884        self.world
885            .iter_elevators()
886            .filter(|(id, _, e)| e.phase() == phase && !self.world.is_disabled(*id))
887            .count()
888    }
889
890    // ── Service mode ────────────────────────────────────────────────
891
892    /// Set the service mode for an elevator.
893    ///
894    /// Emits [`Event::ServiceModeChanged`] if the mode actually changes.
895    ///
896    /// # Errors
897    ///
898    /// Returns [`SimError::EntityNotFound`] if the elevator does not exist.
899    pub fn set_service_mode(
900        &mut self,
901        elevator: EntityId,
902        mode: crate::components::ServiceMode,
903    ) -> Result<(), SimError> {
904        if self.world.elevator(elevator).is_none() {
905            return Err(SimError::EntityNotFound(elevator));
906        }
907        let old = self
908            .world
909            .service_mode(elevator)
910            .copied()
911            .unwrap_or_default();
912        if old == mode {
913            return Ok(());
914        }
915        // Leaving Manual: clear the pending velocity command and zero
916        // the velocity component. Otherwise a car moving at transition
917        // time is stranded — the Normal movement system only runs for
918        // MovingToStop/Repositioning phases, so velocity would linger
919        // forever without producing any position change.
920        if old == crate::components::ServiceMode::Manual {
921            if let Some(car) = self.world.elevator_mut(elevator) {
922                car.manual_target_velocity = None;
923            }
924            if let Some(v) = self.world.velocity_mut(elevator) {
925                v.value = 0.0;
926            }
927        }
928        self.world.set_service_mode(elevator, mode);
929        self.events.emit(Event::ServiceModeChanged {
930            elevator,
931            from: old,
932            to: mode,
933            tick: self.tick,
934        });
935        Ok(())
936    }
937
938    /// Get the current service mode for an elevator.
939    #[must_use]
940    pub fn service_mode(&self, elevator: EntityId) -> crate::components::ServiceMode {
941        self.world
942            .service_mode(elevator)
943            .copied()
944            .unwrap_or_default()
945    }
946}