Easy Multiplayer v0.4 Consumer API

This is the game developer's reference for the public v0.4 authoring surface. Internal engine, ledger, bus, and worker classes are not compatibility APIs.

Start with the quickstart.

Import

import { EasyMultiplayer } from
  'https://libs.letsinspire.com/easy_multiplayer/releases/0.4/EasyMultiplayer.js';

Primary namespace:

EasyMultiplayer.performanceMode

defineGame(definition)

Creates and freezes a game definition.

const game = EasyMultiplayer.performanceMode.defineGame({
  initialState: () => ({}),
  queries: {},
  step(ctx) {},
});

Fields:

Field Required Meaning
initialState yes Factory or initial canonical state. Prefer a factory.
step(ctx) yes One deterministic simulation tick.
queries no Named pure query predicates.
schema no Opts into schema-generated state. Omit for default proxied state.
simpleMode no Enables rawInputs(). Defaults to false.
autoDiscoverParticipants no Worker-game admission convenience; product games normally perform explicit discovery.

Game delivery forms:

game: inlineDefinition

Inline games are time-sliced only.

game: {
  module: new URL('./game.js', import.meta.url),
  export: 'createGame',
  options: {},
}

Module games work in worker and time-sliced modes. The factory options must be structured-cloneable and identical on every peer.

createSession(options)

const session = EasyMultiplayer.performanceMode.createSession({
  room: { id: 'room-123', appId: 'my-game' },
  game: { module: gameUrl, export: 'createGame' },
  getLocalInputs,
  tickMs: 50,
  maxParticipants: 4,
  dev: true,
});

Stable options

Option Meaning
room: {id, appId} Built-in peer-to-peer room.
transport Custom transport. Mutually exclusive with room.
game Inline game or module reference.
getLocalInputs() Samples the local intent envelope.
runtimeMode 'worker-multi-bus' or 'time-sliced'; worker is the default.
buses Total simulation bus count, including render bus.
tickMs Fixed simulation tick duration. Default 50.
maxParticipants Admission ceiling. Spectators may still connect.
time Optional live {offsetMs()} provider.
dev Enables strict deterministic guards and diagnostics.
advanced Validated expert configuration; not a long-term compatibility surface.

Input envelope

getLocalInputs: () => ({
  discoverable: true,
  stopParticipating: false,
  input: {
    left: held.left,
    jumpPress: counters.jump,
  },
})

Session methods

Method Meaning
connect() Starts room/network participation.
disconnect() Stops the session and owned transport.
ready() Resolves when game/runtime prerequisites are loaded.
pump(options?) Advances host/session work; normally called each animation frame.
settle() Test/diagnostic settling primitive.
getMirror() Current read-only render mirror.
getPresentationHints() Render-only interpolation hints.
getRuntimeStatus() Stable runtime summary.
diagnostics() Detailed unstable diagnostics.

Runtime status includes runtimeMode, runtimeFallback, tick, connected, isLive, localId, participant counts/peer IDs, and rollback count.

Step context

step(ctx) receives:

Member Meaning
ctx.state Live canonical state for this tick.
ctx.tick Canonical simulation tick.
ctx.dt Tick duration supplied to the runtime.
ctx.query(ids, frozenContext, predicateOrName) Rollback-tracked input decision.
ctx.participants() Current admitted stable IDs.
ctx.isParticipant(id) Membership guard for a possibly spectator/departed ID.
ctx.discoverParticipants(predicate, limit, context?) Deterministic admission query.
ctx.getStoppedParticipating() Drains canonical departures.
ctx.releaseParticipant(id) Game-initiated departure.
ctx.queryDisconnected(id, tick?) Rollback-tracked liveness decision.
ctx.rawInputs() Simple-mode field-query proxy.
ctx.trail(id, data, time?) Render-only movement sample.
ctx.clearTrail(id) Clears a trail across teleport/wrap/death.
ctx.emit(type, payload) Deterministic presentation event.
ctx.world() Convenience facade over the same context.

Query predicates

Gameplay query:

const moveAxis = (inputs, context) =>
  Number(inputs[0]?.right) - Number(inputs[0]?.left);

inputs[i] corresponds to participantIds[i]. Access builds the dependency read-set. context is frozen/canonicalized for later rechecks. Return a specific serializable decision, never the inputs accessor itself.

Discovery predicate:

const claimSlot = (input, context) => input?.claimSlot === context.slot;

Discovery receives one candidate input as its first argument, not the gameplay inputs array. The second argument is the frozen context supplied to discoverParticipants.

rawInputs()

Requires:

simpleMode: true

Usage:

const inputs = ctx.rawInputs();
if (inputs[playerId].left) moveLeft();

Each property access is one standard query. It remains branch-sensitive and rollback-correct, but separate fields produce separate decision results. Under dev, first use warns that named queries are preferred.

World facade

Both schema and proxied worlds route engine services:

const world = ctx.world();
world.query(...);
world.participants();
world.isParticipant(id);
world.discoverParticipants(...);
world.rawInputs();
world.emit(...);

Schema mode additionally provides class-bound table helpers:

world.get(Type, id);
world.all(Type);
world.spawn(Type, id, record);
world.owned(Type, ownerId, factory);
world.destroy(entityOrType, id);
world.table(name);
world.id(prefix, ...parts);

Canonical state rules

Default proxied state supports plain objects, arrays, and JSON scalars. It rejects functions, class instances, Map, Set, Date, undefined, and object aliases.

Standard array reads work normally. Mutating arrays support push, pop, shift, unshift, splice, sort, reverse, copyWithin, and fill, provided the final state contains no persistent object aliases.

Writes are legal only inside step. Canonical relationships should use stable IDs:

state.heldItemId = item.id;

Do not store the same mutable item object in two locations.

Mirror contract

Proxied mode:

const mirror = session.getMirror();
const state = mirror.state; // deeply frozen

Schema mode:

const players = mirror.tryTable('players');
for (const id of players.ids()) {
  const row = players.get(id);
}

Treat mirrors as replaceable, read-only projections. Never use mirror values to drive canonical simulation.

Presentation

Simulation code must not access DOM, canvas, audio, timers, or sockets.

Use trails for interpolation:

ctx.trail(player.id, { x: player.x, y: player.y });

Use deterministic events for one-shot presentation:

ctx.emit('sound', { key: `jump:${player.id}:${ctx.tick}`, name: 'jump' });

Only finalized events should reach irreversible browser effects.

Advanced configuration

Common expert settings:

advanced: {
  engine: {
    inputDelay: 1,
    autoRelease: { demote: 'consume' },
  },
  backend: {
    snapshotInterval: 10,
    patchesInterval: 3,
  },
  workerPoolSize: 3,
}

Advanced keys are validated but intentionally outside the stable compatibility contract. Measure before tuning.

Custom transport

Provide one of room or transport. A custom transport supplies stable local identity, message delivery, peer events, current peers, disconnect, and the clock-sync channel. See contracts/transport.md for the exact interface.

Determinism checklist