Contract stability
Transport uses Colyseus over WebSocket and the
my_room room. The Godot client never sends an
authoritative position. Changing message names, field types, or the
state schema is a compatibility change and must be coordinated with
the client.
Connection & join options
Authentication requires the following options:
{
"nickname": "Player One"
}
When joining, the client may additionally provide a visual variant:
{
"nickname": "Player One",
"playerColor": "chicken_2"
}
Allowed player colors
| Value | Variant |
|---|---|
chicken_1 |
White / default |
chicken_2 |
Black |
chicken_3 |
Dark brown |
chicken_4 |
Light brown |
Possible authentication rejection codes
Client → server
move
{
"seq": 42,
"clientTime": 1789051200123,
"input": {
"left": false,
"right": true,
"up": false,
"down": false
}
}
Validation rules:
-
seq: a safe, non-negative integer; begin with1. clientTime: a non-negative number.- All four direction flags are required and must be booleans.
-
Additional fields at either the message level or within
inputare rejected. - A message that exceeds the rate limit is ignored.
- The server retains at most 60 recent inputs per player.
clientTime can be used by the client for diagnostics
or prediction, but the server does not use it to calculate
position.
chat_send
{
"text": "Hello!"
}
The payload must contain a string. For normalization and length rules, see the Gameplay guide.
Server → client
game_config
Sent after joining:
{
"playerCollider": {
"type": "circle",
"radius": 5
},
"playerGrowth": {
"initialMass": 5,
"colliderScaleExponent": 0.5
},
"seeds": {
"radius": 2
}
}
For a rectangular collider, playerCollider contains
type, width, and height.
The message includes neither server secrets nor player position.
tick_sync
The first message after joining may contain only the tick:
{
"serverTick": 12
}
Later broadcasts also include the server time:
{
"serverTick": 72,
"serverTime": 1789051200123
}
chat_message
{
"id": "f1bce155-bf16-49c8-8e6d-a75ea14a5bf4",
"playerId": "colyseus-session-id",
"nickname": "Player One",
"text": "Hello!",
"sentAt": 1789051200123
}
chat_error
{
"code": "invalid_message"
}
code is either invalid_message or
rate_limited.
player_eaten
Sent separately to the predator and the prey:
{
"predatorSessionId": "predator-id",
"preySessionId": "prey-id",
"gainedMass": 8
}
gainedMass is the mass actually transferred from the
prey.
Synchronized schema state
The root state contains two maps:
players: MapSchema<Player>
seeds: MapSchema<Seed>
Player
| Field | Type | Meaning |
|---|---|---|
nickname |
string |
Normalized nickname |
x, y |
number |
Authoritative center of the collider |
left/right/up/down |
boolean |
Last processed input |
mass |
number |
Current mass |
lastProcessedSeq |
number |
Input ACK used for reconciliation |
playerColor |
string |
Visual variant |
Seed
Seed contains x and y. Keys
follow the seed-N format within each room.
Recommendations for the Godot client
- Send a full snapshot of all four direction flags, not just the key that changed.
-
Increase
seqmonotonically, starting at1. -
Treat
x,y, mass, and respawn in the schema state as authoritative. -
Reconcile local prediction against
lastProcessedSeqand server state. -
Do not add
xoryto amovepayload: the strict schema rejects it. - Handle Colyseus reconnection and rebuild local state after reconnecting.
- Do not assume that every input will be accepted; the rate limiter may drop it.
Adding messages without breaking the client
-
Add the name to the appropriate enum in
MessageTypes.ts. - Add a Zod schema and payload type.
-
Create a handler and register it in
RoomRuntimeBuilder. - Add tests for valid, invalid, and excessive payloads.
- Only then update the Godot client.
- Do not rename existing messages or fields without versioning the protocol.