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
applycallback. 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 fromSession.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, ornilif 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 gainsseq. 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. Optionaloptions.markerisnormal,teleport, orreset; 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, ornilwhen 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, ornil. 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, ornilwhen no sample is available.
local d = Session.prediction_diagnostics()
Session.prediction_diagnostics()— returns a table withchannelskeyed 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, anddistance; returnsnilon a miss or unavailable history.
local d = Session.query_history_diagnostics()
Session.clear_query_history()
Session.query_history_diagnostics()— returnsdepth,latest_tick,recorded,rejected,dropped,queries,clamped_queries, andmisses.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
| Symptom | What to adjust |
|---|---|
| Local movement feels fine, remote players stutter | Raise interpolation_delay slightly, or check packet loss with diagnostics() counters. |
| Remote players freeze at edges | Raise extrapolation_limit, accepting longer guessing. |
| Frequent visible snap-backs | Check correction counters and the deterministic apply callback; a lower positive prediction_visual_decay retains visual offsets longer. |
| Inputs stall behind one lost packet | Lower input_head_of_line_timeout. |
| Hit checks disagree across peers | Run 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.