Send Tables & the Schema¶
Before any entity data can be decoded, the parser needs a schema that describes every entity class: what fields it has, what types those fields are, and how to decode their binary encoding. That schema is called the send tables.
Where the schema lives¶
A DEM_SendTables outer message (type ID 4) appears exactly once per replay, near the
start of the file and before any entity packets. Its payload is a protobuf
CDemoSendTables message whose data field contains a serialised
CSVCMsg_FlattenedSerializer — the complete class schema for this replay build.
DEM_SendTables (outer message, type 4)
└── CDemoSendTables (protobuf)
└── .data: bytes
└── CSVCMsg_FlattenedSerializer (protobuf, embedded)
├── serializers[0]: name="CBaseAnimatingActivity", version=0
│ └── fields_index: [0, 1, 2, ...]
├── serializers[1]: name="CDOTA_Unit_Hero_Axe", version=0
│ └── fields_index: [0, 3, 7, ...]
└── fields[0..N]: ProtoFlattenedSerializerField_t
├── var_name_sym: symbol index
├── var_type_sym: symbol index
├── field_serializer_name_sym: (if nested type)
├── encoder_sym: (e.g. "coord", "qangle_pitch_yaw")
├── encode_flags: bitmask
├── bit_count: int
├── low_value: float
└── high_value: float
The symbols array in CSVCMsg_FlattenedSerializer maps symbol indices to strings.
All names (var names, type names, encoder names) are stored as symbol indices to avoid
redundant string storage.
gem parses this in src/gem/sendtable.py and builds an in-memory Serializer tree.
Serialisers and fields¶
A serialiser represents one entity class. It has a name (matching the network class name) and a list of fields in order.
A field describes one property of an entity. Key attributes:
| Attribute | Meaning |
|---|---|
var_name |
Field name as it appears in entity state, e.g. m_iHealth |
var_type |
Type string, e.g. int32, float32, CNetworkedQuantizedFloat |
base_type |
Parsed from var_type — the core type name |
encoder |
Optional encoding hint: coord, qangle_pitch_yaw, normal, etc. |
bit_count |
For fixed-width fields: number of bits |
low_value |
Quantized float range minimum |
high_value |
Quantized float range maximum |
field_serializer_name |
For nested types: name of the sub-serialiser |
encode_flags |
Bitmask controlling rounding behaviour for quantized floats |
Field models¶
Each field has one of five models that control how it appears in entity delta packets:
| Model | Meaning |
|---|---|
SIMPLE |
A single scalar value — one path, one decoder call |
FIXED_ARRAY |
A compile-time-fixed-length array, e.g. m_vecAbilities[16] |
FIXED_TABLE |
An embedded sub-serialiser accessed via a pointer field |
VARIABLE_ARRAY |
A runtime-variable-length array; the current length is tracked in the entity state |
VARIABLE_TABLE |
A runtime-variable collection of embedded sub-serialisers |
The model is inferred from the field's type string and whether field_serializer_name is
set. For example:
int32→SIMPLECHandle< CBaseEntity >[16]→FIXED_ARRAYCNetworkUtlVectorBase< CHandle< CBaseEntity > >→VARIABLE_ARRAYCBodyComponent(withfield_serializer_nameset) →FIXED_TABLE
Pointer types¶
Some type names are pointer types — embedded sub-serialisers that are accessed via a pointer field rather than being inlined. Examples:
CBodyComponent, CEntityIdentity, CPhysicsComponent, CRenderComponent,
CDOTAGamerules, CDOTAPlayerController, CDOTATeam
gem identifies these by a fixed list in sendtable.py. When a field's type is a pointer
type, a synthetic boolean field {var_name}.m_bMinimumList is inserted into the
serialiser before the sub-serialiser's fields. This matches how manta/clarity handle the
format.
CDemoClassInfo¶
After DEM_SendTables, a DEM_ClassInfo message (type 5) arrives. It maps:
The network_name is the same string as the serialiser name (e.g.
CDOTA_Unit_Hero_Axe). The class_id is a compact integer used in entity packets to
identify the class without sending the full string name.
gem stores this mapping in EntityManager.classes_by_id so it can look up the
serialiser for any entity in O(1).
Decoder assignment¶
When gem parses the send tables, it calls find_decoder(field) for each field and
stores the result on the field object. Decoders are resolved once at schema-parse
time — not on every entity update. This keeps the hot path (applying thousands of
entity deltas per second) fast.
The decoder assignment logic is in src/gem/field_decoder.py. See
Field Decoders for the dispatch chain.
svc_ServerInfo dependency¶
The CSVCMsg_ServerInfo inner message (type 40) carries two values needed for entity
parsing:
max_classes— the maximum number of entity classes. Used to computeclassIdSize = floor(log2(max_classes)) + 1, the bit width for reading class IDs from entity packets.game_dir— path string containing the game build number, e.g./dota_v9107/.
svc_ServerInfo arrives inside a DEM_SignonPacket before DEM_SendTables.
gem caches it and applies it immediately after the EntityManager is created.
gem API¶
from gem.stream import DemoStream
from gem.sendtable import parse_send_tables
DEM_SEND_TABLES = 4
with DemoStream("my_replay.dem") as stream:
for tick, msg_type, data in stream:
if msg_type == DEM_SEND_TABLES:
serializers = parse_send_tables(data)
break
# Inspect a serialiser
s = serializers["CDOTA_Unit_Hero_Axe"]
for f in s.fields:
print(f.var_name, f.var_type, f.model_name())
Source: src/gem/sendtable.py, src/gem/field_decoder.py