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. `assigned_car`
18//! records which one.
19//! 4. **Cleared** — the assigned car arrives at this stop with its
20//! indicator lamps matching `direction` and opens doors. The HallCall
21//! is removed; an `Event::HallCallCleared` is emitted.
22
23use serde::{Deserialize, Serialize};
24
25use crate::entity::EntityId;
26
27/// Direction a hall call is requesting service in.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[non_exhaustive]
30pub enum CallDirection {
31 /// Requesting service upward (toward higher position).
32 Up,
33 /// Requesting service downward (toward lower position).
34 Down,
35}
36
37impl CallDirection {
38 /// Derive a call direction from the sign of `dest_pos - origin_pos`.
39 /// Returns `None` when the two stops share a position (no travel
40 /// needed — no hall call required).
41 #[must_use]
42 pub fn between(origin_pos: f64, dest_pos: f64) -> Option<Self> {
43 if dest_pos > origin_pos {
44 Some(Self::Up)
45 } else if dest_pos < origin_pos {
46 Some(Self::Down)
47 } else {
48 None
49 }
50 }
51
52 /// The opposite direction.
53 #[must_use]
54 pub const fn opposite(self) -> Self {
55 match self {
56 Self::Up => Self::Down,
57 Self::Down => Self::Up,
58 }
59 }
60}
61
62/// A pressed hall button at `stop` requesting service in `direction`.
63///
64/// Stored per `(stop, direction)` pair — at most two per stop. Built-in
65/// dispatch reads calls via [`DispatchManifest::hall_calls`](
66/// crate::dispatch::DispatchManifest::hall_calls).
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[non_exhaustive]
69pub struct HallCall {
70 /// Stop where the button was pressed.
71 pub stop: EntityId,
72 /// Direction the button requests.
73 pub direction: CallDirection,
74 /// Tick at which the button was first pressed.
75 pub press_tick: u64,
76 /// Tick at which dispatch first sees this call (after ack latency).
77 /// `None` while still pending acknowledgement.
78 pub acknowledged_at: Option<u64>,
79 /// Ticks the controller took to acknowledge this call, copied from
80 /// the serving group's [`ElevatorGroup::ack_latency_ticks`](
81 /// crate::dispatch::ElevatorGroup::ack_latency_ticks) when the
82 /// button was first pressed. Stored on the call itself so
83 /// `advance_transient` can tick the counter without needing to
84 /// look up the group.
85 pub ack_latency_ticks: u32,
86 /// Riders currently waiting on this call. Empty in
87 /// [`HallCallMode::Destination`](crate::dispatch::HallCallMode) mode
88 /// — calls there carry a single destination per press instead of a
89 /// shared direction.
90 pub pending_riders: Vec<EntityId>,
91 /// Destination requested at press time. Populated in
92 /// [`HallCallMode::Destination`](crate::dispatch::HallCallMode) mode
93 /// (lobby kiosk); `None` in Classic mode.
94 pub destination: Option<EntityId>,
95 /// Car assigned to this call by dispatch, if any.
96 pub assigned_car: Option<EntityId>,
97 /// When `true`, dispatch is forbidden from reassigning this call to
98 /// a different car. Set by
99 /// [`Simulation::pin_assignment`](crate::sim::Simulation::pin_assignment).
100 pub pinned: bool,
101}
102
103impl HallCall {
104 /// Create a new unacknowledged, unassigned hall call.
105 #[must_use]
106 pub const fn new(stop: EntityId, direction: CallDirection, press_tick: u64) -> Self {
107 Self {
108 stop,
109 direction,
110 press_tick,
111 acknowledged_at: None,
112 ack_latency_ticks: 0,
113 pending_riders: Vec::new(),
114 destination: None,
115 assigned_car: None,
116 pinned: false,
117 }
118 }
119
120 /// Returns `true` when dispatch is allowed to see this call (ack
121 /// latency has elapsed).
122 #[must_use]
123 pub const fn is_acknowledged(&self) -> bool {
124 self.acknowledged_at.is_some()
125 }
126}