def parse_replay(file): """ Parse replay with added exceptions and set key-arguments """ try: return s2_parse_replay(file, try_lastest=True, parse_events=False, onlyBlizzard=True, withoutRecoverEnabled=True) except s2protocol.decoders.TruncatedError: return None except Exception: logger.error(f"{file}\n{traceback.format_exc()}") return None
def analyse_replay(filepath, main_player_handles=None): """ Analyses the replay and returns the analysis""" """ This whole function is a bit messy. It originated in a very different form. I should be broken into more functions. Players shouldn't be hardcoded to "main" and "ally" but instead a single datastructure. Which player shows on top should be decided in javascript and not here. """ # Data structure is {unitType : [#created, #died, #kills, #killfraction]} unit_type_dict_main = {} unit_type_dict_ally = {} unit_type_dict_amon = {} logger.info(f'Analysing: {filepath}') # Load the replay replay = None for i in range(2): try: replay = s2_parse_replay(filepath, return_events=True) break except: # You can get an error here if SC2 didn't finish writing into the file. Very rare. logger.error( f'Parsing error ({filepath})\n{traceback.format_exc()}') time.sleep(0.2) if replay == None: return {} # Find Amon players amon_players = set() amon_players.add(3) amon_players.add(4) # Add a list of amon players written for each map for m in amon_player_ids: if m in replay['map_name'] or ('[MM] Lnl' in replay['map_name'] and m == 'Lock & Load'): amon_players = amon_players.union(amon_player_ids[m]) break # Find player names and numbers main_player = 1 main_player_found = False if main_player_handles != None and len( main_player_handles) > 0: # Check if you can find the main player for player in replay['players']: if player['pid'] in {1, 2} and not main_player_found and player[ 'handle'] in main_player_handles: main_player = player['pid'] main_player_found = True break # Ally ally_player = 1 if main_player == 2 else 2 # Start saving data replay_report_dict = dict() replay_report_dict['filepath'] = filepath replay_report_dict['replaydata'] = True replay_report_dict['map'] = replay['map_name'] replay_report_dict['extension'] = replay['extension'] replay_report_dict['B+'] = replay['brutal_plus'] replay_report_dict['result'] = replay['result'] replay_report_dict['main'] = replay['players'][main_player].get( 'name', 'None') replay_report_dict['ally'] = replay['players'][ally_player].get( 'name', 'None') replay_report_dict['mainAPM'] = replay['players'][main_player].get( 'apm', 0) replay_report_dict['allyAPM'] = replay['players'][ally_player].get( 'apm', 0) replay_report_dict['positions'] = { 'main': main_player, 'ally': ally_player } # Difficulty diff_1 = replay['difficulty'][0] diff_2 = replay['difficulty'][1] if diff_1 == diff_2: replay_report_dict['difficulty'] = diff_1 elif main_player == 1: replay_report_dict['difficulty'] = f'{diff_1}/{diff_2}' else: replay_report_dict['difficulty'] = f'{diff_2}/{diff_1}' logger.debug(f'Report dict: {replay_report_dict}') # Events unit_dict = { } # Data structure is {unit_id : [UnitType, Owner]}; used to track all units DT_HT_Ignore = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] # Ignore certain amount of DT/HT deaths after archon is initialized. DT_HT_Ignore[player] killcounts = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] START_TIME = replay['start_time'] END_TIME = replay['end_time'] commander_fallback = dict() outlaw_order = [] wave_units = {'second': 0, 'units': []} identified_waves = dict() mastery_fallback = {1: [0, 0, 0, 0, 0, 0], 2: [0, 0, 0, 0, 0, 0]} killbot_feed = [0, 0, 0] custom_kill_count = dict() UsedMutatorSpiderMines = set() PrestigeTalents = [None, None, None] bonus_timings = list() ResearchVesselLandedTiming = None LastBiomassPosition = [0, 0, 0] AbathurKillLocusts = set() MutatorDehakaDragUnitIDs = set() last_aoe_unit_killed = [0] * 17 for player in range(1, 16): last_aoe_unit_killed[player] = [None, 0 ] if player in amon_players else None for event in replay['events']: # Skip events after the game ended if event['_gameloop'] / 16 > END_TIME: continue if event['_event'] == 'NNet.Replay.Tracker.SUpgradeEvent' and event[ 'm_playerId'] in [1, 2]: _upg_name = event['m_upgradeTypeName'].decode() _upg_pid = event['m_playerId'] # Commander fallback (used for arcade maps) if _upg_name in commander_upgrades: commander_fallback[_upg_pid] = commander_upgrades[_upg_name] # Mastery upgrade fallback (used for arcade maps) mas_commander, mas_index = upgrade_is_in_mastery_upgrades( _upg_name) if mas_commander: logger.debug( f'Player {_upg_pid} (com: {mas_commander}) got upgrade {_upg_name} (idx: {mas_index}) (count: {event["m_count"]})' ) mastery_fallback[_upg_pid][mas_index] = event['m_count'] # Prestige talents _prestige = prestige_talent_name(_upg_name) if _prestige != None: PrestigeTalents[_upg_pid] = _prestige if event['_event'] in [ 'NNet.Replay.Tracker.SUnitBornEvent', 'NNet.Replay.Tracker.SUnitInitEvent' ]: _unit_type = event['m_unitTypeName'].decode() _ability_name = event.get('m_creatorAbilityName', None) _ability_name = _ability_name.decode( ) if _ability_name != None else None _control_pid = event['m_controlPlayerId'] unit_dict[unitid(event)] = [_unit_type, _control_pid] # Certain hero units don't die, instead lets track their revival beacons/cocoons. Let's assume they will finish reviving. if _unit_type in revival_types and _control_pid in [ 1, 2 ] and event['_gameloop'] / 16 > START_TIME + 1: if _control_pid == main_player: unit_type_dict_main[revival_types[_unit_type]][1] += 1 unit_type_dict_main[revival_types[_unit_type]][0] += 1 if _control_pid == ally_player: unit_type_dict_ally[revival_types[_unit_type]][1] += 1 unit_type_dict_ally[revival_types[_unit_type]][0] += 1 # Primal combat fix. For every morph we are substracting two losses from the base unit type if _unit_type in primal_combat_predecessors: logger.debug( f'{_unit_type} substracting from {primal_combat_predecessors[_unit_type]}\n' ) if main_player == _control_pid: unit_type_dict_main[ primal_combat_predecessors[_unit_type]][1] -= 2 if ally_player == _control_pid: unit_type_dict_ally[ primal_combat_predecessors[_unit_type]][1] -= 2 # Save stats for units created if main_player == _control_pid: if _unit_type in unit_type_dict_main: unit_type_dict_main[_unit_type][0] += 1 else: unit_type_dict_main[_unit_type] = [1, 0, 0, 0] if ally_player == _control_pid: if _unit_type in unit_type_dict_ally: unit_type_dict_ally[_unit_type][0] += 1 else: unit_type_dict_ally[_unit_type] = [1, 0, 0, 0] if _control_pid in amon_players: if _ability_name == 'MutatorAmonDehakaDrag': MutatorDehakaDragUnitIDs.add(unitid(event)) elif _unit_type in unit_type_dict_amon: unit_type_dict_amon[_unit_type][0] += 1 else: unit_type_dict_amon[_unit_type] = [1, 0, 0, 0] # Outlaw order if _unit_type in tychus_outlaws and _control_pid in [ 1, 2 ] and not (_unit_type in outlaw_order): outlaw_order.append(_unit_type) # Identifying waves if _control_pid in [3, 4, 5, 6] and event[ '_gameloop'] / 16 > START_TIME + 60 and _unit_type in UnitsInWaves: if wave_units['second'] == event['_gameloop'] / 16: wave_units['units'].append(_unit_type) else: wave_units['second'] = event['_gameloop'] / 16 wave_units['units'] = [_unit_type] if len(wave_units['units']) > 5: identified_waves[event['_gameloop'] / 16] = wave_units['units'] # Abathur biomass for identifying locust if _unit_type == 'BiomassPickup': LastBiomassPosition = [ event['m_x'], event['m_y'], event['_gameloop'] ] if _unit_type == 'Locust' and [ event['m_x'], event['m_y'], event['_gameloop'] ] == LastBiomassPosition: AbathurKillLocusts.add(unitid(event)) # In future ignore some Dark/High Templar deaths caused by Archon merge if event['_event'] == 'NNet.Replay.Tracker.SUnitInitEvent' and event[ 'm_unitTypeName'].decode() == "Archon": DT_HT_Ignore[event['m_controlPlayerId']] += 2 if event[ '_event'] == 'NNet.Replay.Tracker.SUnitTypeChangeEvent' and unitid( event) in unit_dict: _old_unit_type = unit_dict[unitid(event)][0] _control_pid = unit_dict[unitid(event)][1] _unit_type = event['m_unitTypeName'].decode() # Void Launch bonus objective. If it lands and soon-ish after takes off, the bonus is complete. if _control_pid == 7 and _unit_type == 'ResearchVesselLanded': ResearchVesselLandedTiming = event['_gameloop'] if _control_pid == 7 and _unit_type == 'ResearchVessel' and ResearchVesselLandedTiming != None and ( ResearchVesselLandedTiming + 1100 > event['_gameloop']): bonus_timings.append(event['_gameloop'] / 16 - START_TIME) ResearchVesselLandedTiming = None # Scythe of Amon bonus objective. If it changes to WarpPrismPhasing, the bonus is completed. if 'Scythe of Amon' in replay[ 'map_name'] and _control_pid == 11 and _unit_type == 'WarpPrismPhasing': bonus_timings.append(event['_gameloop'] / 16 - START_TIME) # Strange. Some units morph to egg, then the morph is created, then the egg morphs back and the unit is killed if _unit_type in units_killed_in_morph: continue # Update unit_dict unit_dict[unitid(event)][0] = _unit_type # Add to created units if _unit_type in UnitNameDict and _old_unit_type in UnitNameDict: if UnitNameDict[_unit_type] != UnitNameDict[ _old_unit_type]: #don't add into created units if it's just a morph # Increase unit type created for controlling player if main_player == _control_pid: if _unit_type in unit_type_dict_main: unit_type_dict_main[_unit_type][0] += 1 else: unit_type_dict_main[_unit_type] = [1, 0, 0, 0] if ally_player == _control_pid: if _unit_type in unit_type_dict_ally: unit_type_dict_ally[_unit_type][0] += 1 else: unit_type_dict_ally[_unit_type] = [1, 0, 0, 0] if _control_pid in amon_players: if _unit_type in unit_type_dict_amon: unit_type_dict_amon[_unit_type][0] += 1 else: unit_type_dict_amon[_unit_type] = [1, 0, 0, 0] else: if main_player == _control_pid and not ( _unit_type in unit_type_dict_main): unit_type_dict_main[_unit_type] = [0, 0, 0, 0] if ally_player == _control_pid and not ( _unit_type in unit_type_dict_ally): unit_type_dict_ally[_unit_type] = [0, 0, 0, 0] if _control_pid in amon_players and not ( _unit_type in unit_type_dict_amon): unit_type_dict_amon[_unit_type] = [0, 0, 0, 0] # Update ownership if event[ '_event'] == 'NNet.Replay.Tracker.SUnitOwnerChangeEvent' and unitid( event) in unit_dict: unit_dict[unitid(event)][1] = event['m_controlPlayerId'] # Malwarfare bonus objective. If it changes, the bonus is completed. if 'Malwarfare' in replay['map_name'] and event[ 'm_controlPlayerId'] == 6: bonus_timings.append(event['_gameloop'] / 16 - START_TIME) # Update some kill stats if event['_event'] == 'NNet.Replay.Tracker.SUnitDiedEvent': try: _killed_unit_type = unit_dict[unitid(event)][0] _losing_player = unit_dict[unitid(event)][1] _killing_player = event['m_killerPlayerId'] # Count kills for players if _losing_player != _killing_player and not _killed_unit_type in [ 'FuelCellPickupUnit', 'ForceField' ] and not (_killing_player in [1, 2] and _losing_player in [ 1, 2 ]) and _killing_player != None: # Don't count team kills killcounts[_killing_player] += 1 # Get last_aoe_unit_killed (used when player units die without a killing unit, it was likely some enemy caster casting persistent AoE spell) if _killed_unit_type in aoe_units and _killing_player in [ 1, 2 ] and _losing_player in amon_players and unitid(event) != None: last_aoe_unit_killed[_losing_player] = [ _killed_unit_type, event['_gameloop'] / 16 ] except: logger.error(traceback.format_exc()) # More kill stats if event['_event'] == 'NNet.Replay.Tracker.SUnitDiedEvent' and unitid( event) in unit_dict: try: _killing_unit_id = unitid(event, killer=True) _killing_player = event['m_killerPlayerId'] _killed_unit_type = unit_dict[unitid(event)][0] _losing_player = int(unit_dict[unitid(event)][1]) _commander = commander_fallback.get(_killing_player, None) # Get killing unit if _killing_unit_id in unit_dict and unitid( event) != None: # We have a killing unit _killing_unit_type = unit_dict[_killing_unit_id][0] else: """ For no-unit, check if we default to some commander no-unit like airstrike, or use 'NoUnit' But lets use this only rarely. Units killed in transports count for this as well. Other not counted sources: Dusk Wings lifting off, CoD explosion, ... """ _killing_unit_type = commander_no_units.get( _commander, 'NoUnit') # Killbot feed if _killing_unit_type in [ 'MutatorKillBot', 'MutatorDeathBot', 'MutatorMurderBot' ] and _losing_player in [1, 2]: killbot_feed[_losing_player] += 1 # Abathur locusts if _killing_unit_type == 'Locust' and _commander == 'Abathur' and not _killing_unit_id in AbathurKillLocusts: _killing_unit_type = 'SwarmHost' # Custom kill count if _killing_player in [1, 2 ] and _losing_player in amon_players: if _killed_unit_type in HFTS_Units: if not 'hfts' in custom_kill_count: custom_kill_count['hfts'] = {1: 0, 2: 0} custom_kill_count['hfts'][_killing_player] += 1 if _killed_unit_type in TUS_Units: if not 'tus' in custom_kill_count: custom_kill_count['tus'] = {1: 0, 2: 0} custom_kill_count['tus'][_killing_player] += 1 elif _killed_unit_type == 'MutatorPropagator': if not 'propagators' in custom_kill_count: custom_kill_count['propagators'] = {1: 0, 2: 0} custom_kill_count['propagators'][_killing_player] += 1 elif _killed_unit_type in [ 'MutatorSpiderMine', 'MutatorSpiderMineBurrowed', 'WidowMineBurrowed', 'WidowMine' ]: if not 'minesweeper' in custom_kill_count: custom_kill_count['minesweeper'] = {1: 0, 2: 0} custom_kill_count['minesweeper'][_killing_player] += 1 elif _killed_unit_type == 'MutatorVoidRift': if not 'voidrifts' in custom_kill_count: custom_kill_count['voidrifts'] = {1: 0, 2: 0} custom_kill_count['voidrifts'][_killing_player] += 1 elif _killed_unit_type in [ 'MutatorTurkey', 'MutatorTurking', 'MutatorInfestedTurkey' ]: if not 'turkey' in custom_kill_count: custom_kill_count['turkey'] = {1: 0, 2: 0} custom_kill_count['turkey'][_killing_player] += 1 elif _killed_unit_type == 'MutatorVoidReanimator': if not 'voidreanimators' in custom_kill_count: custom_kill_count['voidreanimators'] = {1: 0, 2: 0} custom_kill_count['voidreanimators'][ _killing_player] += 1 elif _killed_unit_type in [ 'InfestableBiodome', 'JarbanInfestibleColonistHut', 'InfestedMercHaven', 'InfestableHut' ]: if not 'deadofnight' in custom_kill_count: custom_kill_count['deadofnight'] = {1: 0, 2: 0} custom_kill_count['deadofnight'][_killing_player] += 1 # If an enemy mutator spider mine kills something, counts a kill for the first player who lost a unit to it if _losing_player in [1, 2 ] and _killing_player in amon_players: if _killing_unit_type == 'MutatorSpiderMine' and not _killing_unit_id in UsedMutatorSpiderMines: UsedMutatorSpiderMines.add( _killing_unit_id ) # Count each mutator spider mine only once if not 'minesweeper' in custom_kill_count: custom_kill_count['minesweeper'] = {1: 0, 2: 0} custom_kill_count['minesweeper'][_losing_player] += 1 """Fix kills for some enemy area-of-effect units that kills player units after they are dead. This is the best guess, if one of aoe_units died recently, it was likely that one. """ if _killing_unit_type == 'NoUnit' and _killing_unit_id == None and _killing_player in amon_players and _losing_player != _killing_player: if event['_gameloop'] / 16 - last_aoe_unit_killed[ _killing_player][1] < 9 and last_aoe_unit_killed[ _killing_player][0] != None: unit_type_dict_amon[ last_aoe_unit_killed[_killing_player][0]][2] += 1 logger.debug( f'{last_aoe_unit_killed[_killing_player][0]}({_killing_player}) killed {_killed_unit_type} | {event["_gameloop"]/16}s' ) # Update unit kill stats if ( (_killing_unit_id in unit_dict) or _killing_unit_type in commander_no_units.values() ) and ( _killing_unit_id != unitid(event) ) and _losing_player != _killing_player and _killed_unit_type != 'FuelCellPickupUnit': if main_player == _killing_player: if _killing_unit_type in unit_type_dict_main: unit_type_dict_main[_killing_unit_type][2] += 1 else: unit_type_dict_main[_killing_unit_type] = [ 0, 0, 1, 0 ] if ally_player == _killing_player: if _killing_unit_type in unit_type_dict_ally: unit_type_dict_ally[_killing_unit_type][2] += 1 else: unit_type_dict_ally[_killing_unit_type] = [ 0, 0, 1, 0 ] if _killing_player in amon_players and not _losing_player in amon_players: if _killing_unit_type in unit_type_dict_amon: unit_type_dict_amon[_killing_unit_type][2] += 1 else: unit_type_dict_amon[_killing_unit_type] = [ 0, 0, 1, 0 ] # Debug for player no units kills if _killed_unit_type not in { 'Scarab', 'Interceptor' } and not _killing_unit_id in unit_dict and not _killing_unit_type in commander_no_units.values( ) and _killing_player in { 1, 2 } and _losing_player != _killing_player: logger.debug( f'{_killing_unit_type} ({_killing_player}|{commander_fallback.get(_killing_player,"")}) killed {_killed_unit_type} ({_losing_player}) - {event["m_x"]}x{event["m_y"]} - {event["_gameloop"]/16:.1f}s ' ) # Update unit death stats # Don't count self kills like Fenix switching suits if _killed_unit_type in self_killing_units and _killing_player == None: if main_player == _losing_player: unit_type_dict_main[_killed_unit_type][0] -= 1 if ally_player == _losing_player: unit_type_dict_ally[_killed_unit_type][0] -= 1 continue # Fix for units like Raptorlings that are counted each time they jump (as death and birth) if event[ '_gameloop'] / 16 > 0 and _killed_unit_type in duplicating_units and _killed_unit_type == _killing_unit_type and _losing_player == _killing_player: if main_player == _losing_player: unit_type_dict_main[_killed_unit_type][0] -= 1 continue if ally_player == _losing_player: unit_type_dict_ally[_killed_unit_type][0] -= 1 continue if _killing_player in amon_players: unit_type_dict_amon[_killed_unit_type][0] -= 1 continue # In case of death caused by Archon merge, ignore these kills if (_killed_unit_type == 'HighTemplar' or _killed_unit_type == 'DarkTemplar') and DT_HT_Ignore[_losing_player] > 0: DT_HT_Ignore[_losing_player] -= 1 continue """ Bonus objectives Mostly kills here. Sometimes checking killing player to prevent despawns from counting. In case of trains, killing player is almost always None, and so I'm checking the position of the train. """ if ('Void Thrashing' in replay['map_name'] and _killed_unit_type in {'ArchAngelCoopFighter','ArchAngelCoopAssault'} and _losing_player == 5) or \ ('Dead of Night' in replay['map_name'] and 'ACVirophage' == _killed_unit_type and _losing_player == 7 and _killing_player in {1,2}) or \ (('Lock & Load' in replay['map_name'] or '[MM] LnL' in replay['map_name']) and 'XelNagaConstruct' == _killed_unit_type and _losing_player == 3) or \ ('Chain of Ascension' in replay['map_name'] and 'SlaynElemental' == _killed_unit_type and _losing_player == 10 and _killing_player in {1,2}) or \ ('Rifts to Korhal' in replay['map_name'] and 'ACPirateCapitalShip' == _killed_unit_type and _losing_player == 8 and _killing_player in {1,2}) or \ ('Cradle of Death' in replay['map_name'] and 'LogisticsHeadquarters' == _killed_unit_type and _losing_player == 3) or \ ('Part and Parcel' in replay['map_name'] and _killed_unit_type in {'Caboose','TarsonisEngine'} and not round(event['_gameloop']/16 - START_TIME,0) in bonus_timings and _losing_player == 8 and not (event['m_x'] == 169 and event['m_y'] == 99) and not (event['m_x'] == 38 and event['m_y'] == 178)) or \ ('Oblivion Express' in replay['map_name'] and 'TarsonisEngineFast' == _killed_unit_type and _losing_player == 7 and event['m_x'] < 196) or \ ('Mist Opportunities' in replay['map_name'] and 'COOPTerrazineTank' == _killed_unit_type and _losing_player == 3 and _killing_player in {1,2}) or \ ('The Vermillion Problem' in replay['map_name'] and _killed_unit_type in {'RedstoneSalamander','RedstoneSalamanderBurrowed'} and _losing_player == 9 and _killing_player in {1,2}) or \ ('Miner Evacuation' in replay['map_name'] and _killed_unit_type == 'Blightbringer' and _losing_player == 5 and _killing_player in {1,2}) or \ ('Miner Evacuation' in replay['map_name'] and _killed_unit_type == 'NovaEradicator' and _losing_player == 9 and unit_type_dict_amon[_killed_unit_type][1] == 1 and _killing_player in {1,2}) or \ ('Temple of the Past' in replay['map_name'] and 'ZenithStone' == _killed_unit_type and _losing_player == 8): # Time offset for Cradle of Death as the explosion is delayed if 'Cradle of Death' in replay['map_name']: bonus_timings.append( round(event['_gameloop'] / 16 - START_TIME - 8, 0)) else: bonus_timings.append( round(event['_gameloop'] / 16 - START_TIME, 0)) logger.debug( f'-------------\nBO: {_killed_unit_type} ({_losing_player}) killed by {_killing_player} ({event["_gameloop"]/16/60:.2f})min\n{event}\n-------------' ) # Add deaths if main_player == _losing_player and event[ '_gameloop'] / 16 > 0 and event[ '_gameloop'] / 16 > START_TIME + 1: # Don't count deaths on game init if _killed_unit_type in unit_type_dict_main: unit_type_dict_main[_killed_unit_type][1] += 1 else: unit_type_dict_main[_killed_unit_type] = [0, 1, 0, 0] if ally_player == _losing_player and event[ '_gameloop'] / 16 > 0 and event[ '_gameloop'] / 16 > START_TIME + 1: if _killed_unit_type in unit_type_dict_ally: unit_type_dict_ally[_killed_unit_type][1] += 1 else: unit_type_dict_ally[_killed_unit_type] = [0, 1, 0, 0] if _losing_player in amon_players and event[ '_gameloop'] / 16 > 0 and event[ '_gameloop'] / 16 > START_TIME + 1 and not unitid( event) in MutatorDehakaDragUnitIDs: if _killed_unit_type in unit_type_dict_amon: unit_type_dict_amon[_killed_unit_type][1] += 1 else: unit_type_dict_amon[_killed_unit_type] = [0, 1, 0, 0] except: logger.error(traceback.format_exc()) # pprint(unit_type_dict_main) # pprint(unit_type_dict_ally) # pprint(unit_type_dict_amon) # logger.info(f'Kill counts: {killcounts}') # Save more data to final dictionary replay_report_dict['bonus'] = [ time.strftime('%M:%S', time.gmtime(i)) for i in bonus_timings ] replay_report_dict['comp'] = get_enemy_comp(identified_waves) replay_report_dict['length'] = replay['accurate_length'] / 1.4 del replay['events'] replay_report_dict['parser'] = replay # Main player replay_report_dict['mainCommander'] = replay['players'][main_player].get( 'commander', '') replay_report_dict['mainCommanderLevel'] = replay['players'][ main_player].get('commander_level', 0) replay_report_dict['mainMasteries'] = replay['players'][main_player].get( 'masteries', [0, 0, 0, 0, 0, 0]) replay_report_dict['mainkills'] = killcounts[main_player] replay_report_dict['mainIcons'] = dict() replay_report_dict['mainPrestige'] = PrestigeTalents[main_player] # Ally player replay_report_dict['allyCommander'] = replay['players'][ally_player].get( 'commander', '') replay_report_dict['allyCommanderLevel'] = replay['players'][ ally_player].get('commander_level', 0) replay_report_dict['allyMasteries'] = replay['players'][ally_player].get( 'masteries', [0, 0, 0, 0, 0, 0]) replay_report_dict['allykills'] = killcounts[ally_player] replay_report_dict['allyIcons'] = dict() replay_report_dict['allyPrestige'] = PrestigeTalents[ally_player] # Fallback if replay_report_dict['mainCommander'] == '': replay_report_dict['mainCommander'] = commander_fallback.get( main_player, "") if replay_report_dict['allyCommander'] == '': replay_report_dict['allyCommander'] = commander_fallback.get( ally_player, "") # MM mastery fallback if '[MM]' in filepath: replay_report_dict['mainMasteries'] = mastery_fallback[main_player] replay_report_dict['allyMasteries'] = mastery_fallback[ally_player] # Assume commander level of 15 replay_report_dict['mainCommanderLevel'] = 15 replay_report_dict['allyCommanderLevel'] = 15 # Icons: Tychus' Outlaws if replay_report_dict.get('mainCommander', None) == 'Tychus': replay_report_dict['mainIcons']['outlaws'] = outlaw_order elif replay_report_dict.get('allyCommander', None) == 'Tychus': replay_report_dict['allyIcons']['outlaws'] = outlaw_order # Icons: Kills # First decide between hfts and tus if 'tus' in custom_kill_count and 'hfts' in custom_kill_count: custom_kill_count['tus'][1] += custom_kill_count['hfts'][1] custom_kill_count['tus'][2] += custom_kill_count['hfts'][2] del custom_kill_count['hfts'] for item in [ 'hfts', 'tus', 'propagators', 'voidrifts', 'turkey', 'voidreanimators', 'deadofnight', 'minesweeper' ]: if item in custom_kill_count: # Skip if it's not a Dead of Night map if item == 'deadofnight' and not 'Dead of Night' in replay[ 'map_name']: continue # Skip if just mines died, not a mutation if item == 'minesweeper' and not 'MutatorSpiderMine' in unit_type_dict_amon: continue replay_report_dict['mainIcons'][item] = custom_kill_count[item][ main_player] replay_report_dict['allyIcons'][item] = custom_kill_count[item][ ally_player] # Don't post if no commander, likely non-coop replay if replay_report_dict['mainCommander'] == '': logger.error('Not a Co-op replay') def fill_unit_kills_and_icons(playername, player, pdict, percent_cutoff=0): """ Fills units into output dictionary, fill created/list units into icons """ new_dict = switch_names(pdict) sorted_dict = { k: v for k, v in sorted( new_dict.items(), reverse=True, key=lambda item: item[1][2]) } # Sorts by number of create (0), lost (1), kills (2), K/D (3) # Save data unitkey = 'mainUnits' if player == main_player else 'allyUnits' iconkey = 'mainIcons' if player == main_player else 'allyIcons' replay_report_dict[unitkey] = dict() player_kill_count = killcounts[player] if killcounts[ player] != 0 else 1 # Prevent division by zero for unit in sorted_dict: if (sorted_dict[unit][2] / player_kill_count > percent_cutoff): replay_report_dict[unitkey][unit] = sorted_dict[unit] replay_report_dict[unitkey][unit][3] = round( replay_report_dict[unitkey][unit][2] / player_kill_count, 2) # Copy created & lost stats from Dehaka if unit == 'Zweihaka' and 'Dehaka' in sorted_dict: replay_report_dict[unitkey][unit][0] = sorted_dict[ 'Dehaka'][0] replay_report_dict[unitkey][unit][1] = sorted_dict[ 'Dehaka'][1] if unit in dont_show_created_lost: replay_report_dict[unitkey][unit][0] = '-' replay_report_dict[unitkey][unit][1] = '-' # Icons: Created units if unit in icon_units: replay_report_dict[iconkey][unit] = sorted_dict[unit][0] # Icons: Artifacts and used projections Zeratul_artifacts_collected = 0 for unit in pdict: if unit in [ 'ZeratulArtifactPickup1', 'ZeratulArtifactPickup2', 'ZeratulArtifactPickup3', 'ZeratulArtifactPickupUnlimited' ]: Zeratul_artifacts_collected += pdict[unit][1] if unit in [ 'ZeratulKhaydarinMonolithProjection', 'ZeratulPhotonCannonProjection' ]: if 'ShadeProjection' in replay_report_dict[iconkey]: replay_report_dict[iconkey]['ShadeProjection'] += pdict[ unit][0] else: replay_report_dict[iconkey]['ShadeProjection'] = pdict[ unit][0] if Zeratul_artifacts_collected > 0: replay_report_dict[iconkey][ 'Artifact'] = Zeratul_artifacts_collected fill_unit_kills_and_icons(replay_report_dict['main'], main_player, unit_type_dict_main) fill_unit_kills_and_icons(replay_report_dict['ally'], ally_player, unit_type_dict_ally) # Icons: Killbots if killbot_feed[main_player] > 0: replay_report_dict['mainIcons']['killbots'] = killbot_feed[main_player] if killbot_feed[ally_player] > 0: replay_report_dict['allyIcons']['killbots'] = killbot_feed[ally_player] # Amon kills sorted_amon = switch_names(unit_type_dict_amon) sorted_amon = { k: v for k, v in sorted( sorted_amon.items(), reverse=True, key=lambda item: item[1][0]) } sorted_amon = { k: v for k, v in sorted( sorted_amon.items(), reverse=True, key=lambda item: item[1][2]) } # Get total amount of kills from Amon total_amon_kills = 0 for player in amon_players: total_amon_kills += killcounts[player] total_amon_kills = total_amon_kills if total_amon_kills != 0 else 1 # Calculate Amon unit percentages and fill data replay_report_dict['amonUnits'] = dict() for unit in sorted_amon: if not contains_skip_strings(unit): replay_report_dict['amonUnits'][unit] = sorted_amon[unit] replay_report_dict['amonUnits'][unit][3] = round( replay_report_dict['amonUnits'][unit][2] / total_amon_kills, 2) return replay_report_dict