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