five-on-it / packages/protocol/src/snapshot-v2
packages/protocol/src/snapshot-v2
Delta snapshots: each client is sent only what changed since the snapshot it was last sent (TECHNICAL_SPECS §6.4). The connection is ordered and reliable (a WebSocket), so the server knows exactly what the client holds without acknowledgements: every frame is applied, in order, onto the one before it.
delta u8 kind=5 | u32 tick | u32 baseTick | u32 lastSeq | u8 inputBuffer | u8 flags
| u16 removed | u16 changed | [u32 check] | u16 eid* | change* | u8 events | event*
change u16 eid | u16 mask | [u8 age] | each group's bytes, in group orderAn entity is its full snapshot's record (ENTITY_RECORD_BYTES, codec.ts) followed by its pose quantized (QUANT_POSE_BYTES, quantize.ts), cut into groups that travel only when their bytes change (GROUPS): its position, its rotation, its velocity (the quantized pose), what drives it and its health, what a person does, its flags, who and what it is (its life under its id among them, GEN_AT), its full pose (13 floats, sent instead of the quantized ones for the client's own character, car and trailer: its prediction replays from them) and its spin (quantized). A position that moved less than 128 m on every axis since the last one sent travels as three 16-bit steps from it (SMALL_STEPS, six bytes instead of twelve).
The mask of a change is its groups' bits (0 to 8) and, above them, SMALL_STEPS (bit 9), the acked snapshots' POSE_FULL (bit 10), the entity's tier for this client (bits 11 and 12, TIER_SHIFT: how often its pose comes, TIER_EVERY) and POSE_AGE (bit 13: the pose carried was measured age ticks before the frame's tick, a byte after the mask; the acked snapshots, which may carry a pose measured in a frame that was lost). The tier is in every change of the entity, and a change with no group carries a tier that changed alone.
A far entity's pose goes on its turn only (DeltaFrame.due), and whenever its rest changes against the pose the client holds (changedGroups: it set off, or it came to a stop): a stop and a start are news at once, so the client never draws a far entity moving while the server has it standing, nor standing while it moves. At rest, the pose the client holds is exact for every tick until the next one comes.
A new entity comes whole; one gone is in the removed list, and so is one whose life changed under its id (the simulation turns an id's generation over when it frees the id; the record carries it, GEN_AT): it is removed and comes whole again in the same frame, so the client sees a new entity, never one moving from where the old one stood. A keyframe (the first frame, and any after the client asked again with a resync) makes the client forget what it held first. Every CHECK_EVERY frames a check follows the header, an FNV-1a hash of every record the client should hold: a client that finds otherwise, or a frame whose base is not the last one it applied, asks for a keyframe.
The server quantizes every entity once a tick (EntityArena) and keeps one SnapshotEncoder per client; the client keeps a SnapshotDecoder, which turns every frame (a full snapshot or a delta) back into a whole SnapshotMessage (its entities in a whole snapshot's order: by kind, then id) and says what the frame told it: the entities whose state changed, those whose pose it measured (with the tick of the measurement), those removed since the last frame shown (a new life among them) and the tier of each change (DecodedFrame).
Classes
Interfaces
Type Aliases
Variables
- CHECK_EVERY
- FULL
- GEN_AT
- GROUP_BYTES
- GROUP_MASK
- GROUPS
- HEADER_BYTES
- KIND_DELTA
- POSE_AGE
- POSITION
- QUANT_AT
- QUANTIZED
- RECORD_BYTES
- RESYNC_REQUEST
- SMALL_STEPS
- TIER_EVERY
- TIER_SHIFT