Last active
December 27, 2015 01:19
-
-
Save whutch/7244195 to your computer and use it in GitHub Desktop.
Dota 2 replay analysis stuff
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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