Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Extensions

The core library is deliberately unopinionated – it provides riders, elevators, and stops, but your game decides what a rider means. Maybe riders have a VIP status, a mood, a destination preference, or a cargo manifest. Extensions let you layer game-specific data on top of any entity without forking or wrapping the library.

Extension components

Extension components attach arbitrary typed data to any entity. They work like the built-in components (Rider, Elevator, etc.) but are defined by your code.

Define your type

Extension types must implement Serialize and DeserializeOwned (for snapshot support), plus Send + Sync:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct VipTag {
    level: u32,
    lounge_access: bool,
}
}

Register with the builder

Call .with_ext::<T>() on the builder to register the extension type. The type name is used automatically for snapshot serialization:

use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VipTag { level: u32, lounge_access: bool }
use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;

fn main() -> Result<(), SimError> {
    let mut sim = SimulationBuilder::new()
        .stop(StopId(0), "Ground", 0.0)
        .stop(StopId(1), "Top", 10.0)
        .elevator(ElevatorConfig::default())
        .with_ext::<VipTag>()
        .build()?;
    Ok(())
}

Attach to entities

Use world.insert_ext() to attach your component to an entity:

use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VipTag { level: u32, lounge_access: bool }
use elevator_core::prelude::*;
fn main() -> Result<(), SimError> {
let mut sim = SimulationBuilder::new()
    .stop(StopId(0), "Ground", 0.0)
    .stop(StopId(1), "Top", 10.0)
    .elevator(ElevatorConfig::default())
    .with_ext::<VipTag>()
    .build()?;
let rider_id = sim.spawn_rider(StopId(0), StopId(1), 75.0)?;

sim.world_mut().insert_ext(
    rider_id.entity(),
    VipTag { level: 3, lounge_access: true },
    ExtKey::from_type_name(),
);
Ok(())
}

Read it back

Use world.ext() for a cloned value, world.ext_ref() for a zero-copy borrow, or world.ext_mut() for a mutable reference:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VipTag { level: u32, lounge_access: bool }
use elevator_core::prelude::*;
fn run(sim: &mut Simulation, rider_id: EntityId) {
// Read (cloned)
if let Some(vip) = sim.world().ext::<VipTag>(rider_id) {
    println!("VIP level: {}", vip.level);
}

// Mutate
if let Some(vip) = sim.world_mut().ext_mut::<VipTag>(rider_id) {
    vip.level += 1;
}
}
}

Query with extensions

Extensions integrate with the query builder for ECS-style iteration:

#![allow(unused)]
fn main() {
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct VipTag { level: u32, lounge_access: bool }
use elevator_core::prelude::*;
use elevator_core::query::Ext;
fn run(world: &mut World) {
// Read-only iteration (cloned via Ext<T>)
for (id, vip) in world.query::<(EntityId, &Ext<VipTag>)>().iter() {
    println!("{:?} is VIP level {}", id, vip.level);
}

// Mutable access
world.query_ext_mut::<VipTag>().for_each_mut(|id, tag| {
    tag.level += 1;
});
}
}

The mutable query collects entity IDs first, then iterates with mutable borrows, so it is safe to use without aliasing issues.

World resources

For global data that is not attached to a specific entity, use resources. Resources are typed singletons stored on the World:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
fn run(sim: &mut Simulation) {
// Insert a resource
sim.world_mut().insert_resource(42u32);

// Read it
if let Some(value) = sim.world().resource::<u32>() {
    println!("The answer is {}", value);
}

// Mutate it
if let Some(value) = sim.world_mut().resource_mut::<u32>() {
    *value += 1;
}
}
}

Resources are useful for game state that hooks need to read or write – score counters, time-of-day multipliers, spawn rate controllers, and so on.

Extension vs. resource: which do I want?

NeedUse
Per-entity data that varies by rider/elevator (VIP status, mood, cargo)Extension (with_ext + insert_ext)
One-of value for the whole sim (score, difficulty, tick clock mirror)Resource (insert_resource)
Data that must survive snapshot save/loadExtension (registered by name; resources are not snapshotted)
Quick scratchpad you can wipe between ticksResource
Query “all entities that have X”Extension (query or iterate + filter on get_ext)

Extensions are auto-cleaned on despawn_rider; resources persist until you remove them.

Snapshot integration

Extension components are serialized by their registered type name into the WorldSnapshot. To restore them correctly after loading a snapshot:

  1. Register the extension types on the restored simulation’s world.
  2. Call sim.load_extensions() to deserialize and attach the pending data.
#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
use elevator_core::snapshot::WorldSnapshot;
use serde::{Serialize, Deserialize};
#[derive(Clone, Serialize, Deserialize)] struct VipTag { level: u32 }
fn run(snapshot: WorldSnapshot) {
let mut sim = snapshot.restore(None).unwrap();
sim.world_mut().register_ext::<VipTag>(ExtKey::from_type_name());
sim.load_extensions();
}
}

Unregistered types remain in a PendingExtensions resource until you register them. If you have many extension types, use the register_extensions! macro to register them all in one call.

The key rule: register with with_ext before building, or with register_ext before restoring. If an extension type is missing at restore time, its data is silently held in pending storage rather than lost.

Auto-cleanup on despawn

Extension components are automatically removed when an entity is despawned. You don’t need to manually clean up extension data – despawn_rider, remove_elevator, and other removal APIs handle it.

This means you can freely attach extensions to riders that will be delivered and despawned without worrying about leaked data in the extension storage maps.

Next steps