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}