Update wire protocol to version 7 and enhance presence management features
- Bumped the wire protocol version to 7, reflecting significant changes in the communication structure. - Introduced a new presence message type for real-time occupancy updates, including node activity and individual presence states. - Updated the API to include a directory endpoint for fetching short id→name mappings, improving client-side name resolution. - Revised the map snapshot structure to be static, with people and current lessons now handled through the presence stream. - Enhanced client-side handling of presence updates, including UI adjustments to display live occupancy and activity. - Updated documentation to reflect the new protocol features and changes in presence management. - Added tests to validate the new presence functionalities and ensure robust handling of real-time data.
This commit is contained in:
@@ -12,33 +12,33 @@
|
||||
|
||||
## Задачи
|
||||
|
||||
- [ ] Новое сообщение присутствия, ~2 раза в секунду, только по открытой школе: узлы со
|
||||
- [x] Новое сообщение присутствия, ~2 раза в секунду, только по открытой школе: узлы со
|
||||
счётчиками и люди с их узлом и состоянием
|
||||
- [ ] Снимок карты становится **статическим**: люди и текущий урок уезжают в присутствие,
|
||||
- [x] Снимок карты становится **статическим**: люди и текущий урок уезжают в присутствие,
|
||||
структура уходит один раз при открытии. Пересылка на смене слота убирается
|
||||
- [ ] `ProtocolCodec.cs`, `protocol.ts` и `docs/protocol.md` правятся тем же коммитом, версия
|
||||
- [x] `ProtocolCodec.cs`, `protocol.ts` и `docs/protocol.md` правятся тем же коммитом, версия
|
||||
бумпится до 7
|
||||
- [ ] Справочник «id → имя» по HTTP: клиент забирает один раз при открытии школы и перечитывает,
|
||||
- [x] Справочник «id → имя» по HTTP: клиент забирает один раз при открытии школы и перечитывает,
|
||||
увидев незнакомый id. Имена в кадр присутствия не кладутся
|
||||
- [ ] Дерево: число людей рядом с узлом, живое. Пустой узел остаётся без числа
|
||||
- [ ] Панель локации: кто здесь сейчас, списком с именами; урок в комнате берётся из присутствия,
|
||||
- [x] Дерево: число людей рядом с узлом, живое. Пустой узел остаётся без числа
|
||||
- [x] Панель локации: кто здесь сейчас, списком с именами; урок в комнате берётся из присутствия,
|
||||
а не из снимка
|
||||
- [ ] Карточка человека: где он сейчас — узел, «в пути» или «вне школы»
|
||||
- [ ] Присутствие приходит по всей школе, выбранный узел фильтрует клиент
|
||||
- [ ] Кадр часов несёт разрешение на пропуск; клиент это разрешение **не вычисляет**
|
||||
- [ ] Намерение «пропустить» — своё сообщение, как пауза и скорость. Сервер проверяет условия
|
||||
- [x] Карточка человека: где он сейчас — узел, «в пути» или «вне школы»
|
||||
- [x] Присутствие приходит по всей школе, выбранный узел фильтрует клиент
|
||||
- [x] Кадр часов несёт разрешение на пропуск; клиент это разрешение **не вычисляет**
|
||||
- [x] Намерение «пропустить» — своё сообщение, как пауза и скорость. Сервер проверяет условия
|
||||
заново: кадр из браузера не доверенный
|
||||
- [ ] Кнопка рядом с паузой и скоростями: появляется только когда сервер разрешил, подписана
|
||||
- [x] Кнопка рядом с паузой и скоростями: появляется только когда сервер разрешил, подписана
|
||||
датой, к которой перенесёт
|
||||
- [ ] Строки через `t(...)`, обе локали
|
||||
- [x] Строки через `t(...)`, обе локали
|
||||
|
||||
## Тесты, без которых фаза не закрыта
|
||||
|
||||
- [ ] Байтовая раскладка присутствия сходится с обеих сторон
|
||||
- [ ] Круговой прогон сообщения: сервер → клиент → те же значения
|
||||
- [ ] Снимок карты больше не содержит людей и урока
|
||||
- [ ] Кадр часов с разрешением на пропуск читается обеими сторонами одинаково
|
||||
- [ ] Пропуск, присланный в учебное время, сервер отбрасывает, а не исполняет
|
||||
- [x] Байтовая раскладка присутствия сходится с обеих сторон
|
||||
- [x] Круговой прогон сообщения: сервер → клиент → те же значения
|
||||
- [x] Снимок карты больше не содержит людей и урока
|
||||
- [x] Кадр часов с разрешением на пропуск читается обеими сторонами одинаково
|
||||
- [x] Пропуск, присланный в учебное время, сервер отбрасывает, а не исполняет
|
||||
|
||||
## Критерий готовности
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
| Фаза | Статус | Зачем |
|
||||
| --- | --- | --- |
|
||||
| [18. Присутствие и ходьба](18-presence-walking.md) | ✅ | `HSchool.Ai`, место человека, маршруты, приход и уход |
|
||||
| [19. Присутствие на экране](19-presence-screen.md) | ⬜ | Своё сообщение ~2 Гц, числа в дереве, статический снимок карты, пропуск пустого времени |
|
||||
| [19. Присутствие на экране](19-presence-screen.md) | ✅ | Своё сообщение ~2 Гц, числа в дереве, статический снимок карты, пропуск пустого времени |
|
||||
|
||||
**Этап B — поведение.** Появляются нужды, действия и выбор между ними и обязанностью.
|
||||
|
||||
|
||||
+84
-19
@@ -1,13 +1,13 @@
|
||||
# Wire protocol v6
|
||||
# Wire protocol v7
|
||||
|
||||
The client talks to the server two ways:
|
||||
|
||||
- **HTTP/JSON** for the main menu and the in-school people browser — listing, creating and
|
||||
deleting schools, listing mods, loading a catalog for the create editor, reading a school's
|
||||
roster (filtered list + one-person card), staffing, and the timetable. Those are request/response
|
||||
roster (filtered list + one-person card), a short id→name directory, staffing, and the timetable. Those are request/response
|
||||
by nature, so they are plain REST.
|
||||
- **A binary WebSocket at `/ws/game`** for the school calendar (20 Hz) and the map snapshot
|
||||
sent when a school is opened and whenever the current lesson slot changes.
|
||||
- **A binary WebSocket at `/ws/game`** for the school calendar (20 Hz), a static map snapshot
|
||||
sent once when a school is opened, and a presence stream (~2 Hz) of who is where.
|
||||
|
||||
This document covers both. One protocol message per WebSocket frame, no framing header beyond the
|
||||
message id. **All multi-byte numbers are little-endian.**
|
||||
@@ -188,6 +188,25 @@ Applicants live in `saves/{id}.people.json` next to the roster and are **not** i
|
||||
They are not school staff until hired. A parent who is also looking for work keeps the same id
|
||||
in both places.
|
||||
|
||||
### `GET /api/schools/{id}/directory`
|
||||
|
||||
Short id→name directory for the presence stream. Reads the **published roster snapshot**; it
|
||||
does not post to the worker. Unknown `{id}` is `404` `unknown-school`. `?lang=ru|en` is accepted
|
||||
for symmetry with the other people endpoints — names are stored as written and not translated.
|
||||
|
||||
The client fetches this once on OpenSchool and again when a presence frame carries an unknown
|
||||
id. Names do not ride the WebSocket.
|
||||
|
||||
```json
|
||||
{
|
||||
"people": [
|
||||
{ "id": "f0.c0", "fullName": "Иванова Мария Петровна" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Applicants are not in this list.
|
||||
|
||||
### `GET /api/schools/{id}/people/{personId}`
|
||||
|
||||
One person's card. Goes through the school's mailbox because need values live on entities and
|
||||
@@ -415,11 +434,13 @@ frame is obvious at a glance.
|
||||
| `0x04` | C → S | CloseSchool |
|
||||
| `0x05` | C → S | SetRunning |
|
||||
| `0x06` | C → S | SetSpeed |
|
||||
| `0x07` | C → S | SkipEmpty |
|
||||
| `0x81` | S → C | Welcome |
|
||||
| `0x82` | S → C | Pong |
|
||||
| `0x83` | S → C | Clock |
|
||||
| `0x84` | S → C | SchoolGone |
|
||||
| `0x85` | S → C | MapSnapshot |
|
||||
| `0x86` | S → C | Presence |
|
||||
|
||||
## Client → server
|
||||
|
||||
@@ -443,8 +464,9 @@ The locale byte is the same language the catalog HTTP API takes as `?lang=`.
|
||||
|
||||
### `0x03` OpenSchool — 5 bytes
|
||||
|
||||
Starts watching a school: a map snapshot in the Hello locale arrives once, then clock frames.
|
||||
It does not start the calendar — every school runs on its own from the moment it is created.
|
||||
Starts watching a school: a map snapshot in the Hello locale arrives once, then clock frames
|
||||
and presence frames. It does not start the calendar — every school runs on its own from the
|
||||
moment it is created.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
@@ -478,6 +500,15 @@ Speed indexes are `0 = ×½`, `1 = ×1`, `2 = ×2`, `3 = ×3`, `4 = ×4`; out-of
|
||||
ignored rather than fatal. The base rate is `gameMinutesPerRealSecond` (5), so ×1 is five game
|
||||
minutes per real second.
|
||||
|
||||
### `0x07` SkipEmpty — 1 byte
|
||||
|
||||
Jump empty nights, weekends and holidays. The server re-checks both conditions (campus empty
|
||||
**and** outside the day-frame work window) — a frame from the browser is untrusted. Ignored
|
||||
when the skip is not legal; the calendar does not move.
|
||||
|
||||
Running, speed and skip are **separate messages on purpose**. A button that also resent a
|
||||
neighbouring field would clobber it with a stale client copy.
|
||||
|
||||
## Server → client
|
||||
|
||||
### `0x81` Welcome — 4 bytes
|
||||
@@ -499,9 +530,11 @@ The first frame the client receives.
|
||||
| 1 | `i64` | client clock, echoed unchanged |
|
||||
| 9 | `u32` | server tick when the ping was handled |
|
||||
|
||||
### `0x83` Clock — 15 bytes
|
||||
### `0x83` Clock — 24 bytes
|
||||
|
||||
Sent every tick to every connection that has a school open, and only to those.
|
||||
`skipAllowed` is the server's verdict; the client must not recompute it.
|
||||
`skipTargetUnixMs` is 0 when skip is refused.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
@@ -510,6 +543,8 @@ Sent every tick to every connection that has a school open, and only to those.
|
||||
| 5 | `i64` | in-game date, milliseconds since the Unix epoch, read as UTC |
|
||||
| 13 | `u8` | `1` running, `0` paused |
|
||||
| 14 | `u8` | speed index |
|
||||
| 15 | `u8` | `1` skip allowed, `0` refused |
|
||||
| 16 | `i64` | skip target, milliseconds since the Unix epoch, UTC; `0` if refused |
|
||||
|
||||
### `0x84` SchoolGone — 5 bytes
|
||||
|
||||
@@ -523,10 +558,8 @@ client returns to the menu.
|
||||
|
||||
### `0x85` MapSnapshot — variable
|
||||
|
||||
Sent when a school is opened, on reconnect OpenSchool, and again when the current lesson slot
|
||||
changes (a bell, a weekend, a holiday — not every tick, not on tree clicks). Labels are in the
|
||||
Hello locale. Occupancy is computed from the timetable and the clock; the client must not
|
||||
derive it.
|
||||
Sent when a school is opened, and on reconnect OpenSchool. Structure only — people and the
|
||||
current lesson ride the presence stream. Labels are in the Hello locale.
|
||||
|
||||
Strings are `u16` byte length + UTF-8. Empty string is a zero length.
|
||||
|
||||
@@ -548,24 +581,56 @@ Each node:
|
||||
| `u16` | pupil slots — how many pupils can take a lesson here. Summed from things on the server. |
|
||||
| `u8` | item count, then that many records of: string name + `u8` count |
|
||||
| `u8` | position count, then that many strings |
|
||||
| `u8` | `1` if a lesson is in this room right now, then subject label + class label strings; `0` if free |
|
||||
| `u8` | character count, then that many name strings (teacher and pupils of the lesson) |
|
||||
|
||||
Item `count` is how many of that thing stand in the room (`Парта ×16` is one record, not sixteen). The client must not recompute pupil slots from items. The location panel draws activity and characters from the last two fields.
|
||||
Item `count` is how many of that thing stand in the room (`Парта ×16` is one record, not sixteen). The client must not recompute pupil slots from items.
|
||||
|
||||
### `0x86` Presence — variable
|
||||
|
||||
Live occupancy of an open school, about twice a second, and also once on OpenSchool and after
|
||||
a successful empty-time skip. Covers the whole map; the client filters to the selected tree
|
||||
node. Off-campus people are omitted — a missing id means they are away. Walking people occupy
|
||||
their **current** node. Names are resolved over HTTP, not on this frame.
|
||||
|
||||
Nodes listed are occupied **or** currently taught (count may be 0). Sorted by id. Labels are
|
||||
in the Hello locale, encoded per client.
|
||||
|
||||
| Offset | Type | Field |
|
||||
| --- | --- | --- |
|
||||
| 0 | `u8` | `0x86` |
|
||||
| 1 | `i32` | school id |
|
||||
| 5 | `u16` | node count |
|
||||
| 7… | | nodes, then `u16` person count, then people |
|
||||
|
||||
Each node:
|
||||
|
||||
| Type | Field |
|
||||
| --- | --- |
|
||||
| string | instance id |
|
||||
| `u16` | headcount in this node |
|
||||
| `u8` | `1` if a lesson is in this room right now, then subject label + class label strings; `0` if free |
|
||||
|
||||
Each person:
|
||||
|
||||
| Type | Field |
|
||||
| --- | --- |
|
||||
| string | person id |
|
||||
| string | node id they occupy |
|
||||
| `u8` | `1` here, `2` walking |
|
||||
|
||||
## Guarantees and limits
|
||||
|
||||
- **Inbound** frames larger than 8 KiB are refused with close status `1009 MessageTooBig`. That
|
||||
limit is about what the server reads; it does not bound what the server sends. A `MapSnapshot`
|
||||
of a large school legitimately exceeds it, and the server sizes that frame from the message.
|
||||
or `Presence` frame of a large school legitimately exceeds it, and the server sizes those frames
|
||||
from the message.
|
||||
- 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 clock frames and drops
|
||||
the oldest, because a stale clock is worthless once a newer one exists.
|
||||
- The map snapshot uses a separate reliable queue so ticks cannot crowd it out.
|
||||
- The map snapshot and presence use a separate reliable queue so ticks cannot crowd them out.
|
||||
|
||||
## Not in v6 yet
|
||||
## Not in v7 yet
|
||||
|
||||
Authentication, Sit orders, an event log, walking, and `OpenLocation` on the server — the tree is
|
||||
filtered on the client from the snapshot.
|
||||
Authentication, Sit orders, an event log, walk animation, and `OpenLocation` on the server —
|
||||
the tree and the location panel are filtered on the client from the snapshot plus presence.
|
||||
|
||||
Reference in New Issue
Block a user