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