The Entity System¶
The entity system is the core of a Dota 2 replay. Every game object — heroes, towers, creeps, the game rules, runes, wards, couriers — is an entity. Their state is tracked at every tick through an incremental delta system driven by the schema described in Send Tables.
What is an entity?¶
An entity is any game object with addressable state. Examples:
CDOTA_Unit_Hero_Axe— Axe hero: HP, mana, level, position, items, abilitiesCDOTAGamerulesProxy— game rules: game time, game state, score, Roshan timerCDOTA_NPC_Dota_Tower— a tower: team, HP, kill statusCDOTA_Item_Observer_Ward— a placed observer ward: position, owner, expiryCDOTAPlayerController— per-player state: gold, XP, last hits, denies
At any tick there can be hundreds of active entities. Each entity has dozens to hundreds of fields.
Entity indices and serials¶
Entities are stored in slots numbered 0–16383. Each slot has an associated serial number — a 17-bit counter that increments when a new entity occupies the slot. This prevents stale handles from referencing a new entity in the same slot.
A handle combines both:
svc_PacketEntities¶
All entity updates for a tick arrive in one svc_PacketEntities inner message
(type 55). The payload contains a bitstream with updated_entries entity records.
Reading one entity record¶
1. Read index delta:
ubit_var index_delta (relative to previous entity index, starting from -1)
entity_index = previous_index + index_delta + 1
2. Read 2-bit command:
bits 0b00 → UPDATE (entity exists, delta follows)
bits 0b10 → CREATE (entity is being created)
bits 0b01 → LEAVE (entity leaves PVS / becomes inactive)
bits 0b11 → DELETE (entity is deleted; no field delta)
3. If CREATE:
class_id ← classIdSize bits (classIdSize = floor(log2(max_classes)) + 1)
serial ← 17 bits
padding ← varuint32 (discard)
Apply instancebaseline for this class_id
Apply the packet's own delta on top
4. If UPDATE or CREATE (after baseline):
read_fields(reader, serializer, entity.field_state)
5. Fire EntityTracker callbacks with the entity and its EntityOp flags
FieldState¶
Entity field values are stored in a FieldState — a nested list tree that mirrors the
serialiser hierarchy. Accessing a field requires navigating the tree with a field path:
# Conceptually:
field_state.set([3, 0], 128) # set CBodyComponent.m_cellX to 128
field_state.get([3, 0]) # returns 128
The nesting reflects sub-serialisers:
FieldState (root)
├── [0] m_iTeamNum → 2
├── [1] m_iHealth → 1250
├── [2] m_flMana → 480.0
└── [3] CBodyComponent → [sub-list]
├── [0] m_cellX → 128
├── [1] m_cellY → 95
└── [2] m_vecX → -3.25
FieldState is implemented in src/gem/field_state.py.
EntityOp flags¶
When a callback fires, it receives an EntityOp bitmask describing what happened:
| Flag | Meaning |
|---|---|
CREATED |
Entity was just created this tick |
UPDATED |
One or more fields changed |
DELETED |
Entity was removed |
ENTERED |
Entity entered the PVS (accompanies CREATED or a returning UPDATED) |
LEFT |
Entity left the PVS |
EntityOp is an IntFlag, so you can test combinations:
Entity lifecycle¶
Tick N: CREATE entity #42 (class: CDOTA_Unit_Hero_Axe)
→ op = CREATED | ENTERED
→ fields set from baseline, then delta applied
Tick N+5: UPDATE entity #42
→ op = UPDATED
→ delta applied (only changed fields)
Tick N+10: UPDATE entity #42, entity moves out of PVS
→ op = UPDATED | LEFT
...later...
Tick M: entity #42 slot reused by a new entity (different serial)
→ op = CREATED | ENTERED
→ old state discarded, new baseline applied
Reading field values¶
The Entity class exposes typed getter methods:
hp, ok = entity.get_int32("m_iHealth")
mana, ok = entity.get_float32("m_flMana")
gold, ok = entity.get_uint32("m_iGold")
name, ok = entity.get_string("m_iszUnitName")
alive, ok = entity.get_bool("m_bIsAlive")
# Untyped (returns None if not found)
val = entity.get("m_iHealth")
All typed getters return a (value, bool_ok) tuple. ok is True on success,
False if the field does not exist or has a mismatched type.
Nested field access uses dot notation in the name string:
Subscribing to entity events¶
from gem.parser import ReplayParser
from gem.entities import EntityOp
parser = ReplayParser("my_replay.dem")
def on_entity(entity, op):
if "Hero" not in entity.get_class_name():
return
if op & EntityOp.CREATED:
hp, _ = entity.get_int32("m_iHealth")
print(f"Hero created: {entity.get_class_name()} HP={hp}")
parser.on_entity(on_entity)
parser.parse()
Multiple handlers can be registered. All are called for every entity event.
Full decode pipeline summary¶
DEM_SendTables (once)
→ parse CSVCMsg_FlattenedSerializer
→ build Serializer tree + assign decoders to every field
DEM_ClassInfo (once)
→ map class_id → network_name → Serializer
svc_CreateStringTable "instancebaseline" (once, early)
→ store baseline bytes per class_id
svc_PacketEntities (every tick, ~49,000 times per replay)
→ for each of updated_entries entities:
read index delta → entity_index
read 2-bit command
if CREATE:
read class_id, serial
decode baseline bytes → apply to entity FieldState
read field paths (Huffman) → list of (path, Field) pairs
for each (path, Field):
call field.decoder(reader) → value
store value in entity.field_state at path
fire EntityTracker callbacks with (entity, EntityOp)
Functional reference¶
Source: src/gem/entities.py, src/gem/field_reader.py, src/gem/field_state.py
Reference: refs/manta/entity.go