Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Messages

A message occupies one deciphered channel payload. The payload opens with the header the message’s family carries, and the body begins where that header ends.

schema/messages holds one file per message identity, and each file records one or more revisions. A revision carries the value identifying the message, the channel it travels on, its direction, how the transport delivers it, and the fields of its body.

Families

Every channel carries one family of messages, and schema/channels.json says which. A family fixes two things: the numbering its command values are drawn from, and the header that precedes every body on it. schema/families.json records both. Two channels of one family share a numbering, so a value identifies at most one message across both, and the same value on channels of different families is two unrelated messages.

A family whose header carries no command field has one message each way on each channel it rides, read from the payload’s first byte; its revisions number that message 0.

The family, not the channel, selects the header.

A revision names the channel it travels on, and names any others in alsoOn. Those channels carry the same family, so the command value means the same message on each, and the body is byte for byte the one the revision records. What changes between them is delivery: each entry states how the transport carries the message there. A server MAY send such a message on any channel the revision names, and what it gets is the delivery stated for that channel. A client reads it the same way on every one of them, so a server MUST NOT vary the body with the channel it chooses.

The command

A revision records the value that identifies it, and the family’s header says where that value sits on the wire. That value is not a field of the message: it belongs to the header, along with anything else the family carries before a body begins.

A family can keep some values of the leading byte for framing rather than for messages, and schema/families.json lists which. Where a family names an escape, that value means the real identifier is wider than a byte and travels elsewhere in the header, which is how a family numbers more messages than a byte holds. A revision records the identifier either way, and never the escape.

Directions

A revision states whether it travels clientToServer, serverToClient, or bidirectional. Direction is part of what identifies a message, so one value serves two messages travelling opposite ways within a family. A bidirectional revision claims that value both ways.

A container of several messages

In 1.0.0.25–1.0.0.60

A client unwraps no container: a payload whose command is 255 does nothing, on any channel. A server MUST NOT send one, because nothing inside it reaches the client.

In 1.0.0.96–1.0.0.129

Where the game family keeps 255 for framing, that value introduces a container rather than a message: a run of several messages gathered into one payload, which a reading end unwraps and reads one by one as though each had arrived on its own. A container carries no header, so nothing in schema/messages describes one, and a reading end MUST NOT read the value that introduces it as a command.

A container is the byte 255, then a count of entries in one byte, then the entries one after another. The first entry is a length in one byte, the command in one byte, the object identifier in four bytes, and then a body whose size is the length less five. Every later entry opens with a byte of flags and size:

  • bit 0 clear: a command byte follows, and this entry carries it; bit 0 set: this entry carries the command the entry before it carried;
  • bit 1 clear: a four byte object identifier follows; bit 1 set: one signed byte follows, and this entry’s object identifier is the one before it plus that byte;
  • bits 2 to 7: the size of the body; where they read 63, the size is the next byte instead.

The body follows. Each entry, given the command and object identifier it carries as its header, is a message read as though it had arrived on its own. Where the family’s escape is 254, an entry whose command is 254 carries the wider identifier in the first two bytes of its body, so that, with the command and object identifier before it, it reads as the extended form of the header does.

Where the game family keeps 255, a server MAY send a container on the events channel. A client sends none.

In 1.0.0.126–1.0.0.129

A client unwraps a container on any channel.

Reading a body

A body begins where its family’s header ends. Fields occupy it in the order the revision lists them, each beginning where the one before it ended. A revision never restates a field its family’s header already carries, and never takes a name from it. Nothing is aligned and nothing is padded between fields. A field’s size is fixed by its type, given as a literal, carried by an earlier field, or one of the two words below.

Where a size or a count names an earlier field, it names one that has already been read and one holding an unsigned integer. It names either a whole field or one run of bits inside a field, and a run is written as the field name, a full stop, and the run name. remaining and terminated are reserved words rather than names, so no field carries either.

A field of a body names only fields of that body. A family’s header is read first, but it belongs to every message of the family rather than to this one, so nothing in a body is sized or governed by it.

What counts as an earlier field depends on what encloses the field doing the naming. An item of an array reaches the fields of the body around it. The fields of a struct reach only each other, because a struct is used wherever it occurs, so it cannot depend on what surrounds any one use.

remaining means every byte from where the field begins to the end of what encloses it: the message body, or the byte length of the struct or array the field sits inside. A field measured that way is the last one its enclosure defines, because nothing inside that enclosure can follow it.

An enclosure that states a byte length is itself an ordinary field, and fields follow it as usual. An enclosure that states none ends where the body ends, so it is the last field too, and so is any struct whose own last field is one of these.

A body ends where its last field ends, with one exception: a client accepts a Registered carrying bytes past its record, and takes no meaning from them. A sender MUST write the fields a revision lists and nothing after them.

Byte order

A revision of schema/protocol.json states the byte order that covers a version range, and every multi byte field in that range is read that way. A field that differs states its own order, which overrides the range.

The types

Integers. u8 through u64 and i8 through i64, in two’s complement. f32 and f64 are IEEE 754 binary floating point.

Bytes. A run of bytes with no interpretation. Its size is fixed, named by an earlier field, or remaining.

Strings. A run of bytes and an encoding, either utf8 or ascii. A size is a literal or the name of an earlier field. A string given one occupies that many bytes whatever its content, and the zero bytes at its end are padding rather than content, so they are dropped. A zero anywhere else belongs to the text and stays. A string sized terminated ends at the first zero byte, that byte belongs to the field, and it is the only one dropped. A string sized remaining carries every byte to the end of its enclosure, zeros included. A size counts bytes rather than characters.

Arrays. A run of one repeated shape. An array states either a count of items or a size in bytes, and never both. A count is a literal, the name of an earlier field, or remaining, which repeats to the end of the enclosure. A size in bytes repeats until those bytes are consumed, which is how a run whose length travels as a byte count is recorded.

Structs. A named group of fields, defined in schema/types and used wherever it occurs. A struct has revisions as a message does, and a message revision that holds one lies wholly within one of the struct’s revisions, which gives the layout it holds there. A struct occupies exactly the bytes its own fields occupy. Where its length travels on the wire instead, the field states a size in bytes, as a literal or as the name of an earlier field. A sending end MUST write a struct’s fields to fill that size exactly.

Bits. A field of one or more bytes divided into named runs, whose widths total exactly the bits those bytes hold. No run is wider than 32 bits. The bytes form one little endian word whatever the range’s byte order, or a big endian one where the field says so. Runs are listed from the least significant bit upward, and each reads as an unsigned integer of its own width.

Bit arrays. A run of the same group of bit runs, repeated a stated number of times. The groups follow one another from the low bits of the first byte upward, crossing byte boundaries without padding, and the field occupies as many whole bytes as the groups need. The bits above the last group are clear. A count is written as an array’s is.

Records. A group of fields named together and read one after another, with nothing between them and nothing around them. A record occupies exactly what its fields occupy. Unlike a struct it is written where it is used rather than defined in schema/types, and its fields reach the fields around it, so a record is a way of naming part of a message rather than a shape reused elsewhere.

What a field names

A size, a count, or a rule about whether a field is present names an earlier field. Five forms are written:

  • a name on its own, which is a field of the same enclosure;
  • a name, a full stop, and a run inside it, where that field is bits;
  • a name, square brackets, and a name inside them, which is the item of an earlier array standing at the same position as the item doing the naming, so that two arrays run together;
  • either of the first two inside {"field": …, "minus": N}, which is that field’s value less the literal N, and N is never larger than the value;
  • either of the first two inside {"field": …, "bitsSet": true}, which is how many bits of that field’s value are one.

Nothing else is computed.

Presence

A field states a presence rule when it occupies bytes only under a condition, and a field whose condition does not hold occupies no bytes at all. The rule names an earlier field or bit run holding an integer, and states a value it carries, or a set of values, any one of which satisfies it. Without one, any value other than zero satisfies the rule. A field its own rule left out holds no value, so a rule naming that field does not hold either.

A rule can instead name remaining, which no field is called. A field under that rule is present where the payload has bytes left at that point and absent where it has ended, so a message whose sender stops early reads as one whose later fields are absent. A sender that stops early MUST stop where a field under such a rule begins.

A rule can mark one of its sides unseen; that side’s layout is the one the field states.

A field that travels enciphered

A payload is enciphered as a whole once a connection has a cipher state, and a message says nothing about that. Before a cipher state exists, a payload can carry one field enciphered under the key both ends hold, and the field says so.

Such a field occupies whole cipher blocks, because a field filling part of one would travel half in the clear. Its recorded value is what the bytes mean once deciphered, so a reader deciphers the field alone and leaves the payload around it untouched.

Enumerated values

Where an integer field, or a run of bits inside one, takes a fixed set of values, the field lists what they mean, keyed by the value on the wire. Each entry names the value, in the same form a field name takes, and no two entries of one field share a name. A key fits the width and sign of what it names. A value with no entry has no meaning this specification states.

Revisions

A revision covers a range of client versions, from one version inclusive to another exclusive. Every revision states where it stops. Ranges of one message never overlap. A version no revision of a message covers has no such message: a client there neither sends nor takes it, and what a chapter says of it does not apply.

A revision covers no version that schema/protocol.json, schema/channels.json and schema/families.json do not all describe, because a range none of them reaches has no header, no byte order, and no channel to travel on.

Names and notes

A field name begins with a lower case letter and carries letters and digits after it. A message identity and a struct identity begin with a capital. A file is named for the identity it defines, and a message’s vectors sit in one directory named for that identity, with each capital starting a new part, lower case and joined by hyphens.

A field name is unique among the fields beside it, and an item of an array never takes the name of a field enclosing it.

Anything in schema/ carries a note wherever a fact needs words: a revision, a field, a run of bits, a value. A note states what the wire carries and what each end does. It carries no requirement keyword and says nothing changed.

How a message is delivered

A revision states whether the transport carries it reliably, unreliably, or unsequenced, which is what the message needs of the transport: a message another supersedes can be lost, and one that carries an account of what happened cannot. A server MUST send a message with the delivery its revision states.

Quick chat

Vectors

Every revision carries at least one conformance vector. A message vector opens with its family’s header and is read to the end. conformance/ says how a vector is written and how to run one.

Family headers

A field present when remaining is set is present exactly where the payload has bytes left at that point.

In 1.0.0.25–1.0.0.78

The game family:

FieldTypeWhat it is
commandu8255 introduces a container of several messages rather than a message of its own, which Messages describes.
paddingbytes, 3A client reads nothing here, and most of its own messages leave it unwritten. A server writes zero.
netIdu32A network object the message concerns. Which object, and whether it is the origin or the subject, is settled by each message rather than here, and some messages carry a value naming none.

The session family:

FieldTypeWhat it is
commandu8

The registration family:

FieldTypeWhat it is
commandu8

The chat family:

No header.

In 1.0.0.79–1.0.0.129

The game family:

FieldTypeWhat it is
commandu8255 introduces a container of several messages rather than a message of its own, which Messages describes.
netIdu32A network object the message concerns. Which object, and whether it is the origin or the subject, is settled by each message rather than here, and some messages carry a value naming none.

The session family:

FieldTypeWhat it is
commandu8

The registration family:

FieldTypeWhat it is
commandu8

The chat family:

No header.