Skip to main content

elevator_core/
config.rs

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