Skip to main content

elevator_core/
lib.rs

1//! # elevator-core
2//!
3//! Engine-agnostic, tick-based elevator simulation library for Rust.
4//!
5//! This crate provides the building blocks for modeling vertical transportation
6//! systems — from a 3-story office building to an orbital space elevator.
7//! Stops sit at arbitrary positions rather than uniform floors, and the
8//! simulation is driven by a deterministic 8-phase tick loop.
9//!
10//! ## Key capabilities
11//!
12//! - **Pluggable dispatch** — five built-in algorithms ([`dispatch::scan::ScanDispatch`],
13//!   [`dispatch::look::LookDispatch`], [`dispatch::nearest_car::NearestCarDispatch`],
14//!   [`dispatch::etd::EtdDispatch`], [`dispatch::destination::DestinationDispatch`])
15//!   plus the [`dispatch::DispatchStrategy`] trait for custom implementations.
16//! - **Trapezoidal motion profiles** — realistic acceleration, cruise, and
17//!   deceleration computed per-tick.
18//! - **Extension components** — attach arbitrary `Serialize + DeserializeOwned`
19//!   data to any entity via [`world::World::insert_ext`] without modifying the
20//!   library.
21//! - **Lifecycle hooks** — inject logic before or after any of the eight
22//!   simulation phases. See [`hooks::Phase`].
23//! - **Metrics and events** — query aggregate wait/ride times through
24//!   [`metrics::Metrics`] and react to fine-grained tick events via
25//!   [`events::Event`].
26//! - **Snapshot save/load** — capture and restore full simulation state with
27//!   [`snapshot::WorldSnapshot`].
28//! - **Zero `unsafe` code** — enforced by `#![forbid(unsafe_code)]`.
29//!
30//! ## Quick start
31//!
32//! ```rust
33//! use elevator_core::prelude::*;
34//! use elevator_core::stop::StopConfig;
35//!
36//! let mut sim = SimulationBuilder::demo()
37//!     .stops(StopConfig::linear(&[
38//!         ("Ground", 0.0),
39//!         ("Floor 2", 4.0),
40//!         ("Floor 3", 8.0),
41//!     ]))
42//!     .build()
43//!     .unwrap();
44//!
45//! sim.spawn_rider(StopId(0), StopId(2), 75.0).unwrap();
46//!
47//! for _ in 0..1000 {
48//!     sim.step();
49//! }
50//!
51//! assert!(sim.metrics().total_delivered() > 0);
52//! ```
53//!
54//! Need hand-picked `StopId`s or non-sequential identifiers? Build the
55//! `Vec<StopConfig>` with struct literals instead (`StopConfig { id:
56//! StopId(42), name: "Roof".into(), position: 120.0 }`). See
57//! [`StopConfig::linear`](stop::StopConfig::linear) for the constraints
58//! of the compact form.
59//!
60//! ## Crate layout
61//!
62//! ## Stability
63//!
64//! Every module below is classified as **stable**, **experimental**, or
65//! **internal**. Stable items break only in planned major versions
66//! with a deprecation cycle; experimental items may change in any
67//! minor version; internal items are not part of the supported surface.
68//! See [`STABILITY.md`](https://github.com/andymai/elevator-core/blob/main/STABILITY.md)
69//! for the full policy and cadence commitment.
70//!
71//! | Module | Purpose | Stability |
72//! |--------|---------|-----------|
73//! | [`builder`] | Fluent [`SimulationBuilder`](builder::SimulationBuilder) API | stable |
74//! | [`sim`] | Top-level [`Simulation`](sim::Simulation) runner and tick loop | stable (selected methods — see [`STABILITY.md`](https://github.com/andymai/elevator-core/blob/main/STABILITY.md)) |
75//! | [`dispatch`] | Dispatch strategies and the [`DispatchStrategy`](dispatch::DispatchStrategy) trait | stable |
76//! | [`world`] | ECS-style [`World`](world::World) with typed component storage | experimental |
77//! | [`components`] | Entity data types: [`Rider`](components::Rider), [`Elevator`](components::Elevator), [`Stop`](components::Stop), [`Line`](components::Line), [`Route`](components::Route), [`Patience`](components::Patience), [`Preferences`](components::Preferences), [`AccessControl`](components::AccessControl), [`DestinationQueue`](components::DestinationQueue), [`ServiceMode`](components::ServiceMode), [`Orientation`](components::Orientation), [`Position`](components::Position), [`Velocity`](components::Velocity), [`SpatialPosition`](components::SpatialPosition) | stable (units + core), experimental (rest) |
78//! | [`config`] | RON-deserializable [`SimConfig`](config::SimConfig), [`GroupConfig`](config::GroupConfig), [`LineConfig`](config::LineConfig) | experimental |
79//! | [`events`] | [`Event`](events::Event) variants and the [`EventBus`](events::EventBus) | stable ([`Event`](events::Event)) |
80//! | [`metrics`] | Aggregate [`Metrics`](metrics::Metrics) (wait time, throughput, etc.) | stable |
81//! | [`hooks`] | Lifecycle hook registration by [`Phase`](hooks::Phase) | experimental |
82//! | [`query`] | Entity query builder for filtering by component composition | experimental |
83//! | [`systems`] | Per-phase tick logic (dispatch, movement, doors, loading, ...) | internal |
84//! | [`snapshot`] | [`WorldSnapshot`](snapshot::WorldSnapshot) save/restore with custom-strategy factory | stable |
85//! | [`scenario`] | Deterministic scenario replay from recorded event streams | experimental |
86//! | [`topology`] | Lazy-rebuilt connectivity graph for cross-line routing | experimental |
87//! | [`traffic`] | [`TrafficSource`](traffic::TrafficSource) trait + `PoissonSource` (feature-gated) | experimental |
88//! | [`tagged_metrics`] | Per-tag metric accumulators for zone/line/priority breakdowns | experimental |
89//! | [`movement`] | Trapezoidal velocity-profile primitives ([`braking_distance`](movement::braking_distance), [`tick_movement`](movement::tick_movement)) | experimental |
90//! | [`door`] | Door finite-state machine ([`DoorState`](door::DoorState)) | internal |
91//! | [`eta`] | Per-stop arrival-time estimation used by dispatch | internal |
92//! | [`time`] | Tick-to-wall-clock conversion ([`TimeAdapter`](time::TimeAdapter)) | experimental |
93//! | `energy` | Simplified per-elevator energy modeling (gated behind the `energy` feature) | experimental |
94//! | [`stop`] | [`StopId`](stop::StopId) and [`StopConfig`](stop::StopConfig) | stable |
95//! | [`entity`] | Opaque [`EntityId`](entity::EntityId) runtime identity | stable |
96//! | [`ids`] | Config-level typed identifiers ([`GroupId`](ids::GroupId), etc.) | experimental |
97//! | [`error`] | [`SimError`](error::SimError), [`RejectionReason`](error::RejectionReason), [`RejectionContext`](error::RejectionContext) | stable ([`SimError`](error::SimError)) |
98//!
99//! ## Architecture overview
100//!
101//! ### 8-phase tick loop
102//!
103//! Each call to [`Simulation::step()`](sim::Simulation::step) runs these
104//! phases in order:
105//!
106//! 1. **`AdvanceTransient`** — transitions `Boarding→Riding`, `Exiting→Arrived`,
107//!    teleports walkers.
108//! 2. **Dispatch** — builds a [`DispatchManifest`](dispatch::DispatchManifest)
109//!    and calls each group's [`DispatchStrategy`](dispatch::DispatchStrategy).
110//! 3. **Reposition** — optional phase; moves idle elevators via
111//!    [`RepositionStrategy`](dispatch::RepositionStrategy) for better coverage.
112//! 4. **`AdvanceQueue`** — reconciles each elevator's phase/target with the
113//!    front of its [`DestinationQueue`](components::DestinationQueue), so
114//!    imperative pushes from game code take effect before movement.
115//! 5. **Movement** — applies trapezoidal velocity profiles, detects stop arrivals
116//!    and emits [`PassingFloor`](events::Event::PassingFloor) events.
117//! 6. **Doors** — ticks the [`DoorState`](door::DoorState) FSM per elevator.
118//! 7. **Loading** — boards/exits riders with capacity and preference checks.
119//! 8. **Metrics** — aggregates wait/ride times into [`Metrics`](metrics::Metrics)
120//!    and per-tag accumulators.
121//!
122//! For full per-phase semantics (events emitted, edge cases, design rationale),
123//! see the [guide](https://andymai.github.io/elevator-core/simulation-loop.html).
124//!
125//! ### Component relationships
126//!
127//! ```text
128//! Group ──contains──▶ Line ──has──▶ Elevator ──carries──▶ Rider
129//!   │                  │              │                      │
130//!   └── DispatchStrategy              └── Position           └── Route (optional)
131//!        RepositionStrategy               Velocity               Patience
132//!                      │                  DoorState               Preferences
133//!                      └── Stop (served stops along the shaft)
134//! ```
135//!
136//! ### Rider lifecycle
137//!
138//! Riders progress through phases managed by the simulation:
139//!
140//! ```text
141//! Waiting → Boarding → Riding → Exiting → Arrived
142//!    ↑         (1 tick)           (1 tick)     │
143//!    │                                         ├── settle_rider() → Resident
144//!    │                                         │                       │
145//!    │                                         └── despawn_rider()     │
146//!    │                                                                 │
147//!    └──────── reroute_rider() ────────────────────────────────────────┘
148//!
149//! Waiting ──(patience exceeded)──→ Abandoned ──→ settle/despawn
150//! ```
151//!
152//! - **`Arrived`** / **`Abandoned`**: terminal states; consumer must explicitly
153//!   settle or despawn the rider.
154//! - **`Resident`**: parked at a stop, invisible to dispatch and loading.
155//!   Query with [`Simulation::residents_at()`](sim::Simulation::residents_at).
156//! - **Population queries**: O(1) via maintained reverse index —
157//!   [`residents_at`](sim::Simulation::residents_at),
158//!   [`waiting_at`](sim::Simulation::waiting_at),
159//!   [`abandoned_at`](sim::Simulation::abandoned_at).
160//!
161//! ### Extension storage
162//!
163//! Games attach custom data to any entity without modifying the library:
164//!
165//! ```rust,no_run
166//! # use elevator_core::prelude::*;
167//! # use elevator_core::__doctest_prelude::*;
168//! # use elevator_core::query::Ext;
169//! # use serde::{Serialize, Deserialize};
170//! # #[derive(Debug, Clone, Serialize, Deserialize)]
171//! # struct VipTag { priority: u32 }
172//! # fn run(world: &mut World, rider_id: EntityId) {
173//! // Attach a VIP flag to a rider.
174//! world.insert_ext(rider_id, VipTag { priority: 1 }, ExtKey::from_type_name());
175//!
176//! // Query it alongside built-in components.
177//! for (id, rider, vip) in world.query::<(EntityId, &Rider, &Ext<VipTag>)>().iter() {
178//!     // ...
179//! }
180//! # }
181//! ```
182//!
183//! Extensions participate in snapshots via `serialize_extensions()` /
184//! `register_ext::<T>(key)` + `load_extensions()`.
185//!
186//! ### Snapshot lifecycle
187//!
188//! 1. Capture: `sim.snapshot()` → [`WorldSnapshot`](snapshot::WorldSnapshot)
189//! 2. Serialize: serde (RON, JSON, bincode, etc.)
190//! 3. Deserialize + restore: `snapshot.restore(RestoreOptions::default())` → new `Simulation`
191//! 4. Re-register extensions: `world.register_ext::<T>(key)` per type
192//! 5. Load extension data: `sim.load_extensions()`
193//!
194//! For the common case (save-to-disk, load-from-disk), skip the format choice
195//! and use [`Simulation::snapshot_bytes`](sim::Simulation::snapshot_bytes) /
196//! [`Simulation::restore_bytes`](sim::Simulation::restore_bytes). The byte
197//! blob is postcard-encoded and carries a magic prefix plus the crate version:
198//! restoring bytes from a different `elevator-core` version returns
199//! [`SimError::SnapshotVersion`](error::SimError::SnapshotVersion) instead of
200//! silently producing a garbled sim. Determinism is bit-exact across builds
201//! of the same crate version, which makes snapshots viable as rollback-netcode
202//! checkpoints or deterministic replay fixtures.
203//!
204//! ### Performance
205//!
206//! | Operation | Complexity |
207//! |-----------|-----------|
208//! | Entity iteration | O(n) via `SlotMap` secondary maps |
209//! | Stop-passing detection | O(log n) via `SortedStops` binary search |
210//! | Dispatch manifest build | O(riders) per group |
211//! | Population queries | O(1) via `RiderIndex` reverse index |
212//! | Topology graph queries | O(V+E) BFS, lazy rebuild |
213//!
214//! ## Runtime upgrades
215//!
216//! Elevator kinematic and door-timing parameters can be mutated at runtime
217//! via the `Simulation::set_*` setters — handy for RPG-style upgrade systems
218//! or scripted events that boost speed, capacity, or door behavior mid-game.
219//!
220//! Each setter validates its input, mutates the underlying component, and
221//! emits an [`Event::ElevatorUpgraded`](events::Event::ElevatorUpgraded) so
222//! game code can react (score popups, SFX, UI). Velocity is preserved when
223//! kinematic parameters change — the integrator picks up the new values on
224//! the next tick without jerk. Door-timing changes apply to the next door
225//! cycle and never retroactively retime an in-progress transition.
226//!
227//! See [`examples/runtime_upgrades.rs`][rte] for an end-to-end demonstration
228//! that doubles a car's `max_speed` mid-run and prints the throughput delta.
229//!
230//! [rte]: https://github.com/andymai/elevator-core/blob/main/crates/elevator-core/examples/runtime_upgrades.rs
231//!
232//! ## Door control
233//!
234//! Games that want to drive elevator doors directly — e.g. the player
235//! pressing "open" or "close" on a cab panel in a first-person game, or an
236//! RPG where the player *is* the elevator — use the manual door-control
237//! API on [`Simulation`](sim::Simulation):
238//!
239//! - [`open_door`](sim::Simulation::open_door)
240//! - [`close_door`](sim::Simulation::close_door)
241//! - [`hold_door`](sim::Simulation::hold_door) (cumulative)
242//! - [`cancel_door_hold`](sim::Simulation::cancel_door_hold)
243//!
244//! Each call is either applied immediately (if the car is in a matching
245//! door-FSM state) or queued on the elevator's
246//! [`door_command_queue`](components::Elevator::door_command_queue) and
247//! re-tried every tick until it can be applied. The only hard errors are
248//! [`NotAnElevator`](error::SimError::NotAnElevator) / [`ElevatorDisabled`](error::SimError::ElevatorDisabled) and (for `hold_door`) a
249//! zero-tick argument — the rest return `Ok(())` and let the engine pick
250//! the right moment. A [`DoorCommand`](door::DoorCommand) can be:
251//!
252//! - `Open` — reverses a closing door; no-op if already open or opening;
253//!   queues while the car is moving.
254//! - `Close` — forces an early close from `Loading`. Waits one tick if a
255//!   rider is mid-boarding/exiting (safe-close).
256//! - `HoldOpen { ticks }` — adds to the remaining open dwell; two calls
257//!   of 30 ticks stack to 60. Queues if doors aren't open yet.
258//! - `CancelHold` — clamps any accumulated hold back to the base dwell.
259//!
260//! Every command emits
261//! [`Event::DoorCommandQueued`](events::Event::DoorCommandQueued) when
262//! submitted and
263//! [`Event::DoorCommandApplied`](events::Event::DoorCommandApplied) when
264//! it actually takes effect — useful for driving UI feedback (button
265//! flashes, SFX) without polling the elevator every tick.
266//!
267//! See [`examples/door_commands.rs`][dex] for a runnable demo.
268//!
269//! [dex]: https://github.com/andymai/elevator-core/blob/main/crates/elevator-core/examples/door_commands.rs
270//!
271//! ## Sub-tick position interpolation
272//!
273//! Games that render at a higher framerate than the simulation ticks (e.g.
274//! a 60 Hz sim driving a 144 Hz camera, or a first-person game where the
275//! player is parented to an elevator car) need a smooth position between
276//! ticks. [`Simulation::position_at`](sim::Simulation::position_at) lerps
277//! between the snapshot taken at the start of the current tick and the
278//! post-tick position, using an `alpha` accumulator clamped to `[0.0, 1.0]`:
279//!
280//! ```text
281//! // typical fixed-timestep render loop
282//! accumulator += frame_dt;
283//! while accumulator >= sim.dt() {
284//!     sim.step();
285//!     accumulator -= sim.dt();
286//! }
287//! let alpha = accumulator / sim.dt();
288//! let y = sim.position_at(car, alpha).unwrap();
289//! ```
290//!
291//! The previous-position snapshot is refreshed automatically at the start
292//! of every [`step`](sim::Simulation::step). [`Simulation::velocity`](sim::Simulation::velocity) is
293//! a convenience that returns the raw `f64` along the shaft axis (signed:
294//! +up, -down) for camera tilt, motion blur, or cabin-sway effects.
295//!
296//! See [`examples/fp_player_rider.rs`][fpe] for a runnable demo.
297//!
298//! [fpe]: https://github.com/andymai/elevator-core/blob/main/crates/elevator-core/examples/fp_player_rider.rs
299//!
300//! ## Manual-drive mode
301//!
302//! Games where the player *is* the elevator — driving the car with a
303//! velocity stick, slamming an emergency brake, stopping between floors
304//! — use [`ServiceMode::Manual`](components::ServiceMode::Manual). Manual
305//! elevators are skipped by the automatic dispatch and repositioning
306//! phases; the consumer drives movement via:
307//!
308//! - [`Simulation::set_target_velocity`](sim::Simulation::set_target_velocity) — signed target speed (+up, -down), clamped to the car's `max_speed`.
309//! - [`Simulation::emergency_stop`](sim::Simulation::emergency_stop) — commands an immediate deceleration to zero.
310//!
311//! Physics still apply: velocity ramps toward the target using the car's
312//! `acceleration` / `deceleration` caps, and positions update at the same
313//! rate as normal elevators. Manual elevators can come to rest at any
314//! position — they are not required to align with a configured stop.
315//! Door behaviour is governed by the [manual door-control API](#door-control);
316//! nothing opens or closes automatically. Leaving `Manual` clears any
317//! pending velocity command.
318//!
319//! Each command emits
320//! [`Event::ManualVelocityCommanded`](events::Event::ManualVelocityCommanded)
321//! with the clamped target, or `None` for an emergency stop.
322//!
323//! See [`examples/manual_driver.rs`][mde] for a runnable demo.
324//!
325//! [mde]: https://github.com/andymai/elevator-core/blob/main/crates/elevator-core/examples/manual_driver.rs
326//!
327//! ## ETA queries
328//!
329//! Hall-call dispatch UIs, scheduling overlays, and "press to call" panels
330//! all need the same answer: *how long until this car shows up?* Two methods
331//! on [`Simulation`](sim::Simulation) compute it from the elevator's queued
332//! destinations, current kinematic state, and configured door dwell:
333//!
334//! - [`Simulation::eta`](sim::Simulation::eta) — seconds until a specific
335//!   elevator reaches a specific stop, or an [`EtaError`](error::EtaError) if the
336//!   stop isn't queued, the car is disabled, or the service mode excludes it.
337//! - [`Simulation::best_eta`](sim::Simulation::best_eta) — winner across all
338//!   eligible elevators, optionally filtered by indicator-lamp direction
339//!   ("which up-going car arrives first?").
340//!
341//! Both walk the queue in service order, summing closed-form trapezoidal
342//! travel time per leg plus the configured door cycle at every intermediate
343//! stop. The closed-form solver lives in [`eta::travel_time`] and tracks the
344//! per-tick integrator in [`movement::tick_movement`] to within a tick or
345//! two — close enough for UI countdowns; not a substitute for actually
346//! simulating to compare two dispatch policies.
347//!
348//! For narrative guides, tutorials, and architecture walkthroughs, see the
349//! [mdBook documentation](https://andymai.github.io/elevator-core/).
350
351#![forbid(unsafe_code)]
352#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
353#![deny(missing_docs)]
354
355/// Single source of truth for the host-binding wire/ABI version.
356///
357/// Every consumer crate (FFI's `EV_ABI_VERSION`, wasm's
358/// `ABI_VERSION`, gdext's `ABI_VERSION`) references this constant
359/// at compile time so they cannot drift apart in-source. The C
360/// header (`elevator_ffi.h`) and the example harnesses still embed
361/// a literal copy; `scripts/check-abi-pins.sh` enforces that those
362/// copies match this value.
363///
364/// Bump when any host-facing wire format changes — `EvEvent`
365/// layout, `EvSnapshot` layout, an enum value re-numbering, etc.
366/// See [Host Binding Parity](https://andymai.github.io/elevator-core/host-binding-parity.html)
367/// for the cross-host contract this constant is part of (issue #655
368/// closed out the design — this constant is one of the three shared
369/// types that pattern landed on).
370pub const HOST_PROTOCOL_VERSION: u32 = 5;
371
372#[cfg(doctest)]
373mod doctests;
374
375/// Entity-component data types for the simulation.
376pub mod components;
377/// Entity identity and allocation.
378pub mod entity;
379/// Simulation error types.
380pub mod error;
381/// Shared error classification for host bindings (FFI / wasm /
382/// gdext / Bevy). See [`host_error::ErrorKind`].
383pub mod host_error;
384/// Sealed kebab-case label vocabulary shared across host bindings.
385/// See [`host_label`] for the formatter / parser pairs.
386pub mod host_label;
387/// Typed identifiers for groups and other sim concepts.
388pub mod ids;
389/// ECS-style query builder for iterating entities by component composition.
390pub mod query;
391/// Tick-loop system phases (dispatch, reposition, movement, doors, loading, metrics).
392pub mod systems;
393/// Central entity/component storage.
394pub mod world;
395
396/// Rolling per-stop arrival log, the signal source for rate-driven
397/// dispatch and repositioning.
398pub mod arrival_log;
399/// Fluent builder for constructing a Simulation programmatically.
400pub mod builder;
401/// Building and elevator configuration (RON deserialization).
402pub mod config;
403/// Pluggable dispatch strategies (SCAN, LOOK, nearest-car, ETD).
404pub mod dispatch;
405/// Door finite-state machine.
406pub mod door;
407/// Simplified energy modeling for elevators.
408#[cfg(feature = "energy")]
409pub mod energy;
410/// ETA estimation for queued elevators (closed-form trapezoidal travel time).
411pub mod eta;
412/// Simulation event bus and event types.
413pub mod events;
414/// Floating-point helpers gated by the `deterministic-fp` feature.
415pub(crate) mod fp;
416/// Lifecycle hooks for injecting logic before/after simulation phases.
417pub mod hooks;
418/// Aggregate simulation metrics.
419pub mod metrics;
420/// Trapezoidal velocity-profile movement math.
421///
422/// Exposes [`braking_distance`](movement::braking_distance) for consumers
423/// writing opportunistic dispatch strategies that need the kinematic answer
424/// without constructing a `Simulation`.
425pub mod movement;
426/// Phase-partitioned reverse index for rider population queries.
427mod rider_index;
428/// Scenario replay from recorded event streams.
429pub mod scenario;
430/// Top-level simulation runner.
431pub mod sim;
432/// World snapshot for save/load.
433pub mod snapshot;
434/// Stop configuration helpers.
435pub mod stop;
436/// Tag-based per-entity metrics.
437pub mod tagged_metrics;
438/// Tick-to-wall-clock time conversion.
439pub mod time;
440/// Topology graph for cross-line connectivity queries.
441pub mod topology;
442/// Traffic generation (arrival patterns).
443#[cfg(feature = "traffic")]
444pub mod traffic;
445/// Traffic-mode detector (up-peak / idle / inter-floor).
446pub mod traffic_detector;
447
448/// Register multiple extension types for snapshot deserialization in one call.
449///
450/// Eliminates the manual `register_ext` ceremony after snapshot restore.
451///
452/// # Examples
453///
454/// Name-less form (uses `type_name` automatically):
455///
456/// ```
457/// use elevator_core::prelude::*;
458/// use elevator_core::register_extensions;
459/// use serde::{Deserialize, Serialize};
460///
461/// #[derive(Clone, Debug, Serialize, Deserialize)]
462/// struct VipTag { level: u32 }
463///
464/// #[derive(Clone, Debug, Serialize, Deserialize)]
465/// struct Priority { rank: u8 }
466///
467/// let mut sim = SimulationBuilder::demo().build().unwrap();
468/// register_extensions!(sim.world_mut(), VipTag, Priority);
469/// ```
470///
471/// Named form (explicit storage name per type):
472///
473/// ```
474/// use elevator_core::prelude::*;
475/// use elevator_core::register_extensions;
476/// use serde::{Deserialize, Serialize};
477///
478/// #[derive(Clone, Debug, Serialize, Deserialize)]
479/// struct VipTag { level: u32 }
480///
481/// let mut sim = SimulationBuilder::demo().build().unwrap();
482/// register_extensions!(sim.world_mut(), VipTag => "vip_tag");
483/// ```
484#[macro_export]
485macro_rules! register_extensions {
486    ($world:expr, $($ty:ty),+ $(,)?) => {
487        $( $world.register_ext::<$ty>($crate::world::ExtKey::<$ty>::from_type_name()); )+
488    };
489    ($world:expr, $($ty:ty => $name:expr),+ $(,)?) => {
490        $( $world.register_ext::<$ty>($crate::world::ExtKey::<$ty>::new($name)); )+
491    };
492}
493
494/// Common imports for consumers of this library.
495///
496/// `use elevator_core::prelude::*;` pulls in the 22 names that cover the
497/// vast majority of usage: building a sim, stepping it, querying world
498/// state, writing custom dispatch, and reading aggregate metrics. Anything
499/// beyond that — fine-grained component types, per-strategy structs,
500/// extension keys, traffic generation — is imported explicitly from its
501/// sub-module.
502///
503/// # Contents
504///
505/// - **Sim & builder:** [`Simulation`](crate::sim::Simulation),
506///   [`SimulationBuilder`](crate::builder::SimulationBuilder),
507///   [`RiderBuilder`](crate::sim::RiderBuilder)
508/// - **Config:** [`SimConfig`](crate::config::SimConfig),
509///   [`ElevatorConfig`](crate::config::ElevatorConfig),
510///   [`StopConfig`](crate::stop::StopConfig)
511/// - **Identity:** [`StopId`](crate::stop::StopId),
512///   [`ElevatorId`](crate::entity::ElevatorId),
513///   [`RiderId`](crate::entity::RiderId),
514///   [`EntityId`](crate::entity::EntityId),
515///   [`GroupId`](crate::ids::GroupId)
516/// - **Errors & events:** [`SimError`](crate::error::SimError),
517///   [`RejectionReason`](crate::error::RejectionReason),
518///   [`Event`](crate::events::Event)
519/// - **Phase & direction enums:** [`ElevatorPhase`](crate::components::ElevatorPhase),
520///   [`RiderPhase`](crate::components::RiderPhase),
521///   [`Direction`](crate::components::Direction)
522/// - **Dispatch:** [`DispatchStrategy`](crate::dispatch::DispatchStrategy),
523///   [`BuiltinStrategy`](crate::dispatch::BuiltinStrategy),
524///   [`BuiltinReposition`](crate::dispatch::BuiltinReposition)
525/// - **World & metrics:** [`World`](crate::world::World),
526///   [`Metrics`](crate::metrics::Metrics)
527///
528/// # Not included (import explicitly)
529///
530/// - Fine-grained component types (`Position`, `Velocity`, `Speed`, `Accel`,
531///   `Weight`, `Route`, `Patience`, `ServiceMode`, etc.) → [`crate::components`]
532/// - Per-strategy structs (`ScanDispatch`, `LookDispatch`,
533///   `NearestCarDispatch`, `EtdDispatch`, `RsrDispatch`, `DestinationDispatch`),
534///   `RankContext`, `DispatchManifest`, reposition strategies → [`crate::dispatch`]
535/// - Extension storage (`ExtKey`) → [`crate::world`]
536/// - Traffic detection, arrival logs, time adapters, event bus,
537///   group/line config, and `StopRef` → their respective modules
538/// - Traffic generation types from [`crate::traffic`] (feature-gated)
539/// - Snapshot types from [`crate::snapshot`]
540pub mod prelude {
541    pub use crate::builder::SimulationBuilder;
542    pub use crate::components::{Direction, ElevatorPhase, RiderPhase};
543    pub use crate::config::{ElevatorConfig, SimConfig};
544    pub use crate::dispatch::{BuiltinReposition, BuiltinStrategy, DispatchStrategy};
545    pub use crate::entity::{ElevatorId, EntityId, RiderId};
546    pub use crate::error::{RejectionReason, SimError};
547    pub use crate::events::{Event, EventKind};
548    pub use crate::ids::GroupId;
549    pub use crate::metrics::Metrics;
550    pub use crate::sim::{RiderBuilder, Simulation};
551    pub use crate::stop::{StopConfig, StopId};
552    pub use crate::world::World;
553}
554
555/// Internal-only re-exports used by hidden doc-test setup lines
556/// (`# use elevator_core::__doctest_prelude::*;`) so each chapter's
557/// fenced blocks compile without per-block import surgery while the
558/// public [`prelude`] stays small. Hidden from `cargo doc` and
559/// considered an implementation detail — not part of the public API.
560///
561/// Names already in [`prelude`] are deliberately omitted to avoid
562/// glob-import ambiguity errors when both modules are wildcarded.
563#[doc(hidden)]
564pub mod __doctest_prelude {
565    pub use crate::arrival_log::{ArrivalLog, ArrivalLogRetention, CurrentTick, DestinationLog};
566    pub use crate::components::{
567        Accel, AccessControl, DestinationQueue, Elevator, Line, Orientation, Patience, Position,
568        Preferences, Rider, RiderPhaseKind, Route, ServiceMode, SpatialPosition, Speed, Stop,
569        UnitError, Velocity, Weight,
570    };
571    pub use crate::config::{GroupConfig, LineConfig};
572    pub use crate::dispatch::reposition::{
573        AdaptiveParking, DemandWeighted, NearestIdle, PredictiveParking, ReturnToLobby,
574        SpreadEvenly,
575    };
576    pub use crate::dispatch::{
577        AssignedCar, DestinationDispatch, DispatchDecision, DispatchManifest, ElevatorGroup,
578        EtdDispatch, LookDispatch, NearestCarDispatch, RankContext, RepositionStrategy,
579        RsrDispatch, ScanDispatch,
580    };
581    pub use crate::error::{EtaError, RejectionContext};
582    pub use crate::events::{EventBus, EventCategory};
583    pub use crate::stop::StopRef;
584    pub use crate::time::TimeAdapter;
585    pub use crate::traffic_detector::{TrafficDetector, TrafficMode};
586    pub use crate::world::ExtKey;
587}
588
589#[cfg(test)]
590mod tests;