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.