Update .gitignore to exclude TypeScript build info and add dist directory. Expand README with project overview, technology stack, prerequisites, and instructions for running and testing the application.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user