Skip to main content

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}