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/// 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 ///
325 /// Used only when [`Self::kind`] is `None`; otherwise the kind's own
326 /// bounds (or circumference) take precedence.
327 #[serde(default)]
328 pub min_position: Option<f64>,
329 /// Highest reachable position (auto-computed from stops if `None`).
330 /// See [`Self::min_position`] for the kind interaction.
331 #[serde(default)]
332 pub max_position: Option<f64>,
333 /// Max cars on this line (`None` = unlimited).
334 #[serde(default)]
335 pub max_cars: Option<usize>,
336 /// Topology kind. When `Some`, takes precedence over the flat
337 /// `min_position`/`max_position` fields. When `None`, falls back to
338 /// [`LineKind::Linear`] built from the flat fields (or
339 /// auto-computed from stops). RON authors can opt into a closed
340 /// loop with `kind: Some(Loop(circumference: 200.0, min_headway: 10.0))`
341 /// — but only when the `loop_lines` feature is enabled.
342 #[serde(default)]
343 pub kind: Option<LineKind>,
344}
345
346/// Configuration for an elevator dispatch group.
347///
348/// A group is the logical dispatch unit containing one or more lines.
349/// All elevators within the group share a single [`BuiltinStrategy`].
350///
351/// ## RON example — destination dispatch with controller latency
352///
353/// ```ron
354/// GroupConfig(
355/// id: 0,
356/// name: "Main",
357/// lines: [1],
358/// dispatch: Destination,
359/// hall_call_mode: Some(Destination),
360/// ack_latency_ticks: Some(15),
361/// )
362/// ```
363///
364/// `hall_call_mode` and `ack_latency_ticks` are optional; omitting them
365/// keeps the legacy behavior (Classic collective control, zero latency).
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct GroupConfig {
368 /// Unique group identifier.
369 pub id: u32,
370 /// Human-readable name.
371 pub name: String,
372 /// Line IDs belonging to this group (references [`LineConfig::id`]).
373 pub lines: Vec<u32>,
374 /// Dispatch strategy for this group.
375 pub dispatch: BuiltinStrategy,
376 /// Optional repositioning strategy for idle elevators.
377 ///
378 /// When `None`, idle elevators in this group stay where they stopped.
379 #[serde(default)]
380 pub reposition: Option<BuiltinReposition>,
381 /// How hall calls reveal rider destinations to dispatch.
382 ///
383 /// `None` defers to [`HallCallMode::default()`] (Classic collective
384 /// control). Set to `Some(HallCallMode::Destination)` to model a
385 /// DCS lobby-kiosk group, which is required to make
386 /// [`crate::dispatch::DestinationDispatch`] consult hall-call
387 /// destinations.
388 #[serde(default)]
389 pub hall_call_mode: Option<HallCallMode>,
390 /// Controller ack latency in ticks (button press → dispatch sees
391 /// the call). `None` means zero — dispatch sees presses immediately.
392 /// Realistic values at 60 Hz land around 5–30 ticks.
393 #[serde(default)]
394 pub ack_latency_ticks: Option<u32>,
395}