Files
h-school/docs/protocol.md
T

9.0 KiB
Raw Blame History

Wire protocol v4

The client talks to the server two ways:

  • HTTP/JSON for the main menu — listing, creating and deleting schools, listing mods and loading a catalog for the create editor. 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.

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
Client codec 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.

{
  "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.

Optional ?lang=en draws from the English word list (Northern Academy); any other value, or none, stays Russian. The client sends the active UI language. Names the player types are not translated — they are saved as written.

GET /api/mods

Folders under the server's mods/ directory. core is always first and required: true; other packs can be switched off in the create dialog.

{ "mods": [{ "id": "core", "required": true }] }

GET /api/catalog?lang=ru|en&mods=addon1,addon2

Placeable (non-abstract) types plus labels in lang, and the last-wins maps/default.jsonc for core plus the listed extras. The server always prepends core. mods is a comma-separated list of extra pack ids; omit it for vanilla. Unknown extras return 400 unknown-mod.

lang is the same value Hello carries — not Accept-Language. Anything other than en is Russian.

POST /api/schools

Body:

{
  "name": "Гимназия №14",
  "startDate": "2012-04-03T06:00:00Z",
  "modIds": [],
  "map": null
}

modIds are extras; the server always prepends core. Omit map (or send null) to use that pack set's default layout. A supplied map is validated as a connected yard-and-rooms graph.

Status Meaning
201 Created; body is the school.
400 invalid-name Blank, or longer than 40 characters.
400 invalid-start-date Outside 19002999.
400 invalid-map Missing yard, no rooms, unknown def, or a disconnected graph.
400 unknown-mod An extra pack id is missing under mods/.
400 invalid-catalog The selected packs could not be loaded.
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 0x000x7F, server-to-client ids in 0x800xFF, 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
0x85 S → C MapSnapshot

Client → server

0x01 Hello — 3 bytes

Must be the first frame; the server drops the connection if it does not arrive within 5 seconds. The locale byte is the same language the catalog HTTP API takes as ?lang=.

Offset Type Field
0 u8 0x01
1 u8 protocol version
2 u8 locale: 0 Russian, 1 English; any other value is treated as Russian

0x02 Ping — 9 bytes

Offset Type Field
0 u8 0x02
1 i64 client clock in milliseconds

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.

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

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.

Strings are u16 byte length + UTF-8. Empty string is a zero length.

Offset Type Field
0 u8 0x85
1 i32 school id
5 u16 node count
7… nodes

Each node:

Type Field
u8 kind: 0 territory, 1 building, 2 floor, 3 room
string instance id
string parent id (empty for the yard)
string display name
u8 item count, then that many strings
u8 position count, then that many strings

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.
  • 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.

Not in v4 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.