DEMIENGINEDOCUMENTATION Everything is free
Networking & HTTP · Game systems

Client prediction and historical queries.

Prediction

Networked input takes time to reach the host. Prediction applies a locally owned entity's input immediately, then reconciles it with the host's authoritative controller state. Query history stores selected 2D collision circles on the host so a late hit query can use a recent past tick. Both APIs live in demi.network.session; see Sessions & contracts for the contract and ownership rules.

Why prediction exists

Authority stays with the server: the server's state is the truth. But waiting a full round trip for every input makes controls feel heavy. With prediction:

  • The input immediately updates the client's predicted controller state through the game-defined apply callback. Game code applies that state to its local entity.
  • Inputs are also sent to the server as messages declared in the contract.
  • When an authoritative state snapshot arrives, the client compares and, if needed, reconciles.
  • Remote entities interpolate toward received states and stop extrapolating after a limit.

The result is responsive local control with server authority preserved. Prediction is optional; games that do not need it never enable it.

Configuring prediction

Call configure once during startup with the queue and smoothing limits your game needs:

local Session = require("demi.network.session")

Session.configure({
    input_queue_capacity = 128,
    input_future_window = 8,
    input_head_of_line_timeout = 0.25,
    input_max_per_tick = 4,
    snapshot_buffer = 32,
    interpolation_delay = 0.1,
    extrapolation_limit = 0.25,
    prediction_history_limit = 32,
    prediction_visual_decay = 12.0
})
  • input_queue_capacity — maximum queued authoritative inputs per entity.
  • input_future_window — maximum accepted sequence distance ahead of the current queue.
  • input_head_of_line_timeout — seconds to wait for a missing sequence before skipping it.
  • input_max_per_tick — maximum input frames applied in one simulation tick.
  • snapshot_buffer — how many authoritative state snapshots to retain for reconciliation.
  • interpolation_delay — seconds remote entities render behind the latest state, smoothing jerky arrivals.
  • extrapolation_limit — seconds a remote entity may keep moving on its last known state before freezing instead of guessing.
  • prediction_history_limit — how many local prediction states to keep for rollback comparison.
  • prediction_visual_decay — exponential visual-offset decay rate per second. Zero disables smoothing.

configure returns nothing. Prediction is opt-in and requires nonzero configured history; invalid or missing prediction requirements cause enable_prediction to return false.

Enabling prediction for an entity

Prediction is per entity. enable_prediction registers how that entity's inputs are named, what its state looks like, and how to apply one tick of input:

local enabled = Session.enable_prediction({
    network_id = network_id,
    input_message = "move_input", -- owner-to-server owned_entity contract message
    state = { x = 0, y = 0 },
    apply = function(state, input)
        return {
            x = state.x + input.dx * input.dt,
            y = state.y + input.dy * input.dt,
        }
    end
})
  • network_id — the replicated entity's network ID from Session.spawn.
  • input_message — the contract message name that carries this entity's inputs.
  • state — the initial predicted controller state as an object-like serializable table.
  • apply(state, input) — a deterministic function that applies one input table to the state and returns the new state. This is what gets replayed during rollback.

Returns true when prediction was enabled. It requires a connected client that owns this contract entity. The callback must return a table; a failed callback disables prediction.

The predict and reconcile cycle

local input = { dx = 1, dy = 0, dt = fixed_dt, tick = local_tick }
local sequence = Session.predict_input(network_id, input)
  • Session.predict_input(network_id, input) — applies one input locally, records it for replay, and sends it through the declared contract message. Returns its sequence number, or nil if prediction could not produce an input. Include any game-specific fields required by the message schema.
for _, input in ipairs(Session.take_inputs(network_id)) do
    state = apply_input(state, input) -- input carries authored fields and seq
end
  • Session.take_inputs(network_id) — host only: returns ordered accepted input tables ready for this authoritative tick. Each table retains the input fields and gains seq. Apply them in order.
local published = Session.publish_snapshot(network_id, state)
  • Session.publish_snapshot(network_id, state, options) — host only: records and sends an authoritative object state. Returns a boolean. Optional options.marker is normal, teleport, or reset; teleport and reset clear incompatible client history.
local state = Session.prediction_state(network_id)  -- table or nil
  • Session.prediction_state(network_id) — returns the current predicted state table for the entity, or nil when prediction is not enabled for it.
local predicted = Session.prediction_state(network_id)
local offset = Session.prediction_visual_offset(network_id)
if predicted then draw_x = predicted.x + (offset and offset.x or 0) end
  • Session.prediction_visual_offset(network_id) — returns a table of numeric offsets keyed by controller-state field, or nil. Use it only in rendering; do not feed it back into the simulation.
local remote = Session.remote_state(network_id)  -- table or nil
  • Session.remote_state(network_id) — returns an interpolated, boundedly extrapolated remote state table, or nil when no sample is available.
local d = Session.prediction_diagnostics()
  • Session.prediction_diagnostics() — returns a table with channels keyed by network ID; each channel reports replay, correction, server-queue, and interpolation counters.
local rebased = Session.reset_prediction(network_id, new_state)
  • Session.reset_prediction(network_id, state) — rebases one enabled controller to a supplied object state after a same-session scene change. Returns a boolean.

Query history: rewinding questions

Query history keeps bounded snapshots of selected 2D collision circles on the authoritative host. It answers raycasts without rewinding the live world. Gameplay must decide which circles to record and how to interpret the hit.

Session.configure({
    query_history_capacity = 128,
    query_history_max_entities = 64,
    query_history_rewind_ticks = 32
})
  • query_history_capacity — how many ticks of snapshots to retain.
  • query_history_max_entities — maximum circles in one snapshot.
  • query_history_rewind_ticks — maximum requested rewind. Queries beyond this bound are clamped to the retained window and report their actual sampled tick.
local recorded = Session.record_query_snapshot(server_tick, {
    { entity_id = target_network_id, layer = "players", x = x, y = y, radius = 0.45 },
})
  • Session.record_query_snapshot(server_tick, circles) — host with a contract only. Stores a detached set of authored circles under a monotonically increasing tick; returns a boolean. Invalid or over-limit snapshots are rejected.
local hit = Session.historical_raycast(
    requested_tick, origin_x, origin_y, direction_x, direction_y,
    max_distance, "players", shooter_network_id
)
if hit then
    -- hit.entity_id, hit.x, hit.y, hit.distance, hit.sampled_tick
end
  • Session.historical_raycast(requested_tick, origin_x, origin_y, direction_x, direction_y, maximum_distance, layer, ignored_entity_id) — host only. Casts a 2D ray through recorded circles, optionally filtering by layer and ignoring one entity.
  • Returns a hit table with entity_id, layer, sampled_tick, x, y, normal_x, normal_y, and distance; returns nil on a miss or unavailable history.
local d = Session.query_history_diagnostics()
Session.clear_query_history()
  • Session.query_history_diagnostics() — returns depth, latest_tick, recorded, rejected, dropped, queries, clamped_queries, and misses.
  • Session.clear_query_history() — host only. Drops recorded snapshots and returns a boolean. Use at an authoritative scene or tick reset boundary.

Tuning and determinism

SymptomWhat to adjust
Local movement feels fine, remote players stutterRaise interpolation_delay slightly, or check packet loss with diagnostics() counters.
Remote players freeze at edgesRaise extrapolation_limit, accepting longer guessing.
Frequent visible snap-backsCheck correction counters and the deterministic apply callback; a lower positive prediction_visual_decay retains visual offsets longer.
Inputs stall behind one lost packetLower input_head_of_line_timeout.
Hit checks disagree across peersRun hit checks on the host, record the intended circles every authoritative tick, and inspect sampled_tick and query diagnostics.

Prediction requires a deterministic apply(state, input) callback for replay. Keep game-defined timing and random inputs explicit; see Simulation & seeds.

Contract rules for who may send inputs and state live in Sessions & contracts.