Skip to main content

Crate elevator_core

Crate elevator_core 

Source
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

§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

ModulePurpose
builderFluent SimulationBuilder API
simTop-level Simulation runner and tick loop
dispatchDispatch strategies and the DispatchStrategy trait
worldECS-style World with typed component storage
componentsEntity data types: Rider, Elevator, Stop, Line, Route, Patience, Preferences, AccessControl, DestinationQueue, ServiceMode, Orientation, Position, Velocity, FloorPosition
configRON-deserializable SimConfig, GroupConfig, LineConfig
eventsEvent variants and the EventBus
metricsAggregate Metrics (wait time, throughput, etc.)
hooksLifecycle hook registration by Phase
queryEntity query builder for filtering by component composition
systemsPer-phase tick logic (dispatch, movement, doors, loading, …)
snapshotWorldSnapshot save/restore with custom-strategy factory
scenarioDeterministic scenario replay from recorded event streams
topologyLazy-rebuilt connectivity graph for cross-line routing
trafficTrafficSource trait + PoissonSource (feature-gated)
tagged_metricsPer-tag metric accumulators for zone/line/priority breakdowns
movementTrapezoidal velocity-profile primitives (braking_distance, tick_movement)
doorDoor finite-state machine (DoorState)
timeTick-to-wall-clock conversion (TimeAdapter)
energySimplified per-elevator energy modeling (gated behind the energy feature)
stopStopId and StopConfig
entityOpaque EntityId runtime identity
idsConfig-level typed identifiers (GroupId, etc.)
errorSimError, RejectionReason, RejectionContext

§Architecture overview

§8-phase tick loop

Each call to Simulation::step() runs these phases in order:

  1. AdvanceTransient — transitions Boarding→Riding, Exiting→Arrived, teleports walkers.
  2. Dispatch — builds a DispatchManifest and calls each group’s DispatchStrategy.
  3. Reposition — optional phase; moves idle elevators via RepositionStrategy for better coverage.
  4. AdvanceQueue — reconciles each elevator’s phase/target with the front of its DestinationQueue, so imperative pushes from game code take effect before movement.
  5. Movement — applies trapezoidal velocity profiles, detects stop arrivals and emits PassingFloor events.
  6. Doors — ticks the DoorState FSM per elevator.
  7. Loading — boards/exits riders with capacity and preference checks.
  8. Metrics — aggregates wait/ride times into Metrics and 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/despawn

§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

  1. Capture: sim.snapshot()WorldSnapshot
  2. Serialize: serde (RON, JSON, bincode, etc.)
  3. Deserialize + restore: snapshot.restore(factory) → new Simulation
  4. Re-register extensions: world.register_ext::<T>(name) per type
  5. 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

OperationComplexity
Entity iterationO(n) via SlotMap secondary maps
Stop-passing detectionO(log n) via SortedStops binary search
Dispatch manifest buildO(riders) per group
Population queriesO(1) via RiderIndex reverse index
Topology graph queriesO(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 from Loading. 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:

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, or None if 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 Simulation programmatically.
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.