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