Update wire protocol to version 6 and enhance timetable functionality
ci / server (push) Failing after 3m53s
ci / client (push) Successful in 15s

- Bumped the wire protocol version to 6, reflecting changes in the communication structure.
- Expanded the timetable API with new endpoints for fetching and managing lesson schedules, including `GET /api/schools/{id}/timetable` and `POST /api/schools/{id}/timetable/pin`.
- Updated the protocol documentation to include detailed descriptions of the new timetable features and message structures.
- Enhanced the client-side implementation to support the new timetable functionalities, including lesson pinning and unpinning.
- Revised server-side logic to handle timetable operations and ensure proper integration with existing school management features.
- Added tests to validate the new timetable functionalities and ensure robustness in handling lesson data.
This commit is contained in:
Leonid Pershin
2026-08-19 10:05:05 +03:00
parent 2011d12b1d
commit cad3068bac
30 changed files with 1621 additions and 43 deletions
+8 -8
View File
@@ -11,17 +11,17 @@
## Задачи
- [ ] Расписание пересобирается при найме, увольнении и изменении назначений — **не по тику**
- [ ] Хранится в сейве рядом с составом; пишется при изменении, не по таймеру. Закреплённые
- [x] Расписание пересобирается при найме, увольнении и изменении назначений — **не по тику**
- [x] Хранится в сейве рядом с составом; пишется при изменении, не по таймеру. Закреплённые
игроком уроки сохраняются вместе с ним
- [ ] Воркер публикует расписание снимком, как ростер
- [ ] «Кто где сейчас» **вычисляется** из расписания и часов, а не хранится вторым состоянием
- [ ] Команда закрепления и снятия закрепления урока — через мейлбокс воркера
- [ ] `GET /api/schools/{id}/timetable` — расписание класса и расписание человека; читает
- [x] Воркер публикует расписание снимком, как ростер
- [x] «Кто где сейчас» **вычисляется** из расписания и часов, а не хранится вторым состоянием
- [x] Команда закрепления и снятия закрепления урока — через мейлбокс воркера
- [x] `GET /api/schools/{id}/timetable` — расписание класса и расписание человека; читает
опубликованный снимок
- [ ] Снимок карты отдаёт по узлу, что там сейчас идёт и у кого; протокол и `docs/protocol.md`
- [x] Снимок карты отдаёт по узлу, что там сейчас идёт и у кого; протокол и `docs/protocol.md`
правятся тем же коммитом, версия бумпится
- [ ] Панель локации получает данные для «Сейчас» и «Персонажей»
- [x] Панель локации получает данные для «Сейчас» и «Персонажей»
## Критерий готовности
+1 -1
View File
@@ -66,5 +66,5 @@
| --- | --- | --- |
| [14. Каркас дня и каникулы](14-school-calendar.md) | ✅ | Звонки, длина недели, каникулы |
| [15. Планировщик](15-timetable-planner.md) | ✅ | Раскладка часов по слотам, четыре запрета |
| [16. Расписание в школе](16-timetable-in-school.md) | | Сейв, снимок, «кто где сейчас» |
| [16. Расписание в школе](16-timetable-in-school.md) | | Сейв, снимок, «кто где сейчас» |
| [17. Расписание на экране](17-timetable-screen.md) | ⬜ | Скобки в дереве, сетка класса, расписание учителя |
+72 -13
View File
@@ -1,13 +1,13 @@
# Wire protocol v5
# Wire protocol v6
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, and reading a school's
roster (filtered list + one-person card). 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 one-shot map
snapshot sent when a school is opened.
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
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.
This document covers both. One protocol message per WebSocket frame, no framing header beyond the
message id. **All multi-byte numbers are little-endian.**
@@ -331,6 +331,62 @@ Body: `{ "subject": "Mathematics" }`. Teachers only. Same success payload as GET
Removes one assignment. Payroll drops when the subject was not the only one. Unknown
assignment is `404` `unknown-assignment`.
### `GET /api/schools/{id}/timetable`
The published lesson table and uncovered hours. Optional `?classId=` or `?personId=` filter
the lessons. `?lang=ru|en` labels subjects. Reads the snapshot — it does not post to the
worker. Unknown `{id}` is `404` `unknown-school`.
`day` is 0 = Monday. `period` is the 1-based lesson number from the day frame.
```json
{
"weekDays": 5,
"lessonCount": 7,
"lessons": [
{
"classId": "c5A",
"classYear": 5,
"classLetter": "A",
"subject": "Mathematics",
"subjectLabel": "Математика",
"teacherId": "f3.p1",
"teacherName": "Иванова Ольга Михайловна",
"roomId": "classroom-204",
"day": 1,
"period": 3,
"locked": false
}
],
"uncovered": [
{
"classId": "c5A",
"classYear": 5,
"classLetter": "A",
"subject": "Informatics",
"subjectLabel": "Информатика",
"hours": 1
}
]
}
```
### `POST /api/schools/{id}/timetable/pin`
Body: `{ "classId", "subject", "roomId", "day", "period" }`. Pins a locked lesson there and
rebuilds the rest around it. Same success payload as GET timetable.
| Status | `code` | When |
| --- | --- | --- |
| `400` | `unknown-class` / `unknown-subject` / `unknown-room` | Not in this school. |
| `409` | `no-teacher` | Nobody is assigned that subject. |
| `409` | `pin-rejected` | The slot or room violates the four constraints. |
### `DELETE /api/schools/{id}/timetable/pin`
Query: `classId`, `subject`, `day`, `period`. Drops that lock and rebuilds. Unknown lock is
`404` `unknown-lesson`.
## WebSocket message ids
Client-to-server ids live in `0x000x7F`, server-to-client ids in `0x800xFF`, so a misrouted
@@ -452,9 +508,10 @@ client returns to the menu.
### `0x85` MapSnapshot — variable
Sent once when a school is opened (and again on reconnect OpenSchool). Not every tick, not on
tree clicks. Labels are in the Hello locale. People and in-place activities are omitted — the
client keeps those sections empty.
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.
Strings are `u16` byte length + UTF-8. Empty string is a zero length.
@@ -476,8 +533,10 @@ 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.
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.
## Guarantees and limits
@@ -491,7 +550,7 @@ Item `count` is how many of that thing stand in the room (`Парта ×16` is o
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.
## Not in v5 yet
## Not in v6 yet
Authentication, Sit orders, an event log, and `OpenLocation` on the server — the tree is filtered
on the client from the snapshot. The school's ECS world is created but still empty.
Authentication, Sit orders, an event log, walking, and `OpenLocation` on the server — the tree is
filtered on the client from the snapshot.