Expand description
§elevator-core
Engine-agnostic, tick-based elevator simulation library for Rust.
This crate provides the building blocks for modeling vertical transportation systems — from a 3-story office building to an orbital space elevator. Stops sit at arbitrary positions rather than uniform floors, and the simulation is driven by a deterministic 8-phase tick loop.
§Key capabilities
- Pluggable dispatch — four built-in algorithms (
dispatch::scan::ScanDispatch,dispatch::look::LookDispatch,dispatch::nearest_car::NearestCarDispatch,dispatch::etd::EtdDispatch) plus thedispatch::DispatchStrategytrait for custom implementations. - Trapezoidal motion profiles — realistic acceleration, cruise, and deceleration computed per-tick.
- Extension components — attach arbitrary
Serialize + DeserializeOwneddata to any entity viaworld::World::insert_extwithout modifying the library. - Lifecycle hooks — inject logic before or after any of the eight
simulation phases. See
hooks::Phase. - Metrics and events — query aggregate wait/ride times through
metrics::Metricsand react to fine-grained tick events viaevents::Event. - Snapshot save/load — capture and restore full simulation state with
snapshot::WorldSnapshot. - Zero
unsafecode — enforced by#![forbid(unsafe_code)].
§Quick start
use elevator_core::prelude::*;
use elevator_core::stop::StopConfig;
let mut sim = SimulationBuilder::demo()
.stops(vec![
StopConfig { id: StopId(0), name: "Ground".into(), position: 0.0 },
StopConfig { id: StopId(1), name: "Floor 2".into(), position: 4.0 },
StopConfig { id: StopId(2), name: "Floor 3".into(), position: 8.0 },
])
.build()
.unwrap();
sim.spawn_rider_by_stop_id(StopId(0), StopId(2), 75.0).unwrap();
for _ in 0..1000 {
sim.step();
}
assert!(sim.metrics().total_delivered() > 0);§Crate layout
| Module | Purpose |
|---|---|
builder | Fluent SimulationBuilder API |
sim | Top-level Simulation runner and tick loop |
dispatch | Dispatch strategies and the DispatchStrategy trait |
world | ECS-style World with typed component storage |
components | Entity data types: Rider, Elevator, Stop, Line, Route, Patience, Preferences, AccessControl, DestinationQueue, ServiceMode, Orientation, Position, Velocity, FloorPosition |
config | RON-deserializable SimConfig, GroupConfig, LineConfig |
events | Event variants and the EventBus |
metrics | Aggregate Metrics (wait time, throughput, etc.) |
hooks | Lifecycle hook registration by Phase |
query | Entity query builder for filtering by component composition |
systems | Per-phase tick logic (dispatch, movement, doors, loading, …) |
snapshot | WorldSnapshot save/restore with custom-strategy factory |
scenario | Deterministic scenario replay from recorded event streams |
topology | Lazy-rebuilt connectivity graph for cross-line routing |
traffic | TrafficSource trait + PoissonSource (feature-gated) |
tagged_metrics | Per-tag metric accumulators for zone/line/priority breakdowns |
movement | Trapezoidal velocity-profile primitives (braking_distance, tick_movement) |
door | Door finite-state machine (DoorState) |
time | Tick-to-wall-clock conversion (TimeAdapter) |
energy | Simplified per-elevator energy modeling (gated behind the energy feature) |
stop | StopId and StopConfig |
entity | Opaque EntityId runtime identity |
ids | Config-level typed identifiers (GroupId, etc.) |
error | SimError, RejectionReason, RejectionContext |
§Architecture overview
§8-phase tick loop
Each call to Simulation::step() runs these
phases in order:
AdvanceTransient— transitionsBoarding→Riding,Exiting→Arrived, teleports walkers.- Dispatch — builds a
DispatchManifestand calls each group’sDispatchStrategy. - Reposition — optional phase; moves idle elevators via
RepositionStrategyfor better coverage. AdvanceQueue— reconciles each elevator’s phase/target with the front of itsDestinationQueue, so imperative pushes from game code take effect before movement.- Movement — applies trapezoidal velocity profiles, detects stop arrivals
and emits
PassingFloorevents. - Doors — ticks the
DoorStateFSM per elevator. - Loading — boards/exits riders with capacity and preference checks.
- Metrics — aggregates wait/ride times into
Metricsand per-tag accumulators.
For full per-phase semantics (events emitted, edge cases, design rationale),
see ARCHITECTURE.md §3. This crate-level summary is the short
form; ARCHITECTURE.md is canonical.
§Component relationships
Group ──contains──▶ Line ──has──▶ Elevator ──carries──▶ Rider
│ │ │ │
└── DispatchStrategy └── Position └── Route (optional)
RepositionStrategy Velocity Patience
│ DoorState Preferences
└── Stop (served stops along the shaft)§Rider lifecycle
Riders progress through phases managed by the simulation:
Waiting → Boarding → Riding → Exiting → Arrived
↑ (1 tick) (1 tick) │
│ ├── settle_rider() → Resident
│ │ │
│ └── despawn_rider() │
│ │
└──────── reroute_rider() ────────────────────────────────────────┘
Waiting ──(patience exceeded)──→ Abandoned ──→ settle/despawnArrived/Abandoned: terminal states; consumer must explicitly settle or despawn the rider.Resident: parked at a stop, invisible to dispatch and loading. Query withSimulation::residents_at().- Population queries: O(1) via maintained reverse index —
residents_at,waiting_at,abandoned_at.
§Extension storage
Games attach custom data to any entity without modifying the library:
// Attach a VIP flag to a rider.
world.insert_ext(rider_id, VipTag { priority: 1 }, "vip_tag");
// Query it alongside built-in components.
for (id, rider, vip) in world.query::<(EntityId, &Rider, &Ext<VipTag>)>().iter() {
// ...
}Extensions participate in snapshots via serialize_extensions() /
register_ext::<T>(name) + load_extensions().
§Snapshot lifecycle
- Capture:
sim.snapshot()→WorldSnapshot - Serialize: serde (RON, JSON, bincode, etc.)
- Deserialize + restore:
snapshot.restore(factory)→ newSimulation - Re-register extensions:
world.register_ext::<T>(name)per type - Load extension data:
sim.load_extensions()
For the common case (save-to-disk, load-from-disk), skip the format choice
and use Simulation::snapshot_bytes /
Simulation::restore_bytes. The byte
blob is postcard-encoded and carries a magic prefix plus the crate version:
restoring bytes from a different elevator-core version returns
SimError::SnapshotVersion instead of
silently producing a garbled sim. Determinism is bit-exact across builds
of the same crate version, which makes snapshots viable as rollback-netcode
checkpoints or deterministic replay fixtures.
§Performance
| Operation | Complexity |
|---|---|
| Entity iteration | O(n) via SlotMap secondary maps |
| Stop-passing detection | O(log n) via SortedStops binary search |
| Dispatch manifest build | O(riders) per group |
| Population queries | O(1) via RiderIndex reverse index |
| Topology graph queries | O(V+E) BFS, lazy rebuild |
§Runtime upgrades
Elevator kinematic and door-timing parameters can be mutated at runtime
via the Simulation::set_* setters — handy for RPG-style upgrade systems
or scripted events that boost speed, capacity, or door behavior mid-game.
Each setter validates its input, mutates the underlying component, and
emits an Event::ElevatorUpgraded so
game code can react (score popups, SFX, UI). Velocity is preserved when
kinematic parameters change — the integrator picks up the new values on
the next tick without jerk. Door-timing changes apply to the next door
cycle and never retroactively retime an in-progress transition.
See examples/runtime_upgrades.rs for an end-to-end demonstration
that doubles a car’s max_speed mid-run and prints the throughput delta.
§Door control
Games that want to drive elevator doors directly — e.g. the player
pressing “open” or “close” on a cab panel in a first-person game, or an
RPG where the player is the elevator — use the manual door-control
API on Simulation:
Each call is either applied immediately (if the car is in a matching
door-FSM state) or queued on the elevator’s
door_command_queue and
re-tried every tick until it can be applied. The only hard errors are
“not an elevator” / “elevator disabled” and (for hold_door_open) a
zero-tick argument — the rest return Ok(()) and let the engine pick
the right moment. A DoorCommand can be:
Open— reverses a closing door; no-op if already open or opening; queues while the car is moving.Close— forces an early close fromLoading. Waits one tick if a rider is mid-boarding/exiting (safe-close).HoldOpen { ticks }— adds to the remaining open dwell; two calls of 30 ticks stack to 60. Queues if doors aren’t open yet.CancelHold— clamps any accumulated hold back to the base dwell.
Every command emits
Event::DoorCommandQueued when
submitted and
Event::DoorCommandApplied when
it actually takes effect — useful for driving UI feedback (button
flashes, SFX) without polling the elevator every tick.
See examples/door_commands.rs for a runnable demo.
§Sub-tick position interpolation
Games that render at a higher framerate than the simulation ticks (e.g.
a 60 Hz sim driving a 144 Hz camera, or a first-person game where the
player is parented to an elevator car) need a smooth position between
ticks. Simulation::position_at lerps
between the snapshot taken at the start of the current tick and the
post-tick position, using an alpha accumulator clamped to [0.0, 1.0]:
// typical fixed-timestep render loop
accumulator += frame_dt;
while accumulator >= sim.dt() {
sim.step();
accumulator -= sim.dt();
}
let alpha = accumulator / sim.dt();
let y = sim.position_at(car, alpha).unwrap();The previous-position snapshot is refreshed automatically at the start
of every step. [Simulation::velocity] is
a convenience that returns the raw f64 along the shaft axis (signed:
+up, -down) for camera tilt, motion blur, or cabin-sway effects.
See examples/fp_player_rider.rs for a runnable demo.
§Manual-drive mode
Games where the player is the elevator — driving the car with a
velocity stick, slamming an emergency brake, stopping between floors
— use ServiceMode::Manual. Manual
elevators are skipped by the automatic dispatch and repositioning
phases; the consumer drives movement via:
Simulation::set_target_velocity— signed target speed (+up, -down), clamped to the car’smax_speed.Simulation::emergency_stop— commands an immediate deceleration to zero.
Physics still apply: velocity ramps toward the target using the car’s
acceleration / deceleration caps, and positions update at the same
rate as normal elevators. Manual elevators can come to rest at any
position — they are not required to align with a configured stop.
Door behaviour is governed by the manual door-control API;
nothing opens or closes automatically. Leaving Manual clears any
pending velocity command.
Each command emits
Event::ManualVelocityCommanded
with the clamped target, or None for an emergency stop.
See examples/manual_driver.rs for a runnable demo.
§ETA queries
Hall-call dispatch UIs, scheduling overlays, and “press to call” panels
all need the same answer: how long until this car shows up? Two methods
on Simulation compute it from the elevator’s queued
destinations, current kinematic state, and configured door dwell:
Simulation::eta— seconds until a specific elevator reaches a specific stop, orNoneif the stop isn’t on its route or the car is in a dispatch-excluded service mode.Simulation::best_eta— winner across all eligible elevators, optionally filtered by indicator-lamp direction (“which up-going car arrives first?”).
Both walk the queue in service order, summing closed-form trapezoidal
travel time per leg plus the configured door cycle at every intermediate
stop. The closed-form solver lives in eta::travel_time and tracks the
per-tick integrator in movement::tick_movement to within a tick or
two — close enough for UI countdowns; not a substitute for actually
simulating to compare two dispatch policies.
For narrative guides, tutorials, and architecture walkthroughs, see the mdBook documentation.
Modules§
- builder
- Fluent builder for constructing a Simulation programmatically.
Fluent builder for constructing a
Simulationprogrammatically. - components
- Entity-component data types for the simulation. Entity components — the data attached to simulation entities.
- config
- Building and elevator configuration (RON deserialization). Building and elevator configuration (RON-deserializable).
- dispatch
- Pluggable dispatch strategies (SCAN, LOOK, nearest-car, ETD). Pluggable dispatch strategies for assigning elevators to stops.
- door
- Door finite-state machine. Door open/close finite-state machine.
- energy
- Simplified energy modeling for elevators. Simplified energy modeling for elevators.
- entity
- Entity identity and allocation. Entity identity and allocation via generational keys.
- error
- Simulation error types. Error types for configuration validation and runtime failures.
- eta
- ETA estimation for queued elevators (closed-form trapezoidal travel time). Travel-time estimation for trapezoidal velocity profiles.
- events
- Simulation event bus and event types. Simulation event bus and typed event channels.
- hooks
- Lifecycle hooks for injecting logic before/after simulation phases. Lifecycle hooks for injecting custom logic before/after simulation phases.
- ids
- Typed identifiers for groups and other sim concepts. Typed identifiers for simulation concepts (groups).
- metrics
- Aggregate simulation metrics. Aggregate simulation metrics (wait times, throughput, distance).
- movement
- Trapezoidal velocity-profile movement math.
- prelude
- Common imports for consumers of this library.
- query
- ECS-style query builder for iterating entities by component composition. ECS-style query builder for iterating entities by component composition.
- scenario
- Scenario replay from recorded event streams. Scenario replay: timed rider spawns with pass/fail conditions.
- sim
- Top-level simulation runner. Top-level simulation runner and tick loop.
- snapshot
- World snapshot for save/load. World snapshot for save/load functionality.
- stop
- Stop configuration helpers. Stop identifiers and configuration.
- systems
- Tick-loop system phases (dispatch, reposition, movement, doors, loading, metrics). Tick-loop system phases run in sequence each simulation step.
- tagged_
metrics - Tag-based per-entity metrics. Tag-based per-entity metrics with string labels.
- time
- Tick-to-wall-clock time conversion. Tick-to-wall-clock time conversion.
- topology
- Topology graph for cross-line connectivity queries. Lazy-rebuilt connectivity graph for cross-line topology queries.
- traffic
- Traffic generation (arrival patterns). Traffic generation for rider arrivals.
- world
- Central entity/component storage. Central entity/component storage (struct-of-arrays ECS).
Macros§
- register_
extensions - Register multiple extension types for snapshot deserialization in one call.