Reading Entity State¶
Entities are the game objects inside a replay: heroes, towers, creeps, the game rules object, runes, wards. Their state changes every tick. This guide shows how to subscribe to entity events and read field values.
For a conceptual explanation of how the entity system works at the binary level, see Understanding: The Entity System.
Subscribing to entity events¶
Register a callback with parser.on_entity(handler) before calling parse().
The callback receives (entity, op) for every entity event in the replay.
from gem.parser import ReplayParser
from gem.entities import EntityOp
parser = ReplayParser("my_replay.dem")
def on_entity(entity, op):
print(entity.get_class_name(), op)
parser.on_entity(on_entity)
parser.parse()
EntityOp flags¶
op is an EntityOp bitmask. Common patterns:
if op & EntityOp.CREATED:
... # entity was just created
if op & EntityOp.UPDATED:
... # one or more fields changed
if op & EntityOp.DELETED:
... # entity was removed
if op & EntityOp.ENTERED:
... # entity became active (accompanies CREATED or a re-activation)
EntityOp.has(other) is equivalent to bool(op & other).
Reading field values¶
Every entity exposes typed getter methods. Field names come from the entity class schema
(e.g. m_iHealth, m_flMana, m_iGold).
hp, ok = entity.get_int32("m_iHealth")
mana, ok = entity.get_float32("m_flMana")
gold, ok = entity.get_uint32("m_iGold")
alive, ok = entity.get_bool("m_bIsAlive")
name, ok = entity.get_string("m_iszUnitName")
All typed getters return (value, ok). ok is True on success, False if the field
does not exist or the value is the wrong type.
For quick untyped access:
Filtering by class name¶
Most callbacks should filter by class name immediately — there are hundreds of entity classes and you usually only care about a few:
def on_entity(entity, op):
name = entity.get_class_name()
if "Hero" not in name:
return
# now work with hero entities only
Common class name patterns:
| Pattern | Matches |
|---|---|
"Hero" in name |
All hero entities |
name.startswith("CDOTA_Unit_Hero_") |
Exact hero entity check |
name == "CDOTAGamerulesProxy" |
Game rules (time, score, state) |
name.startswith("CDOTAPlayerController") |
Per-player state (gold, XP, LH) |
"tower" in name.lower() |
Tower entities |
name == "CDOTA_Item_Observer_Ward" |
Observer ward entities |
name == "CDOTA_Item_Sentry_Ward" |
Sentry ward entities |
Hero position example¶
Hero map position combines two fields: the cell (coarse grid in 512-unit cells) and the vector (fine offset in 0–512 unit range within that cell):
def world_coord(cell: int, vec: float) -> float:
"""Convert cell + vec to world coordinate."""
return cell * 128.0 + vec - 16384.0
def on_entity(entity, op):
if not entity.get_class_name().startswith("CDOTA_Unit_Hero_"):
return
cell_x, ok1 = entity.get_uint32("CBodyComponent.m_cellX")
cell_y, ok2 = entity.get_uint32("CBodyComponent.m_cellY")
vec_x, ok3 = entity.get_float32("CBodyComponent.m_vecX")
vec_y, ok4 = entity.get_float32("CBodyComponent.m_vecY")
if ok1 and ok2 and ok3 and ok4:
x = world_coord(cell_x, vec_x)
y = world_coord(cell_y, vec_y)
print(f"{entity.get_class_name()} at ({x:.0f}, {y:.0f})")
Snapshot at a specific tick¶
To inspect all entities at a fixed point in time, stop parsing at that tick and query the entity manager afterwards:
from gem.parser import ReplayParser
parser = ReplayParser("my_replay.dem")
parser.stop_after_tick(6000) # ~3 minutes into the game
parser.parse()
for entity in parser.entity_manager.all_active():
if entity.get_class_name().startswith("CDOTA_Unit_Hero_"):
hp, _ = entity.get_int32("m_iHealth")
print(f"{entity.get_class_name()}: {hp} HP")
Useful entity classes and fields¶
Hero entity (CDOTA_Unit_Hero_*)¶
| Field | Type | Meaning |
|---|---|---|
m_iHealth |
int32 | Current HP |
m_iMaxHealth |
int32 | Maximum HP |
m_flMana |
float32 | Current mana |
m_flMaxMana |
float32 | Maximum mana |
m_iCurrentLevel |
int32 | Hero level |
CBodyComponent.m_cellX |
uint32 | Map cell X (coarse) |
CBodyComponent.m_cellY |
uint32 | Map cell Y (coarse) |
CBodyComponent.m_vecX |
float32 | Map position X (fine) |
CBodyComponent.m_vecY |
float32 | Map position Y (fine) |
m_hOwnerEntity |
uint32 | Handle to the owning PlayerController |
PlayerController entity (CDOTAPlayerController)¶
| Field | Type | Meaning |
|---|---|---|
m_iGold |
uint32 | Current spendable gold |
m_iLastHitCount |
uint32 | Last hit count |
m_iDenyCount |
uint32 | Deny count |
m_iCurrentLevel |
int32 | Player level |
Game rules (CDOTAGamerulesProxy)¶
| Field | Type | Meaning |
|---|---|---|
CDOTAGamerules.m_fGameTime |
float32 | Current game time in seconds |
CDOTAGamerules.m_nGameState |
uint32 | Game state enum |
CDOTAGamerules.m_iRadiantScore |
uint32 | Radiant kills |
CDOTAGamerules.m_iDireScore |
uint32 | Dire kills |
gem implementation¶
Source: src/gem/entities.py, src/gem/field_state.py
EntityManager.all_active() returns all currently active entities.
EntityManager.get_by_handle(handle) resolves an entity handle.