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}