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}