elevator_core/components/hall_call.rs
1//! Hall calls: the "up"/"down" buttons at each stop.
2//!
3//! A [`HallCall`] is the sim's representation of a pressed hall button.
4//! At most two calls exist per stop (one per [`CallDirection`]), aggregated
5//! across every rider who wants to go that direction. Calls are the unit
6//! dispatch strategies see — not riders — so the sim can model real
7//! collective-control elevators where a car doesn't know *who* is waiting,
8//! only that someone going up has pressed the button on floor N.
9//!
10//! ## Lifecycle
11//!
12//! 1. **Pressed** — a rider spawns or a game explicitly calls
13//! [`Simulation::press_hall_button`](crate::sim::Simulation::press_hall_button).
14//! `HallCall::press_tick` is set; `acknowledged_at` is `None`.
15//! 2. **Acknowledged** — after the group's `ack_latency_ticks` have elapsed,
16//! `acknowledged_at` is set and the call becomes visible to dispatch.
17//! 3. **Assigned** — dispatch pairs the call with a car. The assignment
18//! is keyed per-line in [`HallCall::assigned_cars_by_line`] so a stop
19//! shared by multiple lines (e.g. a sky-lobby served by low, high, and
20//! express banks) can record every bank's choice independently. Within
21//! a single line the latest assignment replaces the previous one; across
22//! lines the map grows until the call is cleared or the car is removed.
23//! 4. **Cleared** — the assigned car arrives at this stop with its
24//! indicator lamps matching `direction` and opens doors. The HallCall
25//! is removed; an `Event::HallCallCleared` is emitted.
26
27use std::collections::BTreeMap;
28
29use serde::{Deserialize, Serialize};
30
31use crate::entity::EntityId;
32
33/// Direction a hall call is requesting service in.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[non_exhaustive]
36pub enum CallDirection {
37 /// Requesting service upward (toward higher position).
38 Up,
39 /// Requesting service downward (toward lower position).
40 Down,
41}
42
43impl CallDirection {
44 /// Derive a call direction from the sign of `dest_pos - origin_pos`.
45 /// Returns `None` when the two stops share a position (no travel
46 /// needed — no hall call required).
47 #[must_use]
48 pub fn between(origin_pos: f64, dest_pos: f64) -> Option<Self> {
49 if dest_pos > origin_pos {
50 Some(Self::Up)
51 } else if dest_pos < origin_pos {
52 Some(Self::Down)
53 } else {
54 None
55 }
56 }
57
58 /// The opposite direction.
59 #[must_use]
60 pub const fn opposite(self) -> Self {
61 match self {
62 Self::Up => Self::Down,
63 Self::Down => Self::Up,
64 }
65 }
66}
67
68/// A pressed hall button at `stop` requesting service in `direction`.
69///
70/// Stored per `(stop, direction)` pair — at most two per stop. Built-in
71/// dispatch reads calls via [`DispatchManifest::iter_hall_calls`](
72/// crate::dispatch::DispatchManifest::iter_hall_calls).
73#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
74#[non_exhaustive]
75pub struct HallCall {
76 /// Stop where the button was pressed.
77 pub stop: EntityId,
78 /// Direction the button requests.
79 pub direction: CallDirection,
80 /// Tick at which the button was first pressed.
81 pub press_tick: u64,
82 /// Tick at which dispatch first sees this call (after ack latency).
83 /// `None` while still pending acknowledgement.
84 pub acknowledged_at: Option<u64>,
85 /// Ticks the controller took to acknowledge this call, copied from
86 /// the serving group's [`ElevatorGroup::ack_latency_ticks`](
87 /// crate::dispatch::ElevatorGroup::ack_latency_ticks) when the
88 /// button was first pressed. Stored on the call itself so
89 /// `advance_transient` can tick the counter without needing to
90 /// look up the group.
91 pub ack_latency_ticks: u32,
92 /// Riders currently waiting on this call. Empty in
93 /// [`HallCallMode::Destination`](crate::dispatch::HallCallMode) mode
94 /// — calls there carry a single destination per press instead of a
95 /// shared direction.
96 pub pending_riders: Vec<EntityId>,
97 /// Destination requested at press time. Populated in
98 /// [`HallCallMode::Destination`](crate::dispatch::HallCallMode) mode
99 /// (lobby kiosk); `None` in Classic mode.
100 pub destination: Option<EntityId>,
101 /// Cars assigned to this call by dispatch, keyed by the line entity
102 /// the car runs on. A stop served by multiple lines can hold one
103 /// entry per line simultaneously — the low-bank car, the express
104 /// car, and the service car can all be en route to the same lobby
105 /// without one overwriting another. Within a single line the latest
106 /// assignment replaces the previous one.
107 ///
108 /// Pre-15.23 snapshots stored a single `assigned_car: Option<EntityId>`
109 /// field. Those snapshots silently drop the transient assignment on
110 /// load (serde's default unknown-field handling); the next dispatch
111 /// pass repopulates this map.
112 #[serde(default)]
113 pub assigned_cars_by_line: BTreeMap<EntityId, EntityId>,
114 /// When `true`, dispatch is forbidden from reassigning this call to
115 /// a different car. Set by
116 /// [`Simulation::pin_assignment`](crate::sim::Simulation::pin_assignment).
117 pub pinned: bool,
118}
119
120impl HallCall {
121 /// Create a new unacknowledged, unassigned hall call.
122 #[must_use]
123 pub const fn new(stop: EntityId, direction: CallDirection, press_tick: u64) -> Self {
124 Self {
125 stop,
126 direction,
127 press_tick,
128 acknowledged_at: None,
129 ack_latency_ticks: 0,
130 pending_riders: Vec::new(),
131 destination: None,
132 assigned_cars_by_line: BTreeMap::new(),
133 pinned: false,
134 }
135 }
136
137 /// Returns `true` when dispatch is allowed to see this call (ack
138 /// latency has elapsed).
139 #[must_use]
140 pub const fn is_acknowledged(&self) -> bool {
141 self.acknowledged_at.is_some()
142 }
143
144 /// Any car currently assigned to this call, preferring the entry
145 /// with the numerically smallest line-entity key (stable across
146 /// ticks because `BTreeMap` iteration is ordered). `None` when no
147 /// line has recorded an assignment yet.
148 ///
149 /// This matches the pre-per-line `assigned_car` semantics for
150 /// callers that just want "is anyone coming?" The richer shape is
151 /// available directly on [`assigned_cars_by_line`](Self::assigned_cars_by_line).
152 #[must_use]
153 pub fn any_assigned_car(&self) -> Option<EntityId> {
154 self.assigned_cars_by_line.values().next().copied()
155 }
156}