Not all information is complete, and there are likely to be errors.
Prelude
This book aims to describe the process of how to implement CS2 Demo parsing and streaming.
As of writing (Apr 2024), there is very little information on how read and process this format. We intend to detail it here, collecting information from a number of implementations and our own research.
Referencing this guide will help us double/triple check our implementation, and we hope that developers can implement and understand this format/protocol and not go through the pain that we did.
Resources
To make understanding easier, I would recommend a few resources:
Contributing
This book is not perfect. If you spot a mistake, wish to update or refactor wording your more than welcome too. We use MDBook, and it's all written in Markdown, so it's easy to edit. Open an Issue or a Pull-Request on GitHub and We'll give it a look!
Questions & Feedback
If you have any questions or feedback, please feel free to reach out to me on discord (samhdev)
Alternatively, an unrelated project has a Discord Server that sometimes helps with demo parsing. They're a friendly bunch of people who are interested in the topic.
Authors
- Sam Huddart ([email protected])
- Max Standford Taylor ([email protected])
Glossary
Structural Terms
| Term | Description |
|---|---|
| Magic Header | A header determining the file type |
| Frame | A record within a demo |
| Frame Header | A description of a demo frame |
| Packet | A recorded game packet within a demo |
| Tick | An measurement of time, 64 per second |
Entity Terms
| Term | Description |
|---|---|
| Class | A type of entity ( e.g. Player) |
| Entity | An instance of a class within the server |
| Send-Tables | A version agnostic description of entity structure |
| Symbol Index | A string within the send-tables |
| Serializer | A layout description of a class or set of fields |
| Field | A field within a serializer, describing its type and how to decode it |
| Field Decoder | A decoder for a given field |
| Property | A instance of a field within a entity, perhaps containing nested properties |
| Field Path | A path to a specific field within a tree of serializers, used to set or retrieve a value |
| Entity Update | A server packet to describe creation, deletion and updating entity properties |
Counter-strike Demos
Demos are .dem files containing recorded gameplay from either a server or client perspective. They are an outer wrapper over the game's ProtoBuf protocol, containing the data needed to accurately parse entity and event data contained within.
Demos consist of a Magic Header and a series of frames. We will discuss this below.
Magic File Header
Every demo file begins with a magic header. This header enables us to confirm if a .dem file is indeed a demo and to distinguish between Source-1 and Source-2 demos.
For Source-2, the following ascii bytes should be present.
b'PBDEMS2\0`
This header is seven characters long but eight bytes in length (including the null terminator). We think it means "ProtoBuf Demo Source2"
If you want to determine if a Demo is Source-1, you can check the following 8 bytes:
b'HL2DEMO\0'
Unknown Header Bytes
After reading the magic header, there are eight bytes that have an unknown purpose.
We believe it might contain some verification info, maybe the length of data, but we were unable to find any correlation.
Frames
After the file header, demos consist of multiple frames. They contain both game-packets, parsing information and other demo data.
Frames consist of two parts, a header and a payload. Frame payloads can (but not always) be compressed.
Frame Header
A frame header has three fields, all three of which are in the Protobuf Varint format.
| Field | Description |
|---|---|
| Command | The type of frame payload |
| Tick | The tick at which this frame occurs |
| Length | The length of the payload (in bytes) |
Aside: Protobuf VarInts
Variable-width integers, or varints, are at the core of the wire format. They allow encoding unsigned 64-bit integers using anywhere between one and ten bytes, with small values using fewer bytes. Each byte in the varint has a continuation bit that indicates if the byte that follows it is part of the varint. This is the most significant bit (MSB) of the byte (sometimes also called the sign bit). The lower 7 bits are a payload; the resulting integer is built by appending together the 7-bit payloads of its constituent bytes.
Frame Commands
A Frame commands describe the content of a frame payload.
They are specified in the demo.EDemoCommands enum,
which can be found here.
Here are some of the notable ones:
| Command | Value | Description |
|---|---|---|
DEM_Stop | 0 | End of Demo Frames |
DEM_FileHeader | 1 | A header, describe the server name, map, build version, etc.. |
DEM_FileInfo | 2 | A "footer", often at the end of the demo containing playback information |
DEM_SyncTick | 3 | ? |
DEM_SendTables | 4 | Serializers for Entity Data (We'll discuss this later) |
DEM_ClassInfo | 5 | Entity Class names and IDs mapping to serializers (We'll discuss this later) |
DEM_StringTables | 6 | String tables (We'll discuss this later) |
DEM_Packet | 7 | Game Packet |
DEM_SignonPacket | 8 | Game Packet, but for the server->client init |
DEM_ConsoleCmd | 9 | A Server-side console command |
DEM_UserCmd | 12 | A Client-side user-action, include sub-tick button and movement data |
DEM_FullPacket | 13 | Game Packet, but it has string-tables |
Defined in the protobuf, There are two variants that are not valid commands.
| Command | Value | Description |
|---|---|---|
DEM_Max | 18 | the max (N < MAX) command value |
DEM_IsCompressed | 64 | a flag denoting if the frame is compressed |
These represent two things:
- The range of commands is
0..18exclusive. - The sixth bit (
log2(64) = 6), is an additive flag representing if the frame is compressed.
Note, the current values mean that the command field will only ever be 1 byte in length. Optimise at peril of volvo changing it
Once you know if a frame is compressed, you can negate (set to low) the DEM_IsCompressed bit,
and parse the command correctly.
// e.g. A Compressed DEM_SignonPacket
72 & (!64) = 8
Frame Ticks
The tick field specifies the tick that a given frame happens.
Ticks should always be positive integers, and frames should be in a linear format, e.g. never go back in time.
There is one exception to this,
during the sign-on phase,
frames with the tick 4294967295 (2^32–1) denote a tick before time began.
Frame Compression
Frames may be compressed. The 6th bit of the command field signifies whether a frame is compressed. When set, the frame's payload is compressed using the Snappy compression format.
Frame Payload
Frame payloads directly follow the frame header and consist of length bytes.
These bytes might be compressed (see above) and must be decompressed if necessary.
The payload is a Protobuf message whose format is specified by the command field in the header.
The following table maps commands to their corresponding Protobuf payload types:
| Command | Payload Message | Notes |
|---|---|---|
DEM_Stop | CDemoStop | Empty |
DEM_FileHeader | CDemoFileHeader | |
DEM_FileInfo | CDemoFileInfo | |
DEM_SyncTick | CDemoSyncTick | Empty |
DEM_SendTables | CDemoSendTables | Bytes of another protobuf message |
DEM_ClassInfo | CDemoClassInfo | |
DEM_StringTables | CDemoStringTables | |
DEM_Packet | CDemoPacket | |
DEM_SignonPacket | CDemoPacket | |
DEM_ConsoleCmd | CDemoConsoleCmd | |
DEM_UserCmd | CDemoUserCmd | |
DEM_FullPacket | CDemoFullPacket | CDemoStringTables + CDemoPacket |
Next Steps
That's it for demo frames, now we can parse them into packets.
Game Packets
Certain frames contain game packets, which are recorded server game packets. While stored as byte arrays within these frames, the data is actually encoded in a bitstream format.
Frames contain multiple packets, concatenated together. Within a packet is a single protobuf message, encoded as bytes, we'll discuss this below.
Layout
Packets have three fields:
| Field | Type | Description |
|---|---|---|
packet_id | u_bit_var | the packet type |
packet_size | varint | the length of data in the packet (in bytes) |
packet_data | bytes | the actual packet data |
Note:
While packet_data is a set of bytes, it may not be byte-aligned, which can occur overhead.
Packet IDs
Packet Data consists of protobuf messages with each packet_id corresponding to a given type.
Packet IDs come from multiple protobuf enums. At this time we can guess the following types are packet IDs:
SVC_MessagesNET_MessagesECstrike15UserMessagesEBaseGameEventsEBaseUserMessagesECsgoGameEvents
It may be the case that some of these overlap, while we cannot confirm this fact, we have had success parsing in that order.
Packet Payloads
Each packet IDs matches to its given protobuf message.
Here are some notable ones:
| Packet Id | Payload Type | Description |
|---|---|---|
svc_ServerInfo | CSVCMsg_ServerInfo | Server Info (including build version, map name, class field size) |
svc_ClassInfo | CSVCMsg_ClassInfo | Mappings of class ids to their names/serializers |
svc_PacketEntities | CSVCMsg_PacketEntities | A list of entity updates |
svc_FlattenedSerializer | CSVCMsg_FlattenedSerializer | Serializer specifications |
We wouldn't be able to list all the messages here, I would recommend exploring the protobuf files.
Most enum variants match to a respective message, but some have mismatched names.
Bit-streams
Source-2 uses network order (little endian) bit-streams when sending data to clients, this comes with the advantage of compact wire messages, and ease of implementation.
Writers Note:
I am of-course joking about it being easy, Bit Reading was the source of many of our bugs throughout our development process, and took 4 attempts to get it right. It's hard to debug, and with the only documentation being other peoples code (specifically manta), it's a fucker to get working.
Bit-streams are used in a variety of places, including but not limited to:
- Game Packets
- Entity Updates
- String Tables
This set of chapters aims to document the format of the bit-streams and its many types. However, it's no substitute to looking over some code; you can find our implementation here
Bit Reader
Most data sent over the wire, is formated in bits, but given as bytes.
Implementing the bit stream boils down to four simple statements:
- You keep a buffer, no smaller than 32 bits
- When filling the buffer, you write to the left
- When filling the buffer, you fill byte by byte.
- When consuming from the buffer, you read from the right.
Pseudo Code
def fill
while offset < N
buffer |= next() << offset
offset += 8
def consume
if offset < N then fill
mask = (2 ^ N) - 1
value = buffer & mask
buffer >>= N
offset -= N
return value
Optimisations
Precompute, (hopefully with constant expressions and compile-time optimization) the masks for consuming.
Reading bytes can be improved by checking if the reader is currently "byte aligned".
- This can be done by checking if your offset is zero, e.g. no stored bits. (
offset == 0) - The benefit of which, is skipping the cost of filling then draining the buffer every time.
Base Types
The base types that can be read from the bitstream.
We have provided sample pseudocode for each type, in which we assume a predefined function read(bits) that reads
a number of bits from the bitstream.
Table of Contents
Basic
bit_bool
A boolean value, encoded as a single bit.
FUNCTION bit_bool:
RETURN read(1) > 0
Integers
bit_varuint_32
A 32-bit unsigned integer, encoded as a protobuf style varint.
FUNCTION bit_varuint_32:
value = 0
offset = 0
// A 32-bit varint is at most 5 bytes long. 5 * 7 = 35.
// This while loop is a safeguard against reading too many bytes.
WHILE offset != 35:
byte = read(8)
// Add the lower 7 bits of the byte to the result.
value = value | ((byte & 0x7F) << offset)
offset = offset + 7
// If the most significant bit is 0, we're done.
IF (byte & 0x80) == 0:
BREAK
RETURN value
bit_varint_32
A 32-bit signed integer, encoded as a protobuf style varint.
FUNCTION bit_varint_32:
// First, decode as an unsigned integer.
unsigned_value = bit_varuint_32(reader)
// Then, perform ZigZag decoding.
value = unsigned_value >> 1
IF (unsigned_value & 1) != 0:
value = NOT value // or -(value + 1)
RETURN value
bit_varuint_64
A 64-bit unsigned integer, encoded as a protobuf style varint.
FUNCTION bit_varuint_64:
value = 0
offset = 0
index = 0
LOOP:
byte = read(8)
// If the most significant bit is 0, this is the last byte.
IF byte < 0x80:
// Check for overflow. A 64-bit varint is at most 10 bytes.
IF index > 9 OR (index == 9 AND byte > 1):
THROW VarintOverflowError
RETURN value | (byte << offset)
// Add the lower 7 bits to the result.
value = value | ((byte & 0x7F) << offset)
offset = offset + 7
index = index + 1
bit_varint_64
A 64-bit signed integer, encoded as a protobuf style varint.
FUNCTION bit_varint_64:
// First, decode as an unsigned integer.
unsigned_value = bit_varuint_64(reader)
// Then, perform ZigZag decoding.
value = unsigned_value >> 1
IF (unsigned_value & 1) != 0:
value = NOT value // or -(value + 1)
RETURN value
bit_uint64_le
An 64-bit unsigned integer, encoded as a fixed 64-bit value in little-endian format.
FUNCTION bit_uint64_le:
bytes = read(64)
RETURN u64_from_le_bytes(bytes)
bit_bitvar_uint
A 32-bit unsigned integer, encoded as follows:
- The first 4 bits are the MSB of the value.
- The next 2 bits determine how many more bits to read.
- The remaining bits are the LSB of the value.
FUNCTION bit_ubitvar:
// Read the first 6 bits.
value = read(6)
// The next 2 bits determine how many more bits to read.
SWITCH (value & 0x30):
// 01: read another 4 bits
CASE 16:
RETURN (value & 15) | (read(4) << 4)
// 10: read another 8 bits
CASE 32:
RETURN (value & 15) | (read(8) << 4)
// 11: read another 28 bits
CASE 48:
RETURN (value & 15) | (read(28) << 4)
// 00: the value is just the first 6 bits
DEFAULT:
RETURN value
bit_fieldpath_uint
A 32-bit unsigned integer, encoded in huffman style:
- When
<1>then read 2 bits - When
<01>then read 4 bits - When
<001>then read 10 bits - When
<0001>then read 17 bits - When
<0000>then read 31 bits
This type is used for field paths.
FUNCTION bit_fieldpath_int:
// This encoding uses a series of single-bit flags
// to determine the number of bits to read for the value.
IF bit_bool() == true:
RETURN read(2)
IF bit_bool() == true:
RETURN read(4)
IF bit_bool() == true:
RETURN read(10)
IF bit_bool() == true:
RETURN read(17)
RETURN read(31)
bit_component_uint
A 32-bit unsigned integer, encoded as a single bit.
FUNCTION unsigned_integer_component:
// The value is simply a single bit.
RETURN read(1)
Floating-Point
bit_float32
A 32-bit floating-point number, encoded as a fixed 32-bit value in little-endian format.
FUNCTION bit_float32:
bytes = read(32)
// Read bytes as a little-endian unsigned integer.
le_uint = u32_from_le_bytes(bytes)
// Reinterpret the integer bits as a float.
RETURN f32_from_bits(le_uint)
bit_float32_coord
A 32-bit floating-point number representing a coordinate. It's encoded with flags for the presence of integer and fractional parts, allowing for variable precision.
FUNCTION bit_float32_coord:
// Read flags to see if integer and fractional parts exist
has_integer_part = read(1)
has_fractional_part = read(1)
// If neither part exists, the value is zero
IF has_integer_part == 0 AND has_fractional_part == 0:
RETURN 0.0
// Read the sign of the float
is_negative = bit_bool()
integer_value = 0
IF has_integer_part == 1:
// Read the 14-bit integer part
integer_value = read(14) + 1
fractional_value = 0
IF has_fractional_part == 1:
// Read the 5-bit fractional part
fractional_value = read(5)
// Combine the parts. The fractional part is divided by 2^5 (32)
value = integer_value + fractional_value / 32.0
IF is_negative == TRUE:
value = -value
RETURN value
bit_normal
A 32-bit floating-point number representing a normal vector component, encoded in 11 bits plus a sign bit.
FUNCTION bit_float32_normal:
is_negative = bit_bool()
length = read(11)
// This maps the 11-bit integer to a float range.
value = length * (1.0 / 2048.0 - 1.0)
IF is_negative:
RETURN -value
ELSE:
RETURN value
bit_float32_noscale
A standard 32-bit IEEE float, read directly from the bitstream without any scaling.
FUNCTION bit_float32_noscale:
bits = read(32)
// Reinterpret the integer bits as a float
RETURN f32_from_bits(bits)
bit_float32_angle
An angle in degrees, encoded in a variable number of bits.
The raw integer value is normalised to the [0, 360) range.
FUNCTION bit_float32_angle(bits):
raw_value = read(bits)
// Normalize the raw integer to the range [0, 360)
divisor = 2^bits
angle = raw_value * 360.0 / divisor
RETURN angle
bit_float32_angle_precise
A precise angle, encoded as a 20-bit bit_float32_angle, then mapped to the [-180, 180) range
FUNCTION bit_float32_angle(bits):
RETURN bit_float32_angle(20) - 180.0
Angles
bit_qangle_precise
A 3-component angle vector, where each component is a bit_float32_angle_precise.
Each component is prefixed with a boolean flag indicating if it's present.
FUNCTION bit_qangle_precise:
has_x = bit_bool()
has_y = bit_bool()
has_z = bit_bool()
vec = [0.0, 0.0, 0.0]
IF has_x:
vec[0] = bit_float32_angle_precise()
IF has_y:
vec[1] = bit_float32_angle_precise()
IF has_z:
vec[2] = bit_float32_angle_precise()
RETURN vec
bit_qangle_fixed
A 3-component angle vector, where each component is a bit_float32_angle of a fixed bit-size.
FUNCTION bit_qangle_fixed(bits):
vec = [0.0, 0.0, 0.0]
vec[0] = bit_float32_angle(bits)
vec[1] = bit_float32_angle(bits)
vec[2] = bit_float32_angle(bits)
RETURN vec
bit_qangle_coord
A 3-component angle vector, where each component is a bit_float32_coord.
Each component is prefixed with a boolean flag indicating if it's present.
FUNCTION bit_qangle_coord:
has_x = bit_bool()
has_y = bit_bool()
has_z = bit_bool()
vec = [0.0, 0.0, 0.0]
IF has_x:
vec[0] = bit_float32_coord()
IF has_y:
vec[1] = bit_float32_coord()
IF has_z:
vec[2] = bit_float32_coord()
RETURN vec
Vectors
bit_vec3_normal
A 3D normal vector comprised of bit_normal.
The X and Y components are read if present, and the Z component is calculated to ensure the vector is of unit length.
FUNCTION bit_vec3_normal(reader):
value = [0.0, 0.0, 0.0]
has_x = bit_bool()
has_y = bit_bool()
IF has_x:
value[0] = bit_normal(reader)
IF has_y:
value[1] = bit_normal(reader)
is_z_negative = bit_bool()
// Calculate Z to make it a unit vector
product_sum = value[0] * value[0] + value[1] * value[1]
IF product_sum < 1.0:
value[2] = square_root(1.0 - product_sum)
ELSE:
value[2] = 0.0
IF is_z_negative:
value[2] = -value[2]
RETURN value
bit_vec_float32
FUNCTION bit_vec_float(decoder; degree):
vec = [0.0] * degree
FOR i in 0..degree:
vec[i] = decoder()
RETURN vec
Game Types
bit_simulation_time
A 32-bit float representing the simulation time of the game, decoded from a bit_varuint_32 and scaled by 1/64.
FUNCTION bit_simulation_time:
base = bit_varuint_32()
RETURN base / 64.0
bit_rune_time
A 32-bit float representing the time of the game, decoded as 4 bits
FUNCTION bit_rune_time:
RETURN read(4)
Writers Note: I am unsure what 'rune' means in this context.
We derived this name from field
encoderproperty"runetime"
bit_ammocount
An unsigned integer 32-bit representing an ammo count,
decoded from a bit_varuint_32 with 1 subtracted from it.
FUNCTION bit_ammocount:
value = bit_varuint_32()
RETURN MAX(0, value - 1)
Strings
bit_string_n
A UTF-8 string with a fixed length in bytes.
FUNCTION bit_string_n(length):
bytes = read_bytes(length)
RETURN string_from_utf8(bytes)
bit_string_null_term
A UTF-8 string that is terminated by a null byte (\0).
FUNCTION bit_string_null_term:
buffer = []
LOOP:
byte = read_byte()
IF byte == 0:
BREAK
buffer.push(byte)
RETURN string_from_utf8(buffer)
bit_string_terminated
A string that is terminated by a null byte (\0), with a hard-coded limit of 4096 bytes.
FUNCTION bit_string_terminated:
buffer = ""
FOR i from 0 to 4096:
byte = read_byte()
IF byte == '\0':
BREAK
buffer.append(character(byte))
RETURN buffer
Quantised Floats
Quantised floats are variable precision floats, encoded as bits. They are configured by the server and may change from demo to demo.
Initialisation
Parameters
When initialising a QF, there are four parameters specified by a serializer field.
| Param Name | Type | Description |
|---|---|---|
bit_count | int | The number of bits a value will be encoded with |
flags | int(8) | Flags for configuring behaviour (see below) |
low_value | float(32) | (Optional) The smallest value encodable |
high_value | float(32) | (Optional) The largest value encodable |
Flags
As seen above, the flag field allows different behaviour for a QF
| Flag name | Bit | Description |
|---|---|---|
ROUND_DOWN | 0 | Read an extra bit, to quickly specify the lowest value |
ROUND_UP | 1 | Read an extra bit, to quickly specify the highest value |
ENCODE_ZERO | 2 | Read an extra bit, to quickly specify zero |
ENCODE_INTS | 3 | Have no idea, there's maths involved and it looks scary |
Setup
Setting up a QF is expensive and overcomplicated, much like my attempts at relationships.
Before getting into the weeds, there's two basic steps to perform:
- When low or high values are not specified, they default to
0.0and1.0respectively - When the bit_count is zero, or greater than or equal to 32, it's treated as a no-scale type.
Next, we recompute flags, removing invalid or unnecessary behaviour. Why are these flags present if the server disables them anyway? We'll never know. Thanks, Volvo.
- if
low_value is 0, andround downis set, then we unsetencoding zero. - if
high_value is 0, andround upis set, then we also unsetencoding zero. - if
low_value is 0, andencode zerois set, then we setround down, and unsetencoding zero - if
high_value is 0, andencode zerois set, then we setround up, and unsetencoding zero - if
low_value is greater than zerothen we unsetencoding zero - if
high_value is less than zerothen we unsetencoding zero - if
encode intsis set, then we unset every other flag - if
round upandround_downare both set, then return an error.
Next, we want to calculate 3 values:
offset, the offsetdec_mul,high_low_mul
The first step is to calculate the number of steps,
essentially the size of the float.
This should be equal to 1 << bit_count.
Next, do some rounding adjustment only if we round down, or round up:
- calculate the range of values.
- divide this range by the number of steps, this is
offset - offset the value by offset
- if
round downthen subtract offset fromhigh - if
round upthen add offset tolow
- if
Next, if encode_ints is true:
- calculate the range of values.
- if this range is less, than one then set it to
1.0
- if this range is less, than one then set it to
- calculate the
log2and round up to the highest integer - shift one by the rounded log, we'll call this target range,
Writers Note
This is incomplete!
The original end of this file read:
hahahha i give up why is this so fucked.
Send Tables
"Send Tables" are Source-2's way of describing entity structure in a version ambiguous manner. The Send Tables contain a list of serializers, which in-turn contain fields.
Send tables are processed during the sign-on phase of the demo. The aim is to create a structure, mapping serializer field layout to their respective decoders, which can be used to parse updated (and initialised) fields in entity updates.
They are required before you process any entity updates.
Writers Note:
Send tables are one of the most complex parts of the demo protocol.
They are inherently complex and are one of the most challenging parts of the protocol to implement. It's straightforward to introduce a bug early and not find it until much later, as we found out multiple times.
Continue at your own peril...
Expanding Serializers & Fields
Send-tables can be found in two places, within a demo frame (CDemoSendTables) or in a packet (CSVCMsg_FlattenedSerializer).
These contain the entire serializer set for the demo.
They are sent in a flattened format, which has three properties:
| Field | Description |
|---|---|
symbols | a list of strings, referenced by index, representing names of fields, serializers, etc.. |
serializers | a list of serializer objects (see below) |
fields | a list of serializer field objects, referenced by index (see below) |
Serializers
Serializers (as the ProtoFlattenedSerializer_t type) have three properties:
| Field | Description |
|---|---|
serializer_name_sym | the symbol index of the serializer name |
serializers | the version of this serializer |
fields | a list of indexes, referring to the fields in this serializer |
Serializers are the basic building block of the send-tables. They can be referenced (nested) inside other serializers, and some are entity class types. (We'll discuss this later)
Note: multiple serializers with the same name can exist but will have different versions.
When getting a serializer for a class, only one (determinate) serializer will exist with the given name.
Fields
Fields are the most complex part of the send-tables.
Each field has a model and one or more decoded types, which are derived from a set of parameters.
Here are the notable parameters from the ProtoFlattenedSerializerField_t type:
| Field | Symbol Index | Optional | Type | Description |
|---|---|---|---|---|
var_name_sym | ✓ | all | the name of this field | |
var_type_sym | ✓ | ✓ | valued | the type of this field, |
bit_count | ✓ | valued | the bit-count for a field | |
low_value | ✓ | valued | a specified lowest value (QF) | |
high_value | ✓ | valued | a specified highest value (QF) | |
encode_flags | ✓ | valued | flags for controlling encoding/decoding behaviour (QF) | |
var_encoder_sym | ✓ | ✓ | valued | a specified encoder type for this field |
field_serializer_name_sym | ✓ | ✓ | table | the name of the field's nested serializer |
field_serializer_version | ✓ | ✓ | table | the version of the field's nested serializer |
polymorphic_types | ✓ | ✓ | poly-table | a list of possible serializer types for a poly table |
Field Layouts
Fields can be modelled in six possible ways:
| Name | Nested | Description |
|---|---|---|
Value | A simple, single value. | |
FixedArray | A fixed size, array of values | |
VariableArray | A variable size (resizable) array of values | |
FixedTable | ✓ | A single nested table |
VariableTable | ✓ | A variable size (resizable) list of nested tables |
PolyTable | ✓ | A single table that can change type (polymorphic) |
Parsing Field Type
The var_type_sym field contains a string that describes the type of the field.
It can be used to derive the layout of the field as well as the other parameters we will use later in the
send-table process.
This value is a string and can be parsed using the following grammar:
TYPE ::= BASE_TYPE GENERIC? POINTER? COUNT?
BASE_TYPE ::= /[a-zA-Z0-9]+/
GENERIC ::= < TYPE >
POINTER ::= /\*/
COUNT ::= [ /[0-9]*/ ]
Here is an example regex you can use:
([^<\[*]+)(<\s(.*)\s>)?(\*)?(\[(.*)])?
You will use BASE_TYPE, GENERIC POINTER and, COUNT for multiple times in the send-table process.
(Not just for determining the field layout)
Field Type Edge Cases
When processing the COUNT component:
- If the
COUNT == MAX_ITEM_STOCKSthen the count should be8 - If the
COUNT == MAX_ABILITY_DRAFT_ABILITIESthen the count should be48 - If the
COUNTis empty string then the count should be1024 - If the
COUNTis a number, then it should be parsed. - Otherwise, the count should be ignored.
If the base-type is one of the following, it's deemed also as a pointer type:
CBodyComponentDCGBaseAnimatingCBodyComponentBaseAnimatingCBodyComponentBaseAnimatingOverlayCBodyComponentBaseModelEntityCBodyComponentCBodyComponentSkeletonInstanceCBodyComponentPointCLightComponentCRenderComponentCPhysicsComponent
Mapping Field Type to Layouts
Once extracting components from the var-type, we can map the field to its model:
- If the field has poly-types, then:
PolyTable
- If the field has a serializer (
field_serializer_name_sym && field_serializer_versionare present) then:- If it's a pointer type, then:
FixedTable
- Otherwise:
VariableTable
- If it's a pointer type, then:
- If the count is present and not
0, and the base-type is notcharthen:FixedArray
- If the base-type is
CUtlVectororCNetworkUtlVectorBasethen:VariableArray
- Otherwise
Value
Next Steps
Writers Note:
At this point I would recommend introducing a "constructor" type to contain all these parameters (including the field-model). This will make it easier to map field decoders and to construct the field. We called ours
FieldConstructor, but it could be anything.
The next step is to build out decoders for fields.
Field Decoders
To parse entity updates, we need to map the fields of the send-table to their respective types within an update. To do this, we create a "field decoder" for each field which can be initialised from the parameters found from the send-table.
Note: Within this guide, we will refer to the 'field type' as a "decoder".
Decoder Types
First, let's define the possible types of decoders:
We've listed all the decoders we currently support, along with their wire type and the type they decode to.
| Decoder Name | Wire Type | Type | Description |
|---|---|---|---|
Boolean | bit_bool | bool | |
Signed | bit_varint_32 | int32 | |
Unsigned | bit_varuint_32 | uint32 | |
Component | bit_component_uint | uint32 | |
AmmoCount | bit_ammocount | uint32 | |
Unsigned64 | bit_varuint_64 | uint64 | |
FixedUnsigned64 | bit_uint64_le | uint64 | |
String | bit_string_null_term | string | |
NoScale | bit_float32_noscale | float32 | |
Coord | bit_float32_coord | float32 | |
SimTime | bit_simulation_time | float32 | |
RuneTime | bit_rune_time | float32 | |
Qfloat | quantised_float * | float32 | *maps to a quantised float decoder |
QAnglePrec | bit_qangle_precise | float32 | |
QAngleFixed | bit_qangle_fixed | float32 | |
QAngleCoord | bit_qangle_coord | float32 | |
Vector3Normal | bit_vec3_normal | float32[3] | |
Vector2[F] | F x2 * | float32[2] | *calls the float factory, creates a vector of degree 2 |
Vector3[F] | F x3 * | float32[3] | *calls the float factory, creates a vector of degree 3 |
Vector4[F] | F x4 * | float32[4] | *calls the float factory, creates a vector of degree 4 |
Vector6[F] | F x6 * | float32[6] | *calls the float factory, creates a vector of degree 6 |
Matching Decoders
When matching a field to a decoder, there are three methods:
- Factory: A decoder with logical mapping and parameter initialisation
- Overrides: A overridden type, controlled by another parameter (such as
var_name) - Mapping: A simple name to decoder mapping
Factories
As stated before, not all field types map to a decoder one-to-one. Factories map a type to multiple types of decoders depending on their parameters.
Factories can also be called other factories. For example, the float factory can call the Quantised Float factory, or the vector factory calls the float factory.
Quantised Float Factory
- If
bit_count != nilandbit_count < 32:NoScale
- Otherwise, create a quantised float decoder from the parameters:
bit_count(if not present then default to 0)encode_flags(if not present then default to 0)low_valuehigh_value
Float Factory
- If
encoder == "coord":CoordType
- If
encoder == "simtime":SimTime
- If
encoder == "runetime":RuneTime
- If
bit_count == nilorbit_count < 32:NoScale
- Otherwise, call Quantised Float Factory
Unsigned64 Factory
- If
encoder == "fixed64"Vector3[Normal]
- Otherwise
Unsigned64
QAngle Factory
- If
encoder == "qangle_precise"QAnglePrec
- If
bits != nilandbits != 0:QAngleFixed
- Otherwise
QAngleCoord
Vector3 Factory
- If
encoder == "normal"Vector3Normal
- Otherwise call VectorN Factory
vector(3)
VectorN Factory
In this documentation, we reference the vector factory as Vector[N] Factory
where N dictates the degree (size) of the vector.
The vector factory calls Float Factory to create its inner type,
and on the wire reads this N times to get a resulting vector.
Overrides
Some field types can be overridden, there is currently one case:
| Condition | Decoder |
|---|---|
| `var_name == "m_iClip1" | AmmoCount |
Mapping
This table contains a mapping to a field's decoded type to its respective factory or decoder.
This table should be applied in sequential order to prevent any issues with overlap between factories and mappings.
| Base Type | Factory | Decoder |
|---|---|---|
float32 | Float | |
CNetworkedQuantizedFloat | Quantised Float | |
uint64 | Unsigned 64 | |
CStrongHandle | Unsigned 64 | |
Vector | Vector3 | |
Vector2D | Vector[2] | |
Vector4D | Vector[4] | |
Quaternion | Vector[4] | |
CTransform | Vector[6] | |
QAngle | QAngle | |
bool | Boolean | |
int8 | Signed | |
int16 | Signed | |
int32 | Signed | |
HSequence | Signed | |
CEntityIndex | Signed | |
NPC_STATE | Signed | |
AmmoIndex_t | Signed | |
TakeDamageFlags_t | Signed | |
StanceType_t | Signed | |
RagdollBlendDirection | Unsigned | |
BeamType_t | Unsigned | |
EntityDisolveType_t | Unsigned | |
PrecipitatonType_t | Unsigned | |
BeamClipStyle_t | Unsigned | |
SharpSolid_t | Unsigned | |
ShatterPanelMode | Unsigned | |
gender_t | Unsigned | |
item_definition_index_t | Unsigned | |
uint8 | Unsigned | |
uint16 | Unsigned | |
uint32 | Unsigned | |
CHandle | Unsigned | |
Color | Unsigned | |
CUtlStringToken | Unsigned | |
EHandle | Unsigned | |
CEntityHandle | Unsigned | |
CGameSceneNodeHandle | Unsigned | |
CStrongHandle | Unsigned | |
AttachmentHandle_t | Unsigned | |
MoveCollide_t | Unsigned | |
MoveType_t | Unsigned | |
RenderMode_t | Unsigned | |
RenderFx_t | Unsigned | |
SolidType_t | Unsigned | |
SurroundingBoundsType_t | Unsigned | |
ModelConfigHandle_t | Unsigned | |
WeaponState_t | Unsigned | |
DoorState_t | Unsigned | |
ValueRemapperInputType_t | Unsigned | |
ValueRemapperOutputType_t | Unsigned | |
ValueRemapperHapticsType_t | Unsigned | |
ValueRemapperMomentumType_t | Unsigned | |
ValueRempapperRatchetType_t | Unsigned | |
PointWorldTextJustifyHorizontal_t | Unsigned | |
PointWorldTextJustifyVertical_t | Unsigned | |
PointWorldTextReorientMode_t | Unsigned | |
PoseController_FModType_t | Unsigned | |
itemid_t | Unsigned | |
style_index_t | Unsigned | |
attributeprovidertypes_t | Unsigned | |
DamageOptions_t | Unsigned | |
MaterialModifyMode_t | Unsigned | |
CSWeaponMode | Unsigned | |
ESurvivalSpawnTileState | Unsigned | |
SpawnStage_t | Unsigned | |
ESurvivalGameRuleDecision_t | Unsigned | |
RelativeDamagedDirection_t | Unsigned | |
CSPlayerState | Unsigned | |
MedalRank_t | Unsigned | |
CSPlayerBlockingUseAction_t | Unsigned | |
MoveMountingAmount_t | Unsigned | |
QuestProgress::Reason | Unsigned | |
tablet_skin_state_t | Unsigned | |
ScreenEffectType_t | Unsigned | |
char | String | |
CUtlString | String | |
CUtlSymbolLarge | String | |
GameTime_t | NoScale | |
CBodyComponent | Component | |
CPhysicsComponent | Component | |
CLightComponent | Component | |
CRenderComponent | Component |
Lookup Methodology
Mapping a field type to a decoder is done by a lookup method. There are two variants of this:
Lookup Field Decoder
This is the most common use of decoding a field type. It is implemented as such:
- Check factories
- Check overrides
- Check mapping
- Otherwise,
Unsigned
This will be referenced as lookup_field_decoder
Lookup Field Inner Decoder
This is the less often used decoder, are used for decoding the inner child elements of an array.
Unlike Field Type, Base Type is actually the inner GENERIC type,
not the outer type (because it will always be an array)
It is implemented as such:
- Check if we have a generic type.
- Fatally Error if this is not the case.
- Check factories
- Check mapping.
- Otherwise,
Unsigned
Note: we do not need to check overrides in this implementation.
This will be referenced as lookup_field_inner_decoder
Constructing Serializers
By this point you should be able to:
- Parse the send-tables and extract the fields.
- Parse a field's type
- Determine the layout of a field
- Create a field decoder for a given field type.
The next goal is to create a minimal representation of these fields so they can be referenced efficiently.
As stated before, serializers contain an array of fields, which during updates are referenced by indices. We need to map fields to their decoder or nested serializers while keeping the correct field layout. These fields need to be in the same order as they arrive in the send-tables.
We will combine the field layout and the field decoder into a single type called Field.
Representation of a Field
Each field layout can be boiled down to a few simple parameters:
Value
| Parameter | Type | Description |
|---|---|---|
decoder | field_decoder | the decoder for this value |
FixedArray
| Parameter | Type | Description |
|---|---|---|
decoder | field_decoder | the decoder for the values in this array |
count | int | the size of this array (fixed) |
VariableArray
| Parameter | Type | Description |
|---|---|---|
inner_decoder | field_decoder | the decoder for the values in this array |
FixedTable
| Parameter | Type | Description |
|---|---|---|
serializer | *serializer | the serializer of the nested table |
VariableTable
| Parameter | Type | Description |
|---|---|---|
serializer | *serializer | the serializer of the nested table(s) |
PolyTable
| Parameter | Type | Description |
|---|---|---|
serializers | []*serializer | the serializers of the nested table types |
Constructing Fields
Entity Layout
Entity Updates
Entity updates are sent in the CSVCMsg_PacketEntities message.
This message has a few key fields:
| Field | Description |
|---|---|
serializer_name_sym | the symbol index of the serializer name |
serializers | the version of this serializer |
fields | a list of indexes, referring to the fields in this serializer |