The Combat Log¶
The combat log is the primary source of game event data: damage dealt, spells cast, heroes killed, gold earned, items used, buybacks. It does not come from the entity stream — it arrives through its own channels, with two different formats depending on the replay version.
Two ingestion paths¶
S1 path — older replays¶
Combat log events arrive as game events named dota_combatlog, carried inside
CMsgSource1LegacyGameEvent messages (inner message type 45 or 46). Each event is a
typed key-value record where unit and ability names are integer indices rather than
strings. The actual strings live in the CombatLogNames string table (see
String Tables).
Example: a hero kill event in S1 format:
CMsgSource1LegacyGameEvent (event name: dota_combatlog)
type: 6 ← DOTA_COMBATLOG_DEATH
sourcename: 42 ← index into CombatLogNames → "npc_dota_hero_axe"
targetname: 17 ← index → "npc_dota_hero_juggernaut"
inflictor: 0 ← index → "" (direct attack, no ability)
value: 0
attackerhero: 1 ← attacker is a hero
targethero: 1 ← target is a hero
gem resolves the indices against the current CombatLogNames table at event time.
S2 path — modern/HLTV replays¶
Combat log events arrive as inner messages of type 554 (DOTA_UM_CombatLogDataHLTV).
Each message is a CMsgDOTACombatLogEntry protobuf with names already resolved as
strings — no string table lookup needed.
The same kill event in S2 format:
CMsgDOTACombatLogEntry (inner type 554)
type: DOTA_COMBATLOG_DEATH
attacker_name: "npc_dota_hero_axe"
target_name: "npc_dota_hero_juggernaut"
inflictor_name: ""
value: 0
attacker_hero: true
target_hero: true
gem handles both paths and normalises them into the same CombatLogEntry output.
CombatLogEntry¶
After normalisation, every event becomes a CombatLogEntry dataclass:
@dataclass
class CombatLogEntry:
tick: int
log_type: str # see table below
attacker_name: str # "npc_dota_hero_axe"
target_name: str # "npc_dota_hero_juggernaut"
inflictor_name: str # ability/item name, or "" for attacks
value: int # damage amount, gold amount, etc.
attacker_is_hero: bool
target_is_hero: bool
attacker_is_illusion: bool
target_is_illusion: bool
attacker_team: int # 2 = Radiant, 3 = Dire
target_team: int
Log types¶
The log_type field is a string label from DOTA_COMBATLOG_TYPES:
| Type | What it records |
|---|---|
DAMAGE |
Damage dealt — attacker, target, amount, ability |
HEAL |
HP restored — source, target, amount |
MODIFIER_ADD |
A buff or debuff was applied |
MODIFIER_REMOVE |
A buff or debuff expired or was removed |
DEATH |
A unit died — killer and victim |
ABILITY |
An ability was cast |
ITEM |
An item was used (includes ward placements) |
GOLD |
Gold gained or spent, with a reason code in value |
XP |
Experience gained, with a reason code |
PURCHASE |
An item was purchased |
BUYBACK |
A player bought back; value is the player slot |
KILLSTREAK |
A kill streak milestone |
MULTIKILL |
A multi-kill |
GAME_STATE |
Game state transition (game started, ended) |
CONNECT |
Player connected |
DISCONNECT |
Player disconnected |
Derived economy stats¶
Some economy metrics are reconstructed during match assembly rather than read directly from raw combat-log payloads:
- Buyback gold cost is derived in
match_builderusingfloor(200 + net_worth / 13). - Laning efficiency is computed against a fixed 10-minute baseline of
4948gold.
Ward placements¶
Ward placements appear as ITEM events with specific inflictor names:
inflictor_name |
Ward type |
|---|---|
item_ward_observer |
Observer ward placed |
item_ward_dispenser |
Observer ward (dispenser variant) |
item_ward_sentry |
Sentry ward placed |
The combat log records who placed the ward and when, but not where. Coordinates must be obtained from the entity stream — see Guide: Reading Entity State for how.
100% coverage requires:
- Accept all non-DELETED entity events (not just
CREATED) — recycled entity slots emitUPDATEDbut still carry the full position. - Do not globally consume entity records in the matcher — the same slot is reused
across the game. For each combat log
ITEMevent, find the entity event with the smallest tick delta within ±60 ticks, without marking records consumed.
Smoke of Deceit¶
Smoke tracking uses two event types:
ITEMevent withinflictor_name = "item_smoke_of_deceit"— the item was consumed.MODIFIER_ADDevents withinflictor_name = "modifier_smoke_of_deceit"andtarget_is_hero = True— one per hero that received the smoke buff.
Filter MODIFIER_ADD by target_is_hero = True to exclude summoned units (e.g.
Beastmaster boars) from the smoke group.
Empty group edge case: if the activating hero is inside a sentry ward's truesight
radius at activation, the smoke breaks instantly. The ITEM event fires (item consumed)
but zero MODIFIER_ADD events follow. This is correct game behaviour — the item was
wasted — not a parsing bug.
Summon kill attribution¶
When a summoned unit (Warlock Golem, Undying Zombie, Pugna Nether Ward, etc.) kills a
hero, the combat log DEATH event records the summoned unit as attacker_name, not the
summoning hero. To credit the kill to the owning hero:
- Track all summoned unit entities (
CDOTABaseNPCsubclasses with a player owner handle). - Map summoned unit NPC name → owning hero NPC name at CREATED time.
- On
DEATHevents, substitute the owner name if the attacker is a known summon.
gem's CombatAggregator handles this attribution.
Subscribing to the combat log¶
from gem.parser import ReplayParser
parser = ReplayParser("my_replay.dem")
def on_entry(entry):
if entry.log_type == "DEATH" and entry.target_is_hero:
print(f"tick {entry.tick}: {entry.target_name} killed by {entry.attacker_name}")
parser.on_combat_log_entry(on_entry)
parser.parse()
Functional reference¶
Source: src/gem/combatlog.py
Reference: refs/clarity/src/main/java/skadistats/clarity/processor/gameevents/CombatLog.java