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.

Make sure you skip these!

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.

FieldDescription
CommandThe type of frame payload
TickThe tick at which this frame occurs
LengthThe 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.

Source

Implementation

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:

CommandValueDescription
DEM_Stop0End of Demo Frames
DEM_FileHeader1A header, describe the server name, map, build version, etc..
DEM_FileInfo2A "footer", often at the end of the demo containing playback information
DEM_SyncTick3?
DEM_SendTables4Serializers for Entity Data (We'll discuss this later)
DEM_ClassInfo5Entity Class names and IDs mapping to serializers (We'll discuss this later)
DEM_StringTables 6String tables (We'll discuss this later)
DEM_Packet7Game Packet
DEM_SignonPacket 8Game Packet, but for the server->client init
DEM_ConsoleCmd9A Server-side console command
DEM_UserCmd12A Client-side user-action, include sub-tick button and movement data
DEM_FullPacket13Game Packet, but it has string-tables

Defined in the protobuf, There are two variants that are not valid commands.

CommandValueDescription
DEM_Max18the max (N < MAX) command value
DEM_IsCompressed64a flag denoting if the frame is compressed

These represent two things:

  1. The range of commands is 0..18 exclusive.
  2. 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.

Be aware that the snappy compression format and the snappy frame format are two different things; Demos use the former. Make sure your library supports it and your using the correct version.

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:

CommandPayload MessageNotes
DEM_StopCDemoStopEmpty
DEM_FileHeaderCDemoFileHeader
DEM_FileInfoCDemoFileInfo
DEM_SyncTickCDemoSyncTickEmpty
DEM_SendTablesCDemoSendTablesBytes of another protobuf message
DEM_ClassInfoCDemoClassInfo
DEM_StringTablesCDemoStringTables
DEM_PacketCDemoPacket
DEM_SignonPacketCDemoPacket
DEM_ConsoleCmdCDemoConsoleCmd
DEM_UserCmdCDemoUserCmd
DEM_FullPacketCDemoFullPacketCDemoStringTables + CDemoPacket

Next Steps

That's it for demo frames, now we can parse them into packets.