Skip to content

Instantly share code, notes, and snippets.

@whutch
Last active December 27, 2015 01:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save whutch/7244195 to your computer and use it in GitHub Desktop.
Save whutch/7244195 to your computer and use it in GitHub Desktop.
Dota 2 replay analysis stuff
import tarrasque
# Mapping of hero dt_key values to their keys in the CombatLogNames string table
hero_clnames = {
# This list is incomplete! I add to it whenever I hit a new hero parsing
"DT_DOTA_Unit_Hero_Abaddon": "npc_dota_hero_abaddon",
"DT_DOTA_Unit_Hero_Alchemist": "npc_dota_hero_alchemist",
"DT_DOTA_Unit_Hero_AncientApparition": "npc_dota_hero_ancient_apparition",
"DT_DOTA_Unit_Hero_Axe": "npc_dota_hero_axe",
"DT_DOTA_Unit_Hero_Bane": "npc_dota_hero_bane",
"DT_DOTA_Unit_Hero_Broodmother": "npc_dota_hero_broodmother",
"DT_DOTA_Unit_Hero_Clinkz": "npc_dota_hero_clinkz",
"DT_DOTA_Unit_Hero_CrystalMaiden": "npc_dota_hero_crystal_maiden",
"DT_DOTA_Unit_Hero_DarkSeer": "npc_dota_hero_dark_seer",
"DT_DOTA_Unit_Hero_DeathProphet": "npc_dota_hero_death_prophet",
"DT_DOTA_Unit_Hero_Furion": "npc_dota_hero_furion",
"DT_DOTA_Unit_Hero_Invoker": "npc_dota_hero_invoker",
"DT_DOTA_Unit_Hero_Juggernaut": "npc_dota_hero_juggernaut",
"DT_DOTA_Unit_Hero_Kunkka": "npc_dota_hero_kunkka",
"DT_DOTA_Unit_Hero_Luna": "npc_dota_hero_luna",
"DT_DOTA_Unit_Hero_Necrolyte": "npc_dota_hero_necrolyte",
"DT_DOTA_Unit_Hero_Nevermore": "npc_dota_hero_nevermore",
"DT_DOTA_Unit_Hero_NightStalker": "npc_dota_hero_night_stalker",
"DT_DOTA_Unit_Hero_Nyx_Assassin": "npc_dota_hero_nyx_assassin",
"DT_DOTA_Unit_Hero_Pudge": "npc_dota_hero_pudge",
"DT_DOTA_Unit_Hero_QueenOfPain": "npc_dota_hero_queenofpain",
"DT_DOTA_Unit_Hero_Rattlestrap": "npc_dota_hero_rattletrap",
"DT_DOTA_Unit_Hero_Rubick": "npc_dota_hero_rubick",
"DT_DOTA_Unit_Hero_SandKing": "npc_dota_hero_sand_king",
"DT_DOTA_Unit_Hero_Shadow_Demon": "npc_dota_hero_shadow_demon",
"DT_DOTA_Unit_Hero_Shredder": "npc_dota_hero_shredder",
"DT_DOTA_Unit_Hero_Slardar": "npc_dota_hero_slardar",
"DT_DOTA_Unit_Hero_Sniper": "npc_dota_hero_sniper",
"DT_DOTA_Unit_Hero_Spectre": "npc_dota_hero_spectre",
"DT_DOTA_Unit_Hero_StormSpirit": "npc_dota_hero_storm_spirit",
"DT_DOTA_Unit_Hero_Tiny": "npc_dota_hero_tiny",
"DT_DOTA_Unit_Hero_VengefulSpirit": "npc_dota_hero_vengefulspirit",
"DT_DOTA_Unit_Hero_Venomancer": "npc_dota_hero_venomancer",
"DT_DOTA_Unit_Hero_Visage": "npc_dota_hero_visage",
"DT_DOTA_Unit_Hero_Windrunner": "npc_dota_hero_windrunner",
}
def dtkey_to_clindex(table, key):
name = hero_clnames[key]
index, _ = table.by_name[name]
return index
def list_clnames(replay):
table = replay.string_tables["CombatLogNames"].by_name.items()
for name, (index, _) in sorted(table):
print "{}: {}".format(name, index)
def count_kills(replay, players = (), targets = ()):
if not players:
print "Player options:"
for player in replay.players:
print "{}: {}".format(player.index, player.hero.name)
return
# We need to jump to the end to get the full string table
print "Jumping to replay end"
replay.go_to_state_change("end")
strings = replay.string_tables["CombatLogNames"]
print "Jumping back to game start"
replay.go_to_state_change("game")
# Build some quick-access data
print "Building tables"
player_by_index = {}
for index in range(2,12):
# Working on the assumption that the player entities will always be
# the third through thirteenth entities in the world index
ent = replay.world.by_ehandle[replay.world.by_index[index]]
player = ent[(u'DT_DOTAPlayer', u'm_iPlayerID')]
if player in players:
player_by_index[index] = player
clindex_by_player = {p.index: dtkey_to_clindex(strings, p.hero.dt_key) \
for p in [replay.players[i] for i in players]}
player_by_clindex = {cli: pi for pi, cli in clindex_by_player.items()}
count = {i:[0,0,0,0] for i in players}
# Cycle through each tick looking for combat log messages
print "Starting scan"
for tick in replay.iter_ticks(end = "postgame"):
found_kill = False
# Track combat messages per player to compare to overhead gold
combat_msgs = {i:[0] for i in players}
gold_msgs = {i:[0,0] for i in players}
for event in replay.game_events:
if not isinstance(event, tarrasque.combatlog.CombatLogMessage):
continue
if event.__dict__["properties"]["type"] != 4:
continue
target = event.__dict__["properties"]["targetname"]
if targets and not target in targets:
continue
source = event.__dict__["properties"]["sourcename"]
if not source in player_by_clindex:
continue
source = player_by_clindex[source]
combat_msgs[source][0] += 1
found_kill = True
print "{}: kill on {} by {}".format(event.__dict__["properties"]["timestamp"], target, source)
if not found_kill:
continue
for _, msg in replay.user_messages:
if msg.DESCRIPTOR.name != "CDOTAUserMsg_OverheadEvent":
continue
if not msg.target_player_entindex in player_by_index:
continue
source = player_by_index[msg.target_player_entindex]
gold_msgs[source][0] += 1
gold_msgs[source][1] += msg.value
# Since you can't easily tell which units caused the overhead gold,
# we'll count how many combat messages we got for this tick for the targets and if it is exactly as
# many as the overhead gold messages, we can be sure that all that gold was for those units, otherwise
# the gold for that tick is "unconfirmed"
for player in players:
kills = combat_msgs[player][0]
gold_sources, gold = gold_msgs[player]
count[player][0] += kills
if kills == gold_sources:
# The gold for this tick is all from our targets
count[player][2] += gold
else:
# This gold is tainted by a source other than our targets
count[player][3] += gold
if gold_sources > kills:
# Keep track of how many unknown gold sources there are
count[player][1] += (gold_sources - kills)
# We made it, but we need to jump back to the game start again so the
# player list is populated
end_time = replay.info.game_time
match_time = end_time - replay.info.game_start_time
print "Jumping back to game start (again)"
replay.go_to_state_change("game")
heroes = {i: replay.players[i].hero.dt_key.rsplit("_", 1)[1] for i in players}
targets = [strings.by_index[i][0] for i in targets]
print "Match length: {}:{}".format(int(match_time / 60), int(match_time % 60))
print "Kill count for {} against {}:".format(", ".join(sorted(heroes.values())), ", ".join(sorted(targets)))
for player in players:
_, name = replay.players[player].hero.dt_key.rsplit("_", 1)
kills, unknown, sure_gold, unsure_gold = count[player]
if not unsure_gold:
print "{}: {} kills, {} gold, {} GPM"\
.format(name, kills, sure_gold, int(sure_gold / (match_time / 60)))
else:
print "{}: {} kills, {}-{} gold ({} unknown sources), {}-{} GPM"\
.format(name, kills, sure_gold, sure_gold + unsure_gold, unknown, \
int(sure_gold / (match_time / 60)), int((sure_gold + unsure_gold) / (match_time / 60)))
'''
replay = tarrasque.StreamBinding.from_file("357574606.dem")
d2replayutils.count_kills(replay, (5,6,7,8,9), (41,50))
Match length: 39:56
Kill count for Bane, Luna, Pudge, Slardar, Tiny against npc_dota_broodmother_spiderite, npc_dota_broodmother_spiderling:
Bane: 3 kills, 44-88 gold (1 unknown sources), 1-2 GPM
Pudge: 31 kills, 467 gold, 11 GPM
Tiny: 58 kills, 656-1318 gold (9 unknown sources), 16-32 GPM
Luna: 53 kills, 740-805 gold (1 unknown sources), 18-20 GPM
Slardar: 17 kills, 174-370 gold (4 unknown sources), 4-9 GPM
replay = tarrasque.StreamBinding.from_file("356643683.dem")
d2replayutils.count_kills(replay, (0,1,2,3,4), (48,77))
Match length: 32:33
Kill count for CrystalMaiden, Nevermore, Shredder, Slardar, Venomancer against npc_dota_broodmother_spiderite, npc_dota_broodmother_spiderling:
Venomancer: 20 kills, 312 gold, 9 GPM
CrystalMaiden: 27 kills, 448 gold, 13 GPM
Shredder: 0 kills, 0 gold, 0 GPM
Slardar: 64 kills, 907-1111 gold (3 unknown sources), 27-34 GPM
Nevermore: 3 kills, 40 gold, 1 GPM
d2replayutils.count_kills(replay, (5,6,7,8,9), (63,95,110,112))
Match length: 32:33
Kill count for Axe, Broodmother, Invoker, Kunkka, SandKing against npc_dota_venomancer_plague_ward_1, npc_dota_venomancer_plague_ward_2, npc_dota_venomancer_plague_ward_3, npc_dota_venomancer_plague_ward_4:
SandKing: 2 kills, 29 gold, 0 GPM
Axe: 1 kills, 17 gold, 0 GPM
Broodmother: 2 kills, 34 gold, 1 GPM
Kunkka: 4 kills, 65 gold, 1 GPM
Invoker: 1 kills, 15 gold, 0 GPM
replay = tarrasque.StreamBinding.from_file("364173292.dem")
d2replayutils.count_kills(replay, (0,1,2,3,4), (111,140,169))
Match length: 51:29
Kill count for AncientApparition, Clinkz, Demon, QueenOfPain, Sniper against npc_dota_visage_familiar1, npc_dota_visage_familiar2, npc_dota_visage_familiar3:
AncientApparition: 1 kills, 100 gold, 1 GPM
QueenOfPain: 4 kills, 400 gold, 7 GPM
Demon: 0 kills, 0 gold, 0 GPM
Sniper: 6 kills, 500 gold, 9 GPM
Clinkz: 1 kills, 100 gold, 1 GPM
replay = tarrasque.StreamBinding.from_file("364206892.dem")
d2replayutils.count_kills(replay, (5,6,7,8,9), (37,53))
Match length: 32:32
Kill count for Abaddon, Alchemist, Assassin, Invoker, StormSpirit against npc_dota_broodmother_spiderite, npc_dota_broodmother_spiderling:
StormSpirit: 40 kills, 360-920 gold (6 unknown sources), 11-28 GPM
Abaddon: 1 kills, 0-12 gold (1 unknown sources), 0-0 GPM
Assassin: 37 kills, 548-1148 gold (2 unknown sources), 16-35 GPM
Invoker: 2 kills, 30 gold, 0 GPM
Alchemist: 36 kills, 504-885 gold (3 unknown sources), 15-27 GPM
'''
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment