World Runtime

How the Aether world runtime manages world lifecycle, chunk streaming, tick scheduling, input buffering, and client-side prediction.

The aether-world-runtime crate is the core simulation layer for Aether multiplayer worlds. It manages the complete world lifecycle from manifest loading through authoritative tick-based simulation, chunk streaming with level-of-detail, player input processing, and state synchronization. This guide covers how these systems work together to deliver a consistent multiplayer VR experience.

Overview

The world runtime provides a server-authoritative simulation model. The server is the single source of truth for all game state. Clients send inputs to the server, which validates and applies them, then broadcasts the resulting state. Client-side prediction and interpolation hide network latency from the player.

The runtime is composed of seven modules:

ModuleResponsibility
tickFixed-rate server tick loop with time accumulator
input_bufferPer-player input buffering and validation
predictionEntity state interpolation and prediction snapshots
state_syncDelta-based entity state broadcast
rpcTyped request/response dispatch
sessionPlayer session lifecycle management
eventsInterest-based game event distribution

All modules are pure data-driven with no async runtime or threading. The caller drives the tick loop externally.

World Lifecycle

A world instance progresses through a defined lifecycle:

  1. Manifest loading -- The world manifest declares the initial chunk layout, spawn points, entity prefabs, and scripting modules.
  2. Initialization -- The runtime loads the manifest, allocates chunk storage, and prepares the tick scheduler.
  3. Active simulation -- The tick loop runs, processing player inputs and advancing game state.
  4. Shutdown -- The world persists final state and terminates cleanly.

Tick Scheduling

The TickScheduler implements a fixed-timestep simulation using a time accumulator pattern. The caller provides elapsed wall-clock time, and the scheduler determines how many simulation ticks to run:

use aether_world_runtime::tick::TickScheduler;

let mut scheduler = TickScheduler::new(60); // 60 Hz tick rate

// In the main loop, feed elapsed microseconds
let ticks = scheduler.update(elapsed_us);
for tick in ticks {
    // Process this tick: read inputs, simulate, broadcast state
    process_tick(&tick);
}

The scheduler caps the number of ticks per update to prevent a spiral-of-death. If the server falls behind (e.g., due to a GC pause or load spike), it processes up to max_ticks_per_update and discards excess accumulated time.

Tick Pipeline

Each tick follows a consistent pipeline:

  1. Collect player inputs for the current tick number from the input buffer.
  2. Validate and apply inputs to the authoritative world state.
  3. Run physics simulation.
  4. Execute scripts.
  5. Generate delta snapshots for state synchronization.
  6. Dispatch events and RPC responses.

Input Buffering

The InputBuffer stores incoming player inputs in per-player circular buffers indexed by tick number:

use aether_world_runtime::input_buffer::{InputBuffer, PlayerInput};

let mut buffer = InputBuffer::new(64); // 64-frame buffer per player

// Queue an input from a client
buffer.push(player_id, PlayerInput {
    tick: 1042,
    movement: Vec3::new(0.0, 0.0, 1.0),
    actions: vec![Action::Jump],
})?;

// Retrieve all inputs for a specific tick
let inputs = buffer.inputs_for_tick(1042);

The buffer enforces ordering (tick numbers must be monotonically increasing), rejects duplicates, and handles buffer overflow by dropping the oldest frames.

Chunk-Based Streaming with LOD

Worlds are divided into spatial chunks. The runtime streams chunks to clients based on proximity, using level-of-detail (LOD) tiers to reduce bandwidth:

  • Full detail -- Chunks within the player's immediate vicinity. All entities and geometry are sent at full resolution.
  • Reduced detail -- Nearby chunks with simplified geometry and fewer entity updates.
  • Minimal detail -- Distant chunks with only landmark geometry and no dynamic entity data.
  • Unloaded -- Chunks beyond the streaming radius. No data is sent.

The LOD tier for each chunk is recalculated every tick based on the player's current position.

Client-Side Prediction

To hide network latency, clients predict the outcome of their own inputs locally while waiting for server confirmation. The InterpolationBuffer manages timestamped entity state snapshots:

use aether_world_runtime::prediction::InterpolationBuffer;

let mut buffer = InterpolationBuffer::new(32); // Store up to 32 snapshots

// Add a server-confirmed snapshot
buffer.push(EntityState {
    tick: 1040,
    position: Vec3::new(10.0, 0.0, 5.0),
    rotation: Quat::IDENTITY,
    velocity: Vec3::new(1.0, 0.0, 0.0),
});

// Interpolate between snapshots at a given time fraction
let interpolated = buffer.interpolate(0.5);

The interpolation uses lerp for position and velocity, and slerp for quaternion rotation.

Prediction Reconciliation

When the server state arrives, the client compares it against its predicted state. If the difference exceeds a threshold, the client applies a correction:

  1. Store each predicted state alongside the input that produced it.
  2. When the server confirms a tick, compare the server state to the predicted state for that tick.
  3. If they match (within tolerance), discard old predictions.
  4. If they diverge, snap or smoothly correct the entity to the server state and replay subsequent inputs.

State Synchronization

The StateSyncManager tracks per-entity state and generates delta snapshots for each client:

use aether_world_runtime::state_sync::StateSyncManager;

let mut sync = StateSyncManager::new();

// After each tick, generate deltas for a specific client
let deltas = sync.generate_deltas(client_id);
for delta in deltas {
    transport.send(client_id, delta, delta.channel())?;
}

// When the client acknowledges a tick, update tracking
sync.acknowledge(client_id, acked_tick);

Each entity maintains a "last acknowledged tick" per client. On each tick, the manager compares the current state against the last-acknowledged state to produce a minimal diff.

Channel Types

State updates are sent over two channel types:

ChannelUse CaseDelivery
ReliablePosition, health, inventory changesGuaranteed, ordered
UnreliableVelocity, rotation updatesLatest-wins, no retransmission

RPC System

The runtime provides a typed request/response RPC mechanism for client-server communication:

use aether_world_runtime::rpc::{RpcDispatcher, RpcRequest, RpcResponse};

let mut dispatcher = RpcDispatcher::new();

// Register a handler on the server
dispatcher.register("open_door", |req: RpcRequest| {
    let door_id: u64 = req.parse_arg(0)?;
    // Validate and open the door...
    Ok(RpcResponse::success(()))
});

// Dispatch an incoming request
let response = dispatcher.dispatch(request)?;

RPCs support both client-to-server and server-to-client directions.

Player Session Management

The SessionManager tracks player lifecycle through a state machine: Connecting -> Active -> Disconnected -> Reconnecting -> Active (or Removed).

use aether_world_runtime::session::SessionManager;

let mut sessions = SessionManager::new(30_000); // 30s reconnect window
sessions.connect(player_id, connection_metadata)?;
sessions.disconnect(player_id);
sessions.reconnect(player_id, new_connection)?; // restore within window

Event Distribution

Game events are broadcast to interested clients based on scope. Three scopes are supported: Global (all clients), NearEntity (clients near a specific entity), and Player (a single client).

use aether_world_runtime::events::{EventDispatcher, GameEvent, EventScope};

let mut events = EventDispatcher::new();
events.push(GameEvent::new("world_announcement", EventScope::Global));
events.push(GameEvent::new("explosion", EventScope::NearEntity(entity_id)));
events.push(GameEvent::new("quest_complete", EventScope::Player(player_id)));
let deliveries = events.drain();

Key Types

TypeDescription
TickSchedulerFixed-timestep tick loop with time accumulator
InputBufferPer-player circular buffer for input frames
InterpolationBufferTimestamped entity state snapshots for interpolation
StateSyncManagerDelta snapshot generation with per-client ack tracking
RpcDispatcherTyped RPC handler registry and dispatch
SessionManagerPlayer lifecycle state machine with reconnect support
EventDispatcherScoped game event distribution
PlayerInputA single frame of player input tied to a tick number
EntityStatePosition, rotation, and velocity snapshot for an entity