Skip to main content

elevator_core/dispatch/
loop_sweep.rs

1//! `LoopSweep` — call-driven dispatch for [`LineKind::Loop`] groups.
2//!
3//! On a one-way closed loop, "dispatch" reduces to a label: the
4//! `systems::dispatch` phase already kickstarts an `Idle` Loop car onto
5//! its forward-next stop and excludes Loop cars from the Hungarian idle
6//! pool, and the door FSM hands the car straight from `DoorClosing`
7//! back to `MovingToStop(next)` without ever passing through
8//! `Stopped`. The loading phase boards every eligible rider regardless
9//! of the linear up/down lamps, so a Loop car serves every waiter at
10//! every served stop on every lap.
11//!
12//! That continuous-patrol behaviour is the LoopSweep contract from
13//! `docs/plans/loop-lines-v1.md`. This struct exists so that:
14//!
15//! - Loop groups have a typed default that round-trips through
16//!   snapshots and config files via [`BuiltinStrategy::LoopSweep`]
17//!   instead of silently inheriting [`BuiltinStrategy::Scan`] — which
18//!   would replay any restored sim with the wrong identity.
19//! - The construction-time validation can name the only strategy a
20//!   Loop group is allowed to carry, rejecting Linear-only strategies
21//!   loud rather than silently misbehaving.
22//!
23//! All [`DispatchStrategy`] hooks fall back to defaults: Loop cars
24//! never reach the Hungarian, so [`rank`](DispatchStrategy::rank) is
25//! unreachable in practice, and there is no per-car or per-pass scratch
26//! that needs to round-trip — the whole struct is unit-shaped.
27//!
28//! Future Loop-aware behaviour (skip-empty-stops, headway-driven hold
29//! recovery) will land in successors (`LoopSchedule`).
30//!
31//! [`LineKind::Loop`]: crate::components::LineKind::Loop
32
33use super::{BuiltinStrategy, DispatchStrategy, RankContext};
34
35/// Dispatch strategy for [`LineKind::Loop`] groups.
36///
37/// See the module-level documentation for the full contract. The struct
38/// holds no per-pass state — Loop cars patrol forward on their own and
39/// never enter the Hungarian assignment — so it is a unit struct. The
40/// `Serialize`/`Deserialize` derives keep it round-trip-compatible with
41/// the snapshot identity layer for symmetry with the other built-ins.
42///
43/// [`LineKind::Loop`]: crate::components::LineKind::Loop
44#[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
45pub struct LoopSweepDispatch;
46
47impl LoopSweepDispatch {
48    /// Construct a fresh `LoopSweepDispatch`. Equivalent to
49    /// `LoopSweepDispatch::default()`; spelled out so call sites read
50    /// the same as the other built-ins (`ScanDispatch::new()`, etc.).
51    #[must_use]
52    pub const fn new() -> Self {
53        Self
54    }
55}
56
57impl DispatchStrategy for LoopSweepDispatch {
58    fn rank(&self, _ctx: &RankContext<'_>) -> Option<f64> {
59        // Loop cars are excluded from the Hungarian idle pool in
60        // `systems::dispatch::run`, so this method is unreachable in
61        // practice. Returning `None` keeps the contract conservative
62        // (`Some(finite)` is required and we have no meaningful cost
63        // to report) without panicking, in case a future caller pushes
64        // a Loop car into the matching by mistake.
65        None
66    }
67
68    fn builtin_id(&self) -> Option<BuiltinStrategy> {
69        Some(BuiltinStrategy::LoopSweep)
70    }
71}