Skip to main content

elevator_core/
config.rs

1//! Building and elevator configuration (RON-deserializable).
2
3use crate::components::{Accel, LineKind, Orientation, SpatialPosition, Speed, Weight};
4use crate::dispatch::{BuiltinReposition, BuiltinStrategy, HallCallMode};
5use crate::stop::{StopConfig, StopId};
6use serde::{Deserialize, Serialize};
7
8/// Schema version of [`SimConfig`].
9///
10/// Bumped when the RON shape changes in a way that legacy
11/// `assets/config/*.ron` would silently mis-deserialize (a removed
12/// field, a renamed field, a changed default that materially alters
13/// behaviour). Pre-versioning configs deserialize to `0` via
14/// `#[serde(default)]` and validation flags them as legacy so
15/// consumers can migrate explicitly. See `docs/src/config-versioning.md`
16/// for the full bump-trigger policy and migration playbook.
17pub const CURRENT_CONFIG_SCHEMA_VERSION: u32 = 1;
18
19/// Config-time numeric identifier for an [`ElevatorConfig`].
20///
21/// Unique within the config. Mapped to an
22/// [`EntityId`](crate::entity::EntityId) at construction time; resolve
23/// via [`Simulation::elevator_entity`](crate::sim::Simulation::elevator_entity).
24///
25/// Newtype mirrors [`StopId`]'s pattern so consumers can't accidentally
26/// pass an elevator id where a line/stop id was expected. RON
27/// deserializers unwrap newtype structs by default, so existing config
28/// files with bare `id: 0` continue to parse without changes.
29#[derive(
30    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
31)]
32#[serde(transparent)]
33pub struct ElevatorConfigId(pub u32);
34
35impl std::fmt::Display for ElevatorConfigId {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        self.0.fmt(f)
38    }
39}
40
41/// Config-time numeric identifier for a [`LineConfig`].
42///
43/// Unique within the config. Resolve via
44/// [`Simulation::line_entity`](crate::sim::Simulation::line_entity).
45/// Mirrors [`ElevatorConfigId`]'s pattern; same RON-compat note applies.
46#[derive(
47    Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
48)]
49#[serde(transparent)]
50pub struct LineConfigId(pub u32);
51
52impl std::fmt::Display for LineConfigId {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        self.0.fmt(f)
55    }
56}
57
58/// Top-level simulation configuration, loadable from RON.
59///
60/// Validated at construction time by [`Simulation::new()`](crate::sim::Simulation::new)
61/// or [`SimulationBuilder::build()`](crate::builder::SimulationBuilder::build).
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct SimConfig {
64    /// Schema version of this config. Use [`CURRENT_CONFIG_SCHEMA_VERSION`]
65    /// for new configs; `0` (the `#[serde(default)]`) marks a legacy
66    /// pre-versioning RON file. Validation rejects versions strictly
67    /// greater than the current version (forward-incompatible) and
68    /// surfaces a `legacy_config: 0` reason for `0` so the consumer
69    /// has to opt in to running pre-versioning configs.
70    ///
71    /// See `docs/src/config-versioning.md` for the bump policy.
72    #[serde(default)]
73    pub schema_version: u32,
74    /// Building layout describing the stops (floors/stations) along the shaft.
75    pub building: BuildingConfig,
76    /// Elevator cars to install in the building.
77    ///
78    /// Legacy flat list — used when `building.lines` is `None`.
79    /// When explicit lines are provided, elevators live inside each
80    /// [`LineConfig`] instead.
81    #[serde(default)]
82    pub elevators: Vec<ElevatorConfig>,
83    /// Global simulation timing parameters.
84    pub simulation: SimulationParams,
85    /// Passenger spawning parameters used by the game layer.
86    ///
87    /// The core library does not consume these directly; they are stored here
88    /// for games and traffic generators that read the config.
89    pub passenger_spawning: PassengerSpawnConfig,
90}
91
92impl Default for SimConfig {
93    /// A fresh `SimConfig` pinned to [`CURRENT_CONFIG_SCHEMA_VERSION`].
94    ///
95    /// Programmatically-built configs always start at the current
96    /// version; the legacy `0` marker is reserved for RON files that
97    /// pre-date the version field, where it surfaces via
98    /// `#[serde(default)]` on missing input.
99    fn default() -> Self {
100        Self {
101            schema_version: CURRENT_CONFIG_SCHEMA_VERSION,
102            building: BuildingConfig::default(),
103            elevators: Vec::new(),
104            simulation: SimulationParams::default(),
105            passenger_spawning: PassengerSpawnConfig::default(),
106        }
107    }
108}
109
110/// Building layout.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct BuildingConfig {
113    /// Human-readable building name, displayed in UIs and logs.
114    pub name: String,
115    /// Ordered list of stops in the building.
116    ///
117    /// Must contain at least one stop. Each stop has a unique [`StopId`] and
118    /// an arbitrary position along the shaft axis. Positions need not be
119    /// uniformly spaced — this enables buildings, skyscrapers, and space
120    /// elevators with varying inter-stop distances.
121    pub stops: Vec<StopConfig>,
122    /// Lines (physical paths). If `None`, auto-inferred from the flat
123    /// elevator list on [`SimConfig`].
124    #[serde(default)]
125    pub lines: Option<Vec<LineConfig>>,
126    /// Dispatch groups. If `None`, auto-inferred (single group with all lines).
127    #[serde(default)]
128    pub groups: Option<Vec<GroupConfig>>,
129}
130
131/// Configuration for a single elevator car.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ElevatorConfig {
134    /// Numeric identifier for this elevator, unique within the config.
135    ///
136    /// Mapped to an [`EntityId`](crate::entity::EntityId) at construction
137    /// time; not used at runtime. See [`ElevatorConfigId`].
138    pub id: ElevatorConfigId,
139    /// Human-readable elevator name, displayed in UIs and logs.
140    pub name: String,
141    /// Maximum travel speed in distance units per second.
142    ///
143    /// Must be positive. The trapezoidal velocity profile accelerates up to
144    /// this speed, cruises, then decelerates to stop at the target.
145    ///
146    /// Default (from `SimulationBuilder`): `2.0`.
147    pub max_speed: Speed,
148    /// Acceleration rate in distance units per second squared.
149    ///
150    /// Must be positive. Controls how quickly the elevator reaches
151    /// `max_speed` from rest.
152    ///
153    /// Default (from `SimulationBuilder`): `1.5`.
154    pub acceleration: Accel,
155    /// Deceleration rate in distance units per second squared.
156    ///
157    /// Must be positive. Controls how quickly the elevator slows to a stop
158    /// when approaching a target. May differ from `acceleration` for
159    /// asymmetric motion profiles.
160    ///
161    /// Default (from `SimulationBuilder`): `2.0`.
162    pub deceleration: Accel,
163    /// Maximum total weight the elevator car can carry.
164    ///
165    /// Must be positive. Riders whose weight would exceed this limit are
166    /// rejected during the loading phase.
167    ///
168    /// Units: same as rider weight (typically kilograms).
169    /// Default (from `SimulationBuilder`): `800.0`.
170    pub weight_capacity: Weight,
171    /// The [`StopId`] where this elevator starts at simulation init.
172    ///
173    /// Must reference an existing stop in the building config.
174    pub starting_stop: StopId,
175    /// How many ticks the doors remain fully open before closing.
176    ///
177    /// During this window, riders may board or exit. Longer values
178    /// increase loading opportunity but reduce throughput.
179    ///
180    /// Units: simulation ticks.
181    /// Default (from `SimulationBuilder`): `10`.
182    pub door_open_ticks: u32,
183    /// How many ticks a door open or close transition takes.
184    ///
185    /// Models the mechanical travel time of the door panels. No boarding
186    /// or exiting occurs during transitions.
187    ///
188    /// Units: simulation ticks.
189    /// Default (from `SimulationBuilder`): `5`.
190    pub door_transition_ticks: u32,
191    /// Stop IDs this elevator cannot serve (access restriction).
192    ///
193    /// Riders whose current destination is in this list are rejected
194    /// with [`RejectionReason::AccessDenied`](crate::error::RejectionReason::AccessDenied)
195    /// during the loading phase.
196    ///
197    /// Default: empty (no restrictions).
198    #[serde(default)]
199    pub restricted_stops: Vec<StopId>,
200    /// Energy profile for this elevator. If `None`, energy is not tracked.
201    ///
202    /// Requires the `energy` feature.
203    #[cfg(feature = "energy")]
204    #[serde(default)]
205    pub energy_profile: Option<crate::energy::EnergyProfile>,
206    /// Service mode at simulation start. Defaults to `Normal`.
207    #[serde(default)]
208    pub service_mode: Option<crate::components::ServiceMode>,
209    /// Speed multiplier for Inspection mode (0.0..1.0). Defaults to 0.25.
210    #[serde(default = "default_inspection_speed_factor")]
211    pub inspection_speed_factor: f64,
212    /// Full-load bypass threshold for upward pickups, as a fraction of
213    /// `weight_capacity` in `0.0..=1.0`. When the car is above this
214    /// ratio and travelling up, it skips new upward hall calls — aboard
215    /// riders still get delivered. `None` disables the bypass. Modeled
216    /// on Otis Elevonic 411 (patent US5490580A); commercial defaults
217    /// sit near 0.80.
218    #[serde(default)]
219    pub bypass_load_up_pct: Option<f64>,
220    /// Full-load bypass threshold for downward pickups. Typically
221    /// lower than the upward threshold — commercial defaults sit near
222    /// 0.50. `None` disables.
223    #[serde(default)]
224    pub bypass_load_down_pct: Option<f64>,
225}
226
227/// Default inspection speed factor (25% of normal speed).
228const fn default_inspection_speed_factor() -> f64 {
229    0.25
230}
231
232impl Default for ElevatorConfig {
233    /// Reasonable defaults matching the physics values the rest of
234    /// this struct's field docs advertise. Override any field with
235    /// struct-update syntax:
236    ///
237    /// ```
238    /// use elevator_core::config::ElevatorConfig;
239    /// use elevator_core::components::Speed;
240    /// use elevator_core::stop::StopId;
241    ///
242    /// let fast = ElevatorConfig {
243    ///     name: "Express".into(),
244    ///     max_speed: Speed::from(6.0),
245    ///     starting_stop: StopId(0),
246    ///     ..Default::default()
247    /// };
248    /// # let _ = fast;
249    /// ```
250    ///
251    /// `starting_stop` defaults to `StopId(0)` — the conventional lobby
252    /// id. Override if your config uses a different bottom-stop id.
253    fn default() -> Self {
254        Self {
255            id: ElevatorConfigId(0),
256            name: "Elevator 1".into(),
257            max_speed: Speed::from(2.0),
258            acceleration: Accel::from(1.5),
259            deceleration: Accel::from(2.0),
260            weight_capacity: Weight::from(800.0),
261            starting_stop: StopId(0),
262            door_open_ticks: 10,
263            door_transition_ticks: 5,
264            restricted_stops: Vec::new(),
265            #[cfg(feature = "energy")]
266            energy_profile: None,
267            service_mode: None,
268            inspection_speed_factor: default_inspection_speed_factor(),
269            bypass_load_up_pct: None,
270            bypass_load_down_pct: None,
271        }
272    }
273}
274
275impl Default for BuildingConfig {
276    fn default() -> Self {
277        Self {
278            name: "Building".into(),
279            stops: Vec::new(),
280            lines: None,
281            groups: None,
282        }
283    }
284}
285
286/// Global simulation timing parameters.
287#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct SimulationParams {
289    /// Number of simulation ticks per real-time second.
290    ///
291    /// Must be positive. Determines the time delta per tick (`dt = 1.0 / ticks_per_second`).
292    /// Higher values yield finer-grained simulation at the cost of more
293    /// computation per wall-clock second.
294    ///
295    /// Default (from `SimulationBuilder`): `60.0`.
296    pub ticks_per_second: f64,
297}
298
299impl Default for SimulationParams {
300    fn default() -> Self {
301        Self {
302            ticks_per_second: 60.0,
303        }
304    }
305}
306
307/// Passenger spawning parameters (used by the game layer).
308///
309/// The core simulation does not spawn passengers automatically; these values
310/// are advisory and consumed by game code or traffic generators.
311///
312/// This struct is always available regardless of feature flags. The built-in
313/// traffic generation that consumes it requires the `traffic` feature.
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct PassengerSpawnConfig {
316    /// Mean interval in ticks between passenger spawns.
317    ///
318    /// Used by traffic generators for Poisson-distributed arrivals.
319    ///
320    /// Units: simulation ticks.
321    /// Default (from `SimulationBuilder`): `120`.
322    pub mean_interval_ticks: u32,
323    /// `(min, max)` weight range for randomly spawned passengers.
324    ///
325    /// Weights are drawn uniformly from this range by traffic generators.
326    ///
327    /// Units: same as elevator `weight_capacity` (typically kilograms).
328    /// Default (from `SimulationBuilder`): `(50.0, 100.0)`.
329    pub weight_range: (f64, f64),
330}
331
332impl Default for PassengerSpawnConfig {
333    fn default() -> Self {
334        Self {
335            mean_interval_ticks: 120,
336            weight_range: (50.0, 100.0),
337        }
338    }
339}
340
341/// Configuration for a single line (physical path).
342///
343/// A line represents a shaft, tether, track, or other physical pathway
344/// that one or more elevator cars travel along. Lines belong to a
345/// [`GroupConfig`] for dispatch purposes.
346#[derive(Debug, Clone, Default, Serialize, Deserialize)]
347pub struct LineConfig {
348    /// Unique line identifier (within the config). See [`LineConfigId`].
349    pub id: LineConfigId,
350    /// Human-readable name.
351    pub name: String,
352    /// Stops served by this line (references [`StopConfig::id`]).
353    pub serves: Vec<StopId>,
354    /// Elevators on this line.
355    pub elevators: Vec<ElevatorConfig>,
356    /// Physical orientation (defaults to Vertical).
357    #[serde(default)]
358    pub orientation: Orientation,
359    /// Optional floor-plan position.
360    #[serde(default)]
361    pub position: Option<SpatialPosition>,
362    /// Lowest reachable position (auto-computed from stops if `None`).
363    ///
364    /// Used only when [`Self::kind`] is `None`; otherwise the kind's own
365    /// bounds (or circumference) take precedence.
366    #[serde(default)]
367    pub min_position: Option<f64>,
368    /// Highest reachable position (auto-computed from stops if `None`).
369    /// See [`Self::min_position`] for the kind interaction.
370    #[serde(default)]
371    pub max_position: Option<f64>,
372    /// Max cars on this line (`None` = unlimited).
373    #[serde(default)]
374    pub max_cars: Option<usize>,
375    /// Topology kind. When `Some`, takes precedence over the flat
376    /// `min_position`/`max_position` fields. When `None`, falls back to
377    /// [`LineKind::Linear`] built from the flat fields (or
378    /// auto-computed from stops). RON authors can opt into a closed
379    /// loop with `kind: Some(Loop(circumference: 200.0, min_headway: 10.0))`
380    /// — but only when the `loop_lines` feature is enabled.
381    #[serde(default)]
382    pub kind: Option<LineKind>,
383}
384
385/// Configuration for an elevator dispatch group.
386///
387/// A group is the logical dispatch unit containing one or more lines.
388/// All elevators within the group share a single [`BuiltinStrategy`].
389///
390/// ## RON example — destination dispatch with controller latency
391///
392/// ```ron
393/// GroupConfig(
394///     id: 0,
395///     name: "Main",
396///     lines: [1],
397///     dispatch: Destination,
398///     hall_call_mode: Some(Destination),
399///     ack_latency_ticks: Some(15),
400/// )
401/// ```
402///
403/// `hall_call_mode` and `ack_latency_ticks` are optional; omitting them
404/// keeps the legacy behavior (Classic collective control, zero latency).
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub struct GroupConfig {
407    /// Unique group identifier.
408    pub id: u32,
409    /// Human-readable name.
410    pub name: String,
411    /// Line IDs belonging to this group (references [`LineConfig::id`]).
412    pub lines: Vec<u32>,
413    /// Dispatch strategy for this group.
414    pub dispatch: BuiltinStrategy,
415    /// Optional repositioning strategy for idle elevators.
416    ///
417    /// When `None`, idle elevators in this group stay where they stopped.
418    #[serde(default)]
419    pub reposition: Option<BuiltinReposition>,
420    /// How hall calls reveal rider destinations to dispatch.
421    ///
422    /// `None` defers to [`HallCallMode::default()`] (Classic collective
423    /// control). Set to `Some(HallCallMode::Destination)` to model a
424    /// DCS lobby-kiosk group, which is required to make
425    /// [`crate::dispatch::DestinationDispatch`] consult hall-call
426    /// destinations.
427    #[serde(default)]
428    pub hall_call_mode: Option<HallCallMode>,
429    /// Controller ack latency in ticks (button press → dispatch sees
430    /// the call). `None` means zero — dispatch sees presses immediately.
431    /// Realistic values at 60 Hz land around 5–30 ticks.
432    #[serde(default)]
433    pub ack_latency_ticks: Option<u32>,
434}