Snaps Concept
This page describes the concept of snaps, their structure, when snap items should be sent, and how they are used in the protocol.
Understanding Snaps
Some of the system message chunks are called Snaps. They are used by the server to send the Snap Items to the client. One snap chunk can contain multiple snap items (or no items at all).
Snap items represent the state of something in the game world, such as tees, projectiles, player info, game status, etc... Every snap item has an item ID, ID, and a custom payload structure to follow. The item payload is ALWAYS made up of integers.
Delta Snap
Delta snap is a crucial concept for sending snap chunks correctly; that's why it will be explained first. The first snap sends all snap items. Once the client acknowledges a snapshot, the server can use it as the baseline, also called the delta snap.
The baseline is selected when the client sends an Input Chunk with that snapshot's tick as Ack Game Tick. When the server still has that snapshot, it sends only the difference between the current game state and the baseline.
Every snap chunk contains at least a current tick and delta tick value:
- Current Tick : the game tick of the snapshot being sent. The client acknowledges this tick after successfully reconstructing the snapshot.
- Delta Tick : Current Tick minus the tick of the baseline actually used to encode the payload. The client obtains the baseline tick with
Current Tick - Delta Tick.
INFO
Without an available baseline, the server uses an empty baseline: a snapshot with no items, used only for comparison. All current snap items are sent as new items, so this produces a full snapshot. The baseline tick is -1, so Delta Tick is Current Tick + 1, even if the client's Ack Game Tick has another value.
The original 0.6 server keeps three seconds of snapshot history and removes older snapshots regardless of ACK progress. An acknowledged snapshot may therefore have expired, even while the client continues sending inputs. The retention window is a server policy, not a limit imposed by the protocol.
INFO
You can use a different retention period or limit the number of stored snapshots instead.
WARNING
The header and payload must use the same baseline. Do not signal base tick -1 while encoding differences from a previous snapshot.
Snap Types
There are three snap chunks: Snap Empty, Snap Single, and Snap Slice. Snap Empty contains only the current and delta tick and is used when there are no items to send. This means there are no changes from the baseline. Snap Single carries a delta payload in one chunk, while Snap Slice splits it across multiple chunks.
These data are CRC, removed items count, updated items count, and the snap payload:
- CRC : integer value that is used to check the integrity of the snap payload.
- Size : the size of the snap chunk.¹
- Removed Items Count : amount of items that were removed compared to the delta snap.
- Updated Items Count : amount of items updated (or added) compared to the delta snap.
WARNING
The maximum size of a snap chunk payload is 900 bytes. Removed Items Count and Updated Items Count are part of the snap payload and it sizes must be considered - ¹also for the Size field.
Snap Item ID & ID
Every snap item, in addition to the item ID, has it own ID. The ID is a unique identifier for the item in the current game context. It is important to understand that every snap item will have a different way to have an ID assigned.
There are the following ways for a snap item to have an ID assigned:
- None : the ID will always be 0.
- Connection : the ID will be the connection index of the player (0 to 63).
- Event : the ID will be the index of the item in the event queue of the current snap. From 0 to 128 (this maximum value is not a rule).
- Snap ID Pool : the ID will obtained through a pool that controls reserved and free ids from 0 to a maximum number of 16384 (16 * 1024).
INFO
You can find which way each snap item uses to have an ID assigned on the Snap Items page.
Snap Payload
The snap payload sent by the server is not the list of items, as it depends on the context, the removed items count and updated items count.
When there are removed items, at the beginning of the snap payload, it will send all removed items Item ID, ID (not sending the item payload). When there are new items (counted as updated items), the server will send all items Item Id, ID, Item Payload right after the removed items data.
When there are updated items (not new items), the server will use the same format Item Id, ID, Item Payload. However, the item payload will be the difference between the current state and the item state in the delta snap. The server will loop all item payload integers to make the following difference:
payloadInt[X] = payloadInt[X] - deltaPayloadInt[X]INFO
1. Note that to calculate the payload difference, the server will loop all item payload packet integers and NOT the item payload bytes.
2. Difference Example :
- Delta Item X Position : 50
- Current Item X Position : 10
- Final Payload X Position : -40 (10 - 50)
Snap Slice
Sometimes, you cannot fit all the payload in a Snap Single chunk due to the snap chunk payload size limit, so you need to split them into multiple snap slices (and multiple packets). This is when the Snap Slice comes in.
Snap Slice will be sent with the same current tick, delta tick, and CRC (but a different size). Multiple packets with a snap slice will be sent until the full payload (and items) are sent. The Snap Slice has two extra fields: Total Number and Current Number.
WARNING
Removed Items Count and Updated Items Count are part of the snap payload and must be sent only in the first Snap Slice.
The maximum amount of Snap Slices of a single snap payload is 64.
When to send Snaps
The original 0.6 server tracks a Snap State for every client: INIT, FULL, or RECOVER. Every new client starts in INIT.
An Input Chunk with a positive Ack Game Tick sets the state to FULL. When building a snapshot, if that acknowledged baseline cannot be found in the retained history, a FULL client changes to RECOVER and receives a full snapshot using base tick -1.
Recovery depends on a missing baseline, not simply on receiving no input for three seconds. It can also happen while inputs continue to arrive with an expired or unavailable ACK. A later ACK for a retained snapshot allows normal delta snapshots to resume.
The reference server uses the following sending frequencies:
- INIT : Snap chunk every 10 ticks.
- FULL : Snap chunk every 1 tick (or every 2 ticks to save bandwidth).
- RECOVER : Snap chunk every 50 ticks.
INFO
The Snap State, sending frequency, and history retention system is a way to avoid flooding the client with snap chunks when it is not necessary. It is not a protocol requirement, and you can implement your own system.
Calculating CRC
CRC (Cyclic Redundancy Check) is the value used to check the integrity of the snap payload. To calculate the snap CRC, you can implement the following pseudo-code:
crc = 0
foreach (snapItems as snapItem) {
itemPayload = snapItem->getPayload() // clone the payload on memory to not modify the original
while (true) {
extractInt = itemPayload->extractInt()
if (extractInt == null) {
break
}
crc += extractInt
}
}
crc = toInt32(crc)WARNING
The CRC result must be a signed 32-bit integer. You must ensure the correct overflow, as explained in the Integer Overflow section.
String Packing To Snap Items
There is one snap item that contains a string, the CLIENT INFO. The way you pack strings to this snap differs from the default since the snap payload can only contain integers.
You will need to convert the string with a fixed size into multiple integers using a specific method. You can find the implementation of the pack and unpack of the method here. Or you can use the following pack pseudo-code:
function convertCharsToIntArray(chars, intNum)
{
integers = array_fill(0, intNum, 0) // Creates a new array with intNum elements, filled with 0
charsCount = count(chars)
index = 0
for (i = 0; i < intNum; i++) {
buffer = [0, 0, 0, 0]
for (
c = 0;
c < 4 && index < charsCount;
c++, index++
) {
buffer[c] = chars[index + 1]
}
integers[i] = toInt32(
((buffer[0] + 128) << 24) | ((buffer[1] + 128) << 16) | ((buffer[2] + 128) << 8) | ((buffer[3] + 128) << 0)
)
}
integers[num - 1] = toInt32(integers[num - 1] & 0xFFFF_FF00)
return integers
}INFO
1. Every integer can contain 4 characters of the string. With the exception of the last character of the last integer, that must be empty.
2. Example: if you pass intNum as 5, the maximum string size will be 19 (20 - 1) characters.
WARNING
Every converted integer must be a signed 32-bit integer. You must ensure the correct overflow, as explained in the Integer Overflow section.