This is the binary language of Moisture Vaporators, only intended to be read by protocol droids. :-D If you're interested in the administration REST API, see the TirNanoG Admin Protocol.
All integers are little-endian, and all packets start with a 2 bytes long header:
Offset | Length | Description |
---|---|---|
0 | 2 | Packet type (bit 0 - 4) and length (bit 5 - 15) |
Type can be 0 - 31, length 4 - 2043 (but maximum length is usually smaller, limited by interface MTU to 1500 minus the IP protocol headers, so around 1400 in best case. In fact, the IPv6 RFC guarantees only 1280). Actually the underlying NetQ library adds another 6 more bytes to the datagrams before this header (but that's hidden from the app).
The communication starts on a strongly encrypted, SSL/TLS TCP channel.
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 0, size 8 |
2 | 2 | Supported protocol version (0 as of writing) |
4 | 4 | TirNanoG Player magic 'TNGP' |
Server to client
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 0, size 64 |
2 | 2 | Supported protocol version (0 as of writing) |
4 | 4 | TirNanoG Server magic 'TNGS' |
8 | 8 | supported protocol feature flags, must be zero |
16 | 16 | game id, padded with zeros |
32 | 1 | asset format revision (0 for now) |
33 | 1 | game type (0-top-down, 1-isometric, 2-hex...etc.) |
34 | 1 | tile width (bits 8-9 in game type byte bits 4-5) |
35 | 1 | tile height (bits 8-9 in game type byte bits 6-7) |
36 | 1 | map size in power of two |
37 | 1 | atlas size in power of two |
38 | 1 | audio samples frequency, (0 = 44100 Hz) |
39 | 1 | number of frames per second (by default 30) |
40 | 1 | number of default action handlers (by default 9) |
41 | 1 | number of map layers (by default 11) |
42 | 1 | number of transportation methods (by default 3) |
43 | 1 | highest bytecode command used in scripts |
44 | 1 | character sprites upwards delta y |
45 | 1 | number of char sprites per layer (by default 20) |
46 | 2 | must be zero |
48 | 8 | assets boundle unique id |
56 | 8 | size of the assets boundle |
NOTE: bytes 16 - 56 are the same as the TirNanoG File Format header in the game world file. The client must compare this with the cached asset boundle's header, and reply with a type 1 message if they differ (see below). Assets boundle size must be either the file's size or the offset of the first Map asset in the file, whichever is the smallest, minus 64.
The client can examine the header fields to decide if it can interpret the given game or not. If not, then it closes the connection without a reply, and shows an error message to the user. (Currently not relevant because only one revision of the format exists, but might be important in the future.)
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 1, size 2 |
Server to client
Assets boundle data, as many bytes as advertised in the "Hello" message, then 4 bytes CRC checksum. After sending these bytes the server closes the connection. The client can compare its calculated CRC with the last 4 bytes to determine if the assets were received successfully or if it needs to repeat the process.
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 2, size 36 + u + n |
2 | 2 | client's preferred language |
4 | 32 | password hash |
36 | u | user name, zero terminated UTF-8 |
36 + u | n | character specification |
The data in the character specification field depends on the available character options and attributes sent in the assets boundle. It is a list of int32_t integers, in two blocks. In the first block each value is (option value << 8 | option palette index), and the second block is just simply attribute values, same as in the boundle definition except values for global attributes are omitted. The client's language is just informational, only used with the "Text Chat" and "Dialog" messages (see below) if the server has a translator configured.
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 3, size 36 + u |
2 | 2 | client's preferred language |
4 | 32 | password hash |
36 | u | user name, zero terminated UTF-8 |
Server to client
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 4, size 4 |
2 | 2 | Reason code |
If sent as a response to registration, then it means registration is temporarily disabled or another user by that name
already exists. When sent as a response to login, then it means bad user name or password. The field reson code
might
give some insight on the situation.
Server to client
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 5, size 270 |
2 | 256 | session token |
258 | 8 | session OTP |
266 | 4 | compressed world state size (n) |
270 | n | compressed world state |
The registration/login was successful. Note how the compressed state is sent right after this message, but it's not counted
in the message's size. The compressed world state is in the same format as the TirNanoG Saved Game Format, but
without the header or preview, and must not contain DENY
chunk and it must contain exactly one USER
chunk.
After this message the server closes the connection, and client sends an UDP hello.
Further communication between the client and the server is done over UDP, where the packet data is XOR'd with the session token. Reliability is provided at application level, by the NetQ library.
Packet length is limited in 1400 bytes (including the 6 + 2 bytes header, 6 from NetQ, and 2 from this documentation).
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: 'Hi' |
2 | 4 | must be zero |
6 | 2 | magic: type 6, size 10 |
8 | 8 | session OTP |
This is the one and only UDP packet which isn't encrypted. Its purpose is to allow the server to register the peer's remote UDP port, and to punch a hole on firewalls (just like UPnP, this registers a related status between the client and the server in the firewall, so further UDP packets originating from the server will be allowed by the default related-allow rule). When received by the server for the first time, it echoes this packet back (the protocol allows multiple datagrams here because UDP isn't reliable).
If a client sends this with zero OTP, then the server replies with a (shortened) hello message. This is used to detect servers on LAN.
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 0, size 32 |
2 | 2 | Supported protocol version (0 as of writing) |
4 | 4 | TirNanoG Server magic 'TNGS' |
8 | 8 | supported protocol feature flags, must be zero |
16 | 16 | game id, padded with zeros |
NetQ adds 6 bytes to the header, but that's not returned to the application, so hereafter I'm going to exclude the decrypted message's first 6 bytes from the documentation.
Server to client
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 7, size 2 + n |
2 | n | data |
The data is serialized game state. The server keeps track of player's location, and only sends updates for their surroundings. The server might send this packet with empty game state to check if the client is still connected.
The data is a list of various length records, each prefixed by an identifier byte. 0 - player movement, 1 - player logout, 2 - player sprites, 3 - player inventory, 4 - player equipment.
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 8, size 12 + n |
2 | 4 | internal map id |
6 | 2 | map x |
8 | 2 | map y |
10 | 1 | direction |
11 | 1 | altitude |
12 | n | data |
The data is serialized action list. The client might send this packet with empty actionlist if it wants to tell the server it's still connected, but there's no player activity to report.
In response the server sends an update to the clients on the neighboring maps.
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 7, size 17 |
2 | 1 | 0, player movement |
3 | 4 | internal user id |
7 | 4 | internal map id |
11 | 2 | map x |
13 | 2 | map y |
15 | 1 | direction |
16 | 1 | altitude |
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 9, size 2 |
When the server does not get acknowledged replies from a client for 5 seconds, then it acts as if it has recevied this package. Otherwise this package is sent to the server when the player quits the game.
In response the server sends an update to the clients on the neighboring maps.
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 7, size 7 |
2 | 1 | 1, player logout |
3 | 4 | internal user id |
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 10, size 6 |
2 | 4 | internal map id |
Internal map id is the TirNanoG File Format section type 35 map asset number.
Server to client
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 11, size 12 + n |
2 | 4 | internal map id |
6 | 3 | total size of the map asset data |
9 | 3 | fragment's offset |
12 | n | map asset data fragment |
The data is deflated, and in the same format as in TirNanoG File Format map asset data. There can be more of this packet if asset data does not fit into one UDP packet, and in that case it is the client's duty to re-arrange the packets into sending order (should be done automatically by the underlying NetQ library).
Sent both by the server to the client and client to the server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 12, size 6 + n |
2 | 4 | internal user id (zero on send) |
6 + n | n | zero terminated UTF-8 message |
Also sent by the server in response to Text Chat packet to the players that can "hear" the message. The clients should already have a copy of the nearby players' records, but if by chance not, they might reply with a "Whois" message.
Sent both by the server to the client and client to the server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 13, size 8 + n |
2 | 4 | speaker internal user id (zero on send) |
6 | 1 | sample rate (0 = 22050 Hz, 1 = 44100 Hz) |
7 | 1 | bit 0..3: precision, bit 4..7: compression |
8 | n | PCM data |
When the server receives this packet, then it fills in speaker id otherwise forwards unmodified to the players that can hear the message (in the game world). If the voice recording happens at 44100 Hz, with 8-bit precision (enough for chat), that means 30 packets per second, each with a 1470 bytes of PCM data uncompressed. Same if 22050 Hz used with 16-bit precision. Using 44100 Hz with 16-bit PCM (which is the default for the sound and music assets in the game) would require 60 packets per second. Compression is left out with 22500 Hz and 8-bit or if the MTU size is configured larger than 1526 (1470 data + 8 TNG header + 48 IPv6/fragment header). Byte 7's lower tetrad encodes sample precision (0: 8 bit, 1: 16 bit, 2: 16 bit big endian), upper tetrad the compression method (0: none, 1: gzip deflate). Voice recording is always mono channel.
Sent both by the server to the client and client to the server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 14, size 7 |
2 | 4 | internal quest id |
6 | 1 | 0: accepted, 1: completed, 2: cancelled |
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 15, size 4 |
2 | 4 | internal user id |
In response the server sends an update packet to the same client.
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 7, size 7 + o + n |
2 | 1 | 2, player sprites |
3 | 4 | internal user id |
7 | o | character options (world.numchar) |
7+o | 64 | equipted objects (32 * uint16_t) |
71+o | n | name (zero terminated UTF-8) |
Used to generate the character sprites on client side. For the map, as well as portrait for the dialog window.
Client to server
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 16, size 4 |
2 | 4 | internal user id |
To query the client's own inventory, -1 is used as user id. In response the server sends an update packet to the same client.
Offset | Length | Description |
---|---|---|
0 | 2 | magic: type 7, size 7 + n |
2 | 1 | 3, player inventory |
3 | 4 | internal user id |
7 | n | deflate compressed inventory list |
Used when players exchange items (P2P market).