# Wire protocol v1 Binary frames over a single WebSocket at `/ws/game`. One protocol message per frame, no framing header beyond the message id. **All multi-byte numbers are little-endian.** Three files must stay in sync — change them in the same commit: | Where | File | | --- | --- | | Server codec | [`src/HSchool.Protocol/ProtocolCodec.cs`](../src/HSchool.Protocol/ProtocolCodec.cs) | | Client codec | [`src/HSchool.Client/src/net/protocol.ts`](../src/HSchool.Client/src/net/protocol.ts) | | This document | `docs/protocol.md` | Any change to a layout below bumps `ProtocolConstants.Version` / `PROTOCOL_VERSION`. The server closes connections whose hello carries a different version with `1002 ProtocolError`. ## Message ids Client-to-server ids live in `0x00–0x7F`, server-to-client ids in `0x80–0xFF`, so a misrouted frame is obvious at a glance. | Id | Direction | Message | | --- | --- | --- | | `0x01` | C → S | Hello | | `0x02` | C → S | Input | | `0x03` | C → S | Ping | | `0x81` | S → C | Welcome | | `0x82` | S → C | Snapshot | | `0x83` | S → C | Pong | ## Client → server ### `0x01` Hello Must be the first frame; the server drops the connection if it does not arrive within 5 seconds. | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x01` | | 1 | `u8` | protocol version | | 2 | `u8` | name length in bytes (≤ 32) | | 3 | `u8[]` | UTF-8 name | ### `0x02` Input Sent at ~30 Hz whether or not the mask changed. | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x02` | | 1 | `u32` | sequence number, monotonically increasing | | 5 | `u8` | button mask | Button mask: `1` up, `2` down, `4` left, `8` right. Frames with a sequence lower than the last accepted one are ignored, so a late packet cannot undo a newer intent. ### `0x03` Ping | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x03` | | 1 | `i64` | client clock in milliseconds | ## Server → client ### `0x81` Welcome — 15 bytes The first frame the client receives; no snapshot is queued before it. | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x81` | | 1 | `u8` | protocol version | | 2 | `u32` | replication id of this client's own avatar | | 6 | `u8` | tick rate in Hz | | 7 | `f32` | world width | | 11 | `f32` | world height | ### `0x82` Snapshot — 7 + 21·N bytes Full state, no delta compression yet. **Entities missing from a snapshot are despawned by the client**, which is why every visible entity is present in every frame. Header: | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x82` | | 1 | `u32` | tick | | 5 | `u16` | entity count | Then, per entity (21 bytes): | Offset | Type | Field | | --- | --- | --- | | +0 | `u32` | replication id (never reused within a session) | | +4 | `u8` | kind: `0` unknown, `1` player, `2` obstacle | | +5 | `f32` | x | | +9 | `f32` | y | | +13 | `f32` | radius | | +17 | `u32` | colour, packed `0x00RRGGBB` | ### `0x83` Pong — 13 bytes | Offset | Type | Field | | --- | --- | --- | | 0 | `u8` | `0x83` | | 1 | `i64` | client clock, echoed unchanged | | 9 | `u32` | server tick when the ping was handled | ## Guarantees and limits - Frames larger than 64 KiB are refused with close status `1009 MessageTooBig`. - A malformed frame closes the connection with `1007 InvalidPayloadData`. - Unknown message ids are ignored rather than fatal, so new ids can be added without breaking older clients within the same protocol version. - Snapshot delivery is lossy under back pressure: each connection buffers 32 frames and drops the oldest, because a stale snapshot is worthless once a newer one exists. ## Not in v1 yet Client-side prediction and reconciliation (the `sequence` field exists for it but is never echoed back), delta compression, interest management, and any form of authentication.