Refactor project structure and update documentation. Replace PixiJS with plain DOM for UI rendering, enhance README with game features, and revise protocol documentation for HTTP API. Remove unused files and streamline client code for better maintainability.
This commit is contained in:
+193
-123
@@ -1,123 +1,193 @@
|
||||
# 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.
|
||||
# Wire protocol v3
|
||||
|
||||
The client talks to the server two ways:
|
||||
|
||||
- **HTTP/JSON** for the main menu — listing, creating and deleting schools. Those are
|
||||
request/response by nature, so they are plain REST.
|
||||
- **A binary WebSocket at `/ws/game`** for the school calendar, which changes 20 times a second.
|
||||
|
||||
This document covers both. One protocol message per WebSocket 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`.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Game dates are ISO-8601 UTC instants. The in-game calendar has no time zone — UTC is only used so
|
||||
the wire format is unambiguous, and the client formats it back in UTC.
|
||||
|
||||
### `GET /api/schools`
|
||||
|
||||
Everything the main menu needs in one request.
|
||||
|
||||
```json
|
||||
{
|
||||
"maxSchools": 6,
|
||||
"defaultStartDate": "2012-04-03T06:00:00Z",
|
||||
"gameMinutesPerRealSecond": 5,
|
||||
"schools": [
|
||||
{ "id": 1, "name": "Гимназия №14", "gameTime": "2012-04-03T07:35:00Z", "running": false, "speedIndex": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /api/schools/random-name`
|
||||
|
||||
`{ "name": "Лицей «Северная»" }` — a suggestion that is not already taken.
|
||||
|
||||
### `POST /api/schools`
|
||||
|
||||
Body: `{ "name": "Гимназия №14", "startDate": "2012-04-03T06:00:00Z" }`
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| `201` | Created; body is the school. |
|
||||
| `400` `invalid-name` | Blank, or longer than 40 characters. |
|
||||
| `400` `invalid-start-date` | Outside 1900–2999. |
|
||||
| `409` `school-limit-reached` | `maxSchools` schools already exist. |
|
||||
|
||||
Failures are RFC 7807 problem details with an extra `code` field — that is what the UI switches on.
|
||||
|
||||
### `DELETE /api/schools/{id}`
|
||||
|
||||
`204` when deleted, `404` when the id is unknown. Anyone watching that school over a WebSocket
|
||||
gets a `SchoolGone` frame.
|
||||
|
||||
## WebSocket 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 | Ping |
|
||||
| `0x03` | C → S | OpenSchool |
|
||||
| `0x04` | C → S | CloseSchool |
|
||||
| `0x05` | C → S | SetRunning |
|
||||
| `0x06` | C → S | SetSpeed |
|
||||
| `0x81` | S → C | Welcome |
|
||||
| `0x82` | S → C | Pong |
|
||||
| `0x83` | S → C | Clock |
|
||||
| `0x84` | S → C | SchoolGone |
|
||||
|
||||
## Client → server
|
||||
|
||||
### `0x01` Hello — 2 bytes
|
||||
|
||||
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 |
|
||||
|
||||
### `0x02` Ping — 9 bytes
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x02` |
|
||||
| 1 | `i64` | client clock in milliseconds |
|
||||
|
||||
### `0x03` OpenSchool — 5 bytes
|
||||
|
||||
Starts watching a school: clock frames for it begin to arrive. It does not start the calendar —
|
||||
every school runs on its own from the moment it is created.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x03` |
|
||||
| 1 | `i32` | school id |
|
||||
|
||||
### `0x04` CloseSchool — 1 byte
|
||||
|
||||
Back to the menu: the clock frames stop. The school keeps running — only `SetRunning` pauses it,
|
||||
and that pause survives leaving and reconnecting.
|
||||
|
||||
### `0x05` SetRunning — 2 bytes
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x05` |
|
||||
| 1 | `u8` | `1` running, `0` paused |
|
||||
|
||||
### `0x06` SetSpeed — 2 bytes
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x06` |
|
||||
| 1 | `u8` | speed index |
|
||||
|
||||
Running and speed are **separate messages on purpose**. A single "set clock" message forces each
|
||||
button to resend the other field from the client's own copy of the state, which is always at least
|
||||
one tick stale — pressing play and then a speed button would pause the school again.
|
||||
|
||||
Speed indexes are `0 = ×½`, `1 = ×1`, `2 = ×2`, `3 = ×3`, `4 = ×4`; out-of-range values are
|
||||
ignored rather than fatal. The base rate is `gameMinutesPerRealSecond` (5), so ×1 is five game
|
||||
minutes per real second.
|
||||
|
||||
## Server → client
|
||||
|
||||
### `0x81` Welcome — 4 bytes
|
||||
|
||||
The first frame the client receives.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x81` |
|
||||
| 1 | `u8` | protocol version |
|
||||
| 2 | `u8` | tick rate in Hz |
|
||||
| 3 | `u8` | maximum number of schools |
|
||||
|
||||
### `0x82` Pong — 13 bytes
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x82` |
|
||||
| 1 | `i64` | client clock, echoed unchanged |
|
||||
| 9 | `u32` | server tick when the ping was handled |
|
||||
|
||||
### `0x83` Clock — 15 bytes
|
||||
|
||||
Sent every tick to every connection that has a school open, and only to those.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x83` |
|
||||
| 1 | `i32` | school id |
|
||||
| 5 | `i64` | in-game date, milliseconds since the Unix epoch, read as UTC |
|
||||
| 13 | `u8` | `1` running, `0` paused |
|
||||
| 14 | `u8` | speed index |
|
||||
|
||||
### `0x84` SchoolGone — 5 bytes
|
||||
|
||||
The open school no longer exists — deleted from the menu in another tab, or never existed. The
|
||||
client returns to the menu.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x84` |
|
||||
| 1 | `i32` | school id |
|
||||
|
||||
## Guarantees and limits
|
||||
|
||||
- Frames larger than 8 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.
|
||||
- Clock delivery is lossy under back pressure: each connection buffers 32 frames and drops the
|
||||
oldest, because a stale clock is worthless once a newer one exists.
|
||||
|
||||
## Not in v3 yet
|
||||
|
||||
Saving schools to disk (they live in server memory), authentication, and any game state beyond the
|
||||
calendar — the school's ECS world is created but still empty.
|
||||
|
||||
Reference in New Issue
Block a user