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}