A Dota 2 replay parser for Python
The replay, read back as facts.
gem turns Source 2 .dem files into typed Python objects, DataFrames and interactive reports. It reports what happened; the interpretation is yours.
- Offline
- No servers, no API keys
- Old and new
- Both combat-log formats, one schema
- Pure Python
- 3.10 and newer, MIT licensed
01 · Parsing
From replay bytes to a table.
A replay is a stream of protobuf messages and entity updates. gem decodes it into aParsedMatch: players, fights, wards, objectives and the full combat log.
import gem
from gem.constants import hero_display
match = gem.parse("8856501050.dem")
for p in match.players[:5]:
print(hero_display(p.hero_name),
p.kills, p.deaths, p.assists,
p.net_worth, p.gold_per_min)
frames = gem.parse_to_dataframe("8856501050.dem")| Hero | K / D / A | Net worth | GPM |
|---|---|---|---|
| Tiny | 10 / 6 / 12 | 51,761 | 718 |
| Sniper | 11 / 8 / 13 | 60,021 | 773 |
| Timbersaw | 6 / 8 / 20 | 58,262 | 660 |
| Lion | 4 / 15 / 19 | 19,033 | 332 |
| Ancient Apparition | 0 / 7 / 19 | 24,409 | 432 |
match.players.02 · The map
A map model you can check.
Regions and camps are traced on the 7.41 map and checked against the replays' own neutral spawners and combat log. gem.region_of(x, y) tells you where any position is. Wards carry exact positions; gem samples heroes each second, and Figure 3 reads them on every replay packet.
- large
- small
- ancient
- medium
- flooded small
- flooded medium
Observer
Sentry- Radiant
- Dire
Damage per secondRadiant aboveDire belowDrag, or click a ✕ or ↺, to pick a stretch
- Physical
- Magical
- Pure
- Cast, caster to target
- Disabled (the arc is the time left)
- Spell immune
- Hidden from the enemy
- Smoked
03 · Recipes
Questions, answered from the facts.
04 · Where to start
gem is a pure-Python parser. The Go (Manta) and Java (Clarity) parsers are faster; gem aims for Python-native data and an implementation you can read end to end.