Skip to content

Networking

Status: Current Last reviewed: 2026-07-07 Source: shared/src/protocol.ts, shared/src/constants.ts, server/src/game/zone.ts

The transport is one WebSocket per client carrying JSON messages. Every message has a string t discriminator. The protocol is versioned by PROTOCOL_VERSION (2); a client that sends the wrong version in join_world is rejected and disconnected.

Client to Server

Message Purpose
join_world Enter the world with a protocol version and a name.
player_input One fixed-step input command: seq, dt, moveX, moveZ, yaw, pitch, jump, sprint, block. Never carries a position.
equip_weapon Request a weapon swap (blocked while the weapon state machine is busy).
attack Start a melee swing (or a quick snap shot with the bow) with an aim yaw/pitch.
draw_start Begin drawing the bow. The server timestamps the draw itself.
draw_cancel Abandon a draw without loosing.
draw_release Loose the arrow with a final aim direction.
cast_spell Cast fireball, lightning_bolt, or rift_burst with an aim direction.
interact Interact with a target id (loot bag or NPC).
quest_action accept, turn_in, or abandon a quest.
use_item Consume an item (healing or mana draught).
chat Send a chat line.
ping Latency probe; echoed as pong.
request_zone Ask for the full zone document (sent on a cache miss).
admin_login Authenticate an editor session with a token.
admin_cmd Issue one editor/playtest command (see Level Editor).

Server to Client

Message Purpose
welcome Join accepted: self id, zone id/name, zoneHash, tick rate, inventory, skills, quest definitions, quest log, ember snapshot, admin flag.
zone_data The full ZoneData document plus its hash (answer to request_zone or after an import/reload).
zone_object_update A single editor change (create/update/delete) plus the new hash, so live clients rebuild one object.
server_snapshot The per-tick world state (see below).
entity_spawned / entity_removed An entity entered or left the client's view.
damage_event A hit resolved: target, source, amount, kind, position, killed, blocked.
loot_event Items granted from a loot bag.
skill_progress_event A skill advanced (and whether it levelled).
inventory_update The player's inventory changed.
chat A chat line on the say, system, or combat channel.
dialogue An NPC's lines plus the quests it can offer or complete for this player.
quest_update New per-objective progress for one quest.
quest_event A quest was accepted, became ready, or was turned in (with rewards).
ember_state The current Emberwake snapshot (level, pressure, gate, champion).
effect A one-shot visual/audio cue (explosion, lightning beam, parry, ground slam, ember shift, gate open, and so on).
death / respawn The player died (with a respawn timer) or respawned.
pong Latency reply.
error A fatal or advisory message.
admin_state Result of an admin_login (is-admin plus a message).
admin_result Result of an admin_cmd (ok plus a message).

Snapshot Structure

server_snapshot is the heartbeat, sent at 15 Hz:

  • tick, time (server clock), ackSeq (the highest input sequence the server has processed for this client).
  • self: the authoritative local player state, including position, velocity, on-ground flag, hp/stamina/mana and their maxima, weapon, dead and safe-zone flags, the weapon-state-machine value wstate, an optional bow draw amount, and any non-zero cooldowns.
  • entities: an array of small EntitySnap records (id, position, yaw, and where relevant pitch, velocity, hp, anim state, weapon) for everything else in view.

Full descriptions arrive once via entity_spawned; snapshots then carry only the small changing fields.

Interest Management

Foundations are in place; full per-player spatial interest is future work.

  • Entities beyond INTEREST.nearRange (90 m) from a player are sent only on every INTEREST.farEvery-th snapshot (every 4th).
  • Combat-relevant entities (aggro'd goblins, all projectiles) are always included regardless of distance.
  • The starter zone is small, so this mainly exercises the mechanism. A production build will replace it with a real spatial grid and per-client subscription sets.

Zone Data Loading

Map geometry is never streamed per tick. welcome carries a zoneHash (an FNV-style hash of the zone document). The client keeps the last zone in localStorage; on a hash match it builds from cache, otherwise it sends request_zone and receives one zone_data message. Editor edits arrive as small zone_object_update messages rather than whole-zone resends.

Prediction and Interpolation

  • The client predicts its own movement by running the shared stepMovement on each player_input. On each snapshot it rewinds to the server's authoritative self, replays inputs newer than ackSeq, and folds any small residual into a decaying visual offset so the camera never pops. Corrections larger than 3 m snap.
  • Remote players, goblins, and projectiles are rendered about INTERP_DELAY_MS (140 ms) in the past, interpolating between the two buffered snapshot states that straddle the render time.

Anti-Cheat Sanity Checks

The trust boundary is thin in the prototype (names are unauthenticated), but the server never trusts client-supplied physics:

  • dt per input is clamped to MAX_INPUT_DT (0.1 s) and at most MAX_INPUTS_PER_TICK (6) inputs are consumed per tick; the queue is bounded.
  • Horizontal speed is clamped to MOVE.maxSpeed inside the shared movement step, which the server also runs.
  • Stamina, mana, cooldowns, and bow draw time are all validated server-side. A client that claims a full draw still only gets the draw the server timed.
  • Pitch is clamped; chat is length-limited and name characters are filtered.

Known limitations

There is no delta-compression of snapshots yet (each snapshot is a full state for the entities in view), no per-client spatial interest beyond the near/far split above, and no account authentication. These are called out on the Roadmap.