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