124 lines
3.7 KiB
Markdown
124 lines
3.7 KiB
Markdown
# 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.
|