Skip to content

2. UDP packet header

Every UDP datagram, in both directions, MUST start with this 18-byte header. The payload of its type follows immediately. Encoding rules are in section 1.

2.1 Layout

0               4   5   6       8       10              14              18
+---------------+---+---+-------+-------+---------------+---------------+
|     magic     |ver|typ|  seq  |  ack  |   ack_bits    |    session    |
+---------------+---+---+-------+-------+---------------+---------------+
Offset Size Field Type Content
0 4 magic u32 0x50595452, sent as the bytes 52 54 59 50 ("RTYP" in ASCII)
4 1 version u8 1
5 1 type u8 Packet type (2.3) in bits 0 to 6; bit 7 set when the payload is compressed (2.6)
6 2 sequence u16 Number of this datagram (2.2)
8 2 ack u16 Most recent sequence received from the peer (2.2)
10 4 ack_bits u32 The 32 sequences before ack that were received (2.2)
14 4 session u32 Session token (2.2)

A datagram therefore carries at most 1200 − 18 = 1182 bytes of payload.

2.2 Fields

  • magic identifies the protocol. A receiver MUST drop a datagram whose first four bytes are not 52 54 59 50.
  • version is the protocol version, 1 for this document. A receiver MUST drop a datagram of another version.
  • type says what the payload is (2.3) in its 7 low bits. Its high bit (0x80) says that the payload is compressed (2.6): a compressed Snapshot has the type byte 0x84. A receiver MUST drop a datagram of an unknown type, with or without that bit.
  • sequence numbers the datagrams of one sender in a session: the sender MUST add 1 for each datagram it sends, whatever its type, going from 65535 back to 0. Because of this wrap, sequences MUST be compared as in RFC 1982 (serial number arithmetic): sequence a is more recent than b when a is 1 to 32767 steps ahead of b, counting modulo 65536, that is when a > b and a − b < 32768, or a < b and b − a > 32768. So 0 is more recent than 65535, and 10 than 40000. Two sequences exactly 32768 apart are not ordered: a receiver MUST NOT treat either as the more recent one.
  • ack is the most recent sequence received from the peer, and bit n of ack_bits (bit 0 being the least significant) is set when sequence ack − 1 − n was received too. Every datagram thus acknowledges the last 33 datagrams of the peer, without dedicated acknowledgement packets. The rules that use them, including their values before anything was received, are in section 5.
  • session is the token the server issued at login (section 3); 0 is never issued. The server MUST drop a datagram whose token does not match the session bound to its source endpoint, except a Connect datagram, which binds them (section 3.4).

2.3 Packet types

Value Name Direction Purpose
1 Connect client → server Binds the client's UDP endpoint to its session
2 Accept server → client Confirms the binding: gameplay traffic may start
3 Input client → server The player's input state for one tick
4 Snapshot server → client The state of the world
5 Event both Gameplay events that must arrive (spawns, deaths...), resent until acknowledged
6 Heartbeat both Keeps the session alive and carries acknowledgements when nothing else is sent
7 Disconnect both The sender leaves the session

Values 0 and 8 to 127 are reserved, with or without the compressed bit. The payloads are specified in section 3.4 (Connect, Accept), section 4 (Input, Snapshot, Event) and section 5.5 (Heartbeat, Disconnect), and the reliability of Event in section 5.

2.4 Processing

A receiver MUST check, in this order:

  1. that the datagram holds at least 18 bytes;
  2. the magic;
  3. the version;
  4. the type.

At the first failed check, the datagram MUST be dropped without reading further. A server SHOULD count dropped datagrams per source, to detect floods and broken clients. The checks that follow the header are in section 6.3.

2.5 Example

A Snapshot with sequence 0x1234 (4660), acknowledging the peer's sequence 0xABCD (43981) and the ones at distance 1 and 32 before it, in session 0xDEADBEEF:

52 54 59 50  01  04  34 12  CD AB  01 00 00 80  EF BE AD DE
magic        ver typ seq    ack    ack_bits     session

The Aegis2D implementation is net::PacketHeader, net::write_header() and net::read_header() (aegis2d/net/packet.hpp); its unit tests check these exact bytes. Sequences are compared with net::sequence_greater_than() and net::sequence_less_than() (aegis2d/net/sequence.hpp).

2.6 Compressed payloads

A sender MAY compress the payload of a datagram with LZ4 (its block format) and set bit 7 of type. The compressed payload is:

Field Type Content
size u16 Size of the original payload, 1 to 1182
block bytes The original payload as one LZ4 block, up to the end of the datagram
  • A sender SHOULD compress only payloads that come out smaller, and only from a size where that is likely: Aegis2D tries from 128 bytes (net::default_compression_threshold) and sends the payload as it is when it does not shrink. A peer that never compresses thus still reads every datagram of one that does.
  • Compression is optional per datagram: the same session MAY mix both. The original payload follows the rules of its type, as if it had been sent as it is; the acknowledgements and the sequence of the header do not change.
  • A receiver MUST decompress the payload before checking it, and MUST drop the datagram, without acknowledging it, when size is 0 or above 1182, when the block is invalid, or when it does not give exactly size bytes (section 6.3). The LZ4 decoder used MUST never read or write past its buffers, whatever the block (LZ4_decompress_safe()).

For example, the Event datagram of the fuzz corpus tests/fuzz/corpus/packet/event-compressed carries one event of 300 bytes that repeat: its 305-byte payload is sent as 135 bytes (size then a 133-byte block), with the type byte 0x85, so the datagram takes 153 bytes instead of 323.

The Aegis2D implementation is aegis2d/net/compression.hpp: compress_payload(), decompress_payload(), write_datagram(), which compresses from a threshold when that pays, and read_payload(), which decompresses when the header says so.