class Replay(Resource): #: A nested dictionary of player => { attr_name : attr_value } for #: known attributes. Player 16 represents the global context and #: contains attributes like game speed. attributes = defaultdict(dict) #: Fully qualified filename of the replay file represented. filename = str() #: Total number of frames in this game at 16 frames per second. frames = int() #: The SCII client build number build = int() #: The SCII game engine build number base_build = int() #: The full version release string as seen on Battle.net release_string = str() #: A tuple of the individual pieces of the release string versions = tuple() #: The game speed: Slower, Slow, Normal, Fast, Faster speed = str() #: Deprecated, use :attr:`game_type` or :attr:`real_type` instead type = str() #: The game type choosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA game_type = str() #: The real type of the replay as observed by counting players on teams. #: For outmatched games, the smaller team numbers come first. #: Example Values: 1v1, 2v2, 3v3, FFA, 2v4, etc. real_type = str() #: The category of the game, Ladder and Private category = str() #: A flag for public ladder games is_ladder = bool() #: A flag for private non-ladder games is_private = bool() #: The raw hash name of the s2ma resource as hosted on bnet depots map_hash = str() #: The name of the map the game was played on map_name = str() #: A reference to the loaded :class:`Map` resource. map = None #: The UTC time (according to the client NOT the server) that the game #: was ended as represented by the Windows OS windows_timestamp = int() #: The UTC time (according to the client NOT the server) that the game #: was ended as represented by the Unix OS unix_timestamp = int() #: The time zone adjustment for the time zone registered on the local #: computer that recorded this replay. time_zone = int() #: Deprecated: See `end_time` below. date = None #: A datetime object representing the utc time at the end of the game. end_time = None #: A datetime object representing the utc time at the start of the game start_time = None #: Deprecated: See `game_length` below. length = None #: The :class:`Length` of the replay as an alternative to :attr:`frames` game_length = None #: The :class:`Length` of the replay in real time adjusted for the game speed real_length = None #: The gateway the game was played on: us, eu, sea, etc gateway = str() #: An integrated list of all the game events events = list() #: A list of :class:`Team` objects from the game teams = list() #: A dict mapping team number to :class:`Team` object team = dict() #: A list of :class:`Player` objects from the game players = list() #: A dual key dict mapping player names and numbers to #: :class:`Player` objects player = utils.PersonDict() #: A list of :class:`Observer` objects from the game observers = list() #: A list of :class:`Person` objects from the game representing #: both the player and observer lists people = list() #: A dual key dict mapping :class:`Person` object to their #: person id's and names person = utils.PersonDict() #: A list of :class:`Person` objects from the game representing #: only the human players from the :attr:`people` list humans = list() #: A list of :class:`Computer` objects from the game. computers = list() #: A list of all the chat message events from the game messages = list() #: A list of pings sent by all the different people in the game pings = list() #: A list of packets sent between the various game clients packets = list() #: A reference to the :class:`Person` that recorded the game recorder = None #: If there is a valid winning team this will contain a :class:`Team` otherwise it will be :class:`None` winner = None #: A dictionary mapping unit unique ids to their corresponding classes objects = dict() #: A sha256 hash uniquely representing the combination of people in the game. #: Can be used in conjunction with date times to match different replays #: of the game game. people_hash = str() #: SC2 Expansion. One of 'WoL', 'HotS' expasion = str() def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() # The current load level of the replay self.load_level = None #default values, filled in during file read self.player_names = list() self.other_people = set() self.speed = "" self.type = "" self.game_type = "" self.real_type = "" self.category = "" self.is_ladder = False self.is_private = False self.map = None self.map_hash = "" self.gateway = "" self.events = list() self.events_by_type = defaultdict(list) self.teams, self.team = list(), dict() self.player = utils.PersonDict() self.observer = utils.PersonDict() self.human = utils.PersonDict() self.computer = utils.PersonDict() self.entity = utils.PersonDict() self.players = list() self.observers = list() # Unordered list of Observer self.humans = list() self.computers = list() self.entities = list() self.attributes = defaultdict(dict) self.messages = list() self.recorder = None # Player object self.packets = list() self.objects = {} self.active_units = {} self.game_fps = 16.0 self.tracker_events = list() self.game_events = list() # Bootstrap the readers. self.registered_readers = defaultdict(list) self.register_default_readers() # Bootstrap the datapacks. self.registered_datapacks = list() self.register_default_datapacks() # Unpack the MPQ and read header data if requested # Since the underlying traceback isn't important to most people, don't expose it in python2 anymore if load_level >= 0: self.load_level = 0 try: self.archive = mpyq.MPQArchive(replay_file, listfile=False) except Exception as e: raise exceptions.MPQError("Unable to construct the MPQArchive", e) header_content = self.archive.header['user_data_header']['content'] header_data = BitPackedDecoder(header_content).read_struct() self.versions = list(header_data[1].values()) self.frames = header_data[3] self.build = self.versions[4] self.base_build = self.versions[5] self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) self.game_length = utils.Length(seconds=self.frames / 16) self.length = self.real_length = utils.Length( seconds=int(self.frames / self.game_fps)) # Load basic details if requested if load_level >= 1: self.load_level = 1 for data_file in [ 'replay.initData', 'replay.details', 'replay.attributes.events' ]: self._read_data(data_file, self._get_reader(data_file)) self.load_details() self.datapack = self._get_datapack() # Can only be effective if map data has been loaded if options.get('load_map', False): self.load_map() # Load players if requested if load_level >= 2: self.load_level = 2 for data_file in ['replay.message.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_message_events() self.load_players() # Load tracker events if requested if load_level >= 3: self.load_level = 3 for data_file in ['replay.tracker.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_tracker_events() # Load events if requested if load_level >= 4: self.load_level = 4 for data_file in ['replay.game.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_game_events() # Run this replay through the engine as indicated if engine: engine.run(self) def load_details(self): if 'replay.attributes.events' in self.raw_data: # Organize the attribute data to be useful self.attributes = defaultdict(dict) attributesEvents = self.raw_data['replay.attributes.events'] for attr in attributesEvents: self.attributes[attr.player][attr.name] = attr.value # Populate replay with attributes self.speed = self.attributes[16]['Game Speed'] self.category = self.attributes[16]['Game Mode'] self.type = self.game_type = self.attributes[16]['Teams'] self.is_ladder = (self.category == "Ladder") self.is_private = (self.category == "Private") if 'replay.details' in self.raw_data: details = self.raw_data['replay.details'] self.map_name = details['map_name'] self.gateway = details['cache_handles'][0].server.lower() self.map_hash = details['cache_handles'][-1].hash self.map_file = details['cache_handles'][-1] #Expand this special case mapping if self.gateway == 'sg': self.gateway = 'sea' dependency_hashes = [d.hash for d in details['cache_handles']] if hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode( 'utf8')).hexdigest() in dependency_hashes: self.expansion = 'HotS' elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode( 'utf8')).hexdigest() in dependency_hashes: self.expansion = 'WoL' else: self.expansion = '' self.windows_timestamp = details['file_time'] self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp) self.end_time = datetime.utcfromtimestamp(self.unix_timestamp) # The utc_adjustment is either the adjusted windows timestamp OR # the value required to get the adjusted timestamp. We know the upper # limit for any adjustment number so use that to distinguish between # the two cases. if details['utc_adjustment'] < 10**7 * 60 * 60 * 24: self.time_zone = details['utc_adjustment'] / (10**7 * 60 * 60) else: self.time_zone = (details['utc_adjustment'] - details['file_time']) / (10**7 * 60 * 60) self.game_length = self.length self.real_length = utils.Length( seconds=int(self.length.seconds / GAME_SPEED_FACTOR[self.speed])) self.start_time = datetime.utcfromtimestamp( self.unix_timestamp - self.real_length.seconds) self.date = self.end_time # backwards compatibility def load_map(self): self.map = self.factory.load_map(self.map_file, **self.opt) def load_players(self): #If we don't at least have details and attributes_events we can go no further if 'replay.details' not in self.raw_data: return if 'replay.attributes.events' not in self.raw_data: return if 'replay.initData' not in self.raw_data: return self.clients = list() self.client = dict() # For players, we can use the details file to look up additional # information. detail_id marks the current index into this data. detail_id = 0 player_id = 1 details = self.raw_data['replay.details'] initData = self.raw_data['replay.initData'] # Assume that the first X map slots starting at 1 are player slots # so that we can assign player ids without the map self.entities = list() for slot_id, slot_data in enumerate(initData['lobby_state']['slots']): user_id = slot_data['user_id'] if slot_data['control'] == 2: if slot_data['observe'] == 0: self.entities.append( Participant(slot_id, slot_data, user_id, initData['user_initial_data'][user_id], player_id, details['players'][detail_id], self.attributes.get(player_id, dict()))) detail_id += 1 player_id += 1 else: self.entities.append( Observer(slot_id, slot_data, user_id, initData['user_initial_data'][user_id], player_id)) player_id += 1 elif slot_data['control'] == 3: self.entities.append( Computer(slot_id, slot_data, player_id, details['players'][detail_id], self.attributes.get(player_id, dict()))) detail_id += 1 player_id += 1 def get_team(team_id): if team_id is not None and team_id not in self.team: team = Team(team_id) self.team[team_id] = team self.teams.append(team) return self.team[team_id] # Set up all our cross reference data structures for entity in self.entities: if entity.is_observer is False: entity.team = get_team(entity.team_id) entity.team.players.append(entity) self.players.append(entity) self.player[entity.pid] = entity else: self.observers.append(entity) self.observer[entity.uid] = entity if entity.is_human: self.humans.append(entity) self.human[entity.uid] = entity else: self.computers.append(entity) self.computer[entity.pid] = entity # Index by pid so that we can match events to players in pre-HotS replays self.entity[entity.pid] = entity # Pull results up for teams for team in self.teams: results = set([p.result for p in team.players]) if len(results) == 1: team.result = list(results)[0] if team.result == 'Win': self.winner = team else: self.logger.warn( "Conflicting results for Team {0}: {1}".format( team.number, results)) team.result = 'Unknown' self.teams.sort(key=lambda t: t.number) # These are all deprecated self.clients = self.humans self.people = self.entities self.client = self.human self.person = self.entity self.real_type = utils.get_real_type(self.teams) # Assign the default region to computer players for consistency # We know there will be a default region because there must be # at least 1 human player or we wouldn't have a replay. default_region = self.humans[0].region for entity in self.entities: if not entity.region: entity.region = default_region # Pretty sure this just never worked, forget about it for now self.recorder = None entity_names = sorted(map(lambda p: p.name, self.entities)) hash_input = self.gateway + ":" + ','.join(entity_names) self.people_hash = hashlib.sha256( hash_input.encode('utf8')).hexdigest() # The presence of observers and/or computer players makes this not actually ladder # This became an issue in HotS where Training, vs AI, Unranked, and Ranked # were all marked with "amm" => Ladder if len(self.observers) > 0 or len(self.humans) != len(self.players): self.is_ladder = False def load_message_events(self): if 'replay.message.events' not in self.raw_data: return self.messages = self.raw_data['replay.message.events'].messages self.pings = self.raw_data['replay.message.events'].pings self.packets = self.raw_data['replay.message.events'].packets self.message_events = self.messages + self.pings + self.packets self.events = sorted(self.events + self.message_events, key=lambda e: e.frame) def load_game_events(self): # Copy the events over # TODO: the events need to be fixed both on the reader and processor side if 'replay.game.events' not in self.raw_data: return self.game_events = self.raw_data['replay.game.events'] self.events = sorted(self.events + self.game_events, key=lambda e: e.frame) # hideous hack for HotS 2.0.0.23925, see https://github.com/GraylinKim/sc2reader/issues/87 if self.events and self.events[-1].frame > self.frames: self.frames = self.events[-1].frame self.length = utils.Length(seconds=int(self.frames / self.game_fps)) def load_tracker_events(self): if 'replay.tracker.events' not in self.raw_data: return self.tracker_events = self.raw_data['replay.tracker.events'] self.events = sorted(self.tracker_events + self.events, key=lambda e: e.frame) def register_reader(self, data_file, reader, filterfunc=lambda r: True): """ Allows you to specify your own reader for use when reading the data files packed into the .SC2Replay archives. Datapacks are checked for use with the supplied filterfunc in reverse registration order to give user registered datapacks preference over factory default datapacks. Don't use this unless you know what you are doing. :param data_file: The full file name that you would like this reader to parse. :param reader: The :class:`Reader` object you wish to use to read the data file. :param filterfunc: A function that accepts a partially loaded :class:`Replay` object as an argument and returns true if the reader should be used on this replay. """ self.registered_readers[data_file].insert(0, (filterfunc, reader)) def register_datapack(self, datapack, filterfunc=lambda r: True): """ Allows you to specify your own datapacks for use when loading replays. Datapacks are checked for use with the supplied filterfunc in reverse registration order to give user registered datapacks preference over factory default datapacks. This is how you would add mappings for your favorite custom map. :param datapack: A :class:`BaseData` object to use for mapping unit types and ability codes to their corresponding classes. :param filterfunc: A function that accepts a partially loaded :class:`Replay` object as an argument and returns true if the datapack should be used on this replay. """ self.registered_datapacks.insert(0, (filterfunc, datapack)) # Override points def register_default_readers(self): """Registers factory default readers.""" self.register_reader('replay.details', readers.DetailsReader(), lambda r: True) self.register_reader('replay.initData', readers.InitDataReader(), lambda r: True) self.register_reader('replay.tracker.events', readers.TrackerEventsReader(), lambda r: True) self.register_reader('replay.message.events', readers.MessageEventsReader(), lambda r: True) self.register_reader('replay.attributes.events', readers.AttributesEventsReader(), lambda r: True) self.register_reader('replay.game.events', readers.GameEventsReader_15405(), lambda r: 15405 <= r.base_build < 16561) self.register_reader('replay.game.events', readers.GameEventsReader_16561(), lambda r: 16561 <= r.base_build < 17326) self.register_reader('replay.game.events', readers.GameEventsReader_17326(), lambda r: 17326 <= r.base_build < 18574) self.register_reader('replay.game.events', readers.GameEventsReader_18574(), lambda r: 18574 <= r.base_build < 19595) self.register_reader('replay.game.events', readers.GameEventsReader_19595(), lambda r: 19595 <= r.base_build < 22612) self.register_reader('replay.game.events', readers.GameEventsReader_22612(), lambda r: 22612 <= r.base_build < 23260) self.register_reader('replay.game.events', readers.GameEventsReader_23260(), lambda r: 23260 <= r.base_build < 24247) self.register_reader('replay.game.events', readers.GameEventsReader_24247(), lambda r: 24247 <= r.base_build < 26490) self.register_reader('replay.game.events', readers.GameEventsReader_26490(), lambda r: 26490 <= r.base_build) self.register_reader('replay.game.events', readers.GameEventsReader_HotSBeta(), lambda r: r.versions[1] == 2 and r.build < 24247) def register_default_datapacks(self): """Registers factory default datapacks.""" self.register_datapack( datapacks['WoL']['16117'], lambda r: r.expansion == 'WoL' and 16117 <= r.build < 17326) self.register_datapack( datapacks['WoL']['17326'], lambda r: r.expansion == 'WoL' and 17326 <= r.build < 18092) self.register_datapack( datapacks['WoL']['18092'], lambda r: r.expansion == 'WoL' and 18092 <= r.build < 19458) self.register_datapack( datapacks['WoL']['19458'], lambda r: r.expansion == 'WoL' and 19458 <= r.build < 22612) self.register_datapack( datapacks['WoL']['22612'], lambda r: r.expansion == 'WoL' and 22612 <= r.build < 24944) self.register_datapack( datapacks['WoL']['24944'], lambda r: r.expansion == 'WoL' and 24944 <= r.build) self.register_datapack( datapacks['HotS']['base'], lambda r: r.expansion == 'HotS' and r.build < 23925) self.register_datapack( datapacks['HotS']['23925'], lambda r: r.expansion == 'HotS' and 23925 <= r.build < 24247) self.register_datapack( datapacks['HotS']['24247'], lambda r: r.expansion == 'HotS' and 24247 <= r.build <= 24764) self.register_datapack( datapacks['HotS']['24764'], lambda r: r.expansion == 'HotS' and 24764 <= r.build) # Internal Methods def _get_reader(self, data_file): for callback, reader in self.registered_readers[data_file]: if callback(self): return reader else: raise ValueError( "Valid {0} reader could not found for build {1}".format( data_file, self.build)) def _get_datapack(self): for callback, datapack in self.registered_datapacks: if callback(self): return datapack else: return None def _read_data(self, data_file, reader): data = utils.extract_data_file(data_file, self.archive) if data: self.raw_data[data_file] = reader(data, self) elif self.opt.debug and data_file not in [ 'replay.message.events', 'replay.tracker.events' ]: raise ValueError("{0} not found in archive".format(data_file))
class Replay(Resource): #: A nested dictionary of player => { attr_name : attr_value } for #: known attributes. Player 16 represents the global context and #: contains attributes like game speed. attributes = defaultdict(dict) #: Fully qualified filename of the replay file represented. filename = str() #: Total number of frames in this game at 16 frames per second. frames = int() #: The SCII game engine build number build = int() #: The full version release string as seen on Battle.net release_string = str() #: A tuple of the individual pieces of the release string versions = tuple() #: The game speed: Slower, Slow, Normal, Fast, Faster speed = str() #: Deprecated, use :attr:`game_type` or :attr:`real_type` instead type = str() #: The game type choosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA game_type = str() #: The real type of the replay as observed by counting players on teams. #: For outmatched games, the smaller team numbers come first. #: Example Values: 1v1, 2v2, 3v3, FFA, 2v4, etc. real_type = str() #: The category of the game, Ladder and Private category = str() #: A flag for public ladder games is_ladder = bool() #: A flag for private non-ladder games is_private = bool() #: The raw hash name of the s2ma resource as hosted on bnet depots map_hash = str() #: The name of the map the game was played on map_name = str() #: A reference to the loaded :class:`Map` resource. map = None #: The UTC time (according to the client NOT the server) that the game #: was ended as represented by the Windows OS windows_timestamp = int() #: The UTC time (according to the client NOT the server) that the game #: was ended as represented by the Unix OS unix_timestamp = int() #: The time zone adjustment for the time zone registered on the local #: computer that recorded this replay. time_zone = int() #: Deprecated: See `end_time` below. date = None #: A datetime object representing the utc time at the end of the game. end_time = None #: A datetime object representing the utc time at the start of the game start_time = None #: Deprecated: See `game_length` below. length = None #: The :class:`Length` of the replay as an alternative to :attr:`frames` game_length = None #: The :class:`Length` of the replay in real time adjusted for the game speed real_length = None #: The gateway the game was played on: us, eu, sea, etc gateway = str() #: An integrated list of all the game events events = list() #: A list of :class:`Team` objects from the game teams = list() #: A dict mapping team number to :class:`Team` object team = dict() #: A list of :class:`Player` objects from the game players = list() #: A dual key dict mapping player names and numbers to #: :class:`Player` objects player = utils.PersonDict() #: A list of :class:`Observer` objects from the game observers = list() #: A list of :class:`Person` objects from the game representing #: both the player and observer lists people = list() #: A dual key dict mapping :class:`Person` object to their #: person id's and names person = utils.PersonDict() #: A list of :class:`Person` objects from the game representing #: only the human players from the :attr:`people` list humans = list() #: A list of all the chat message events from the game messages = list() #: A list of pings sent by all the different people in the game pings = list() #: A list of packets sent between the various game clients packets = list() #: A reference to the :class:`Person` that recorded the game recorder = None #: If there is a valid winning team this will contain a :class:`Team` otherwise it will be :class:`None` winner = None #: A dictionary mapping unit unique ids to their corresponding classes objects = dict() #: A sha256 hash uniquely representing the combination of people in the game. #: Can be used in conjunction with date times to match different replays #: of the game game. people_hash = str() #: SC2 Expansion. One of 'WoL', 'HotS' expasion = str() def __init__(self, replay_file, filename=None, load_level=4, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() #default values, filled in during file read self.player_names = list() self.other_people = set() self.speed = "" self.type = "" self.game_type = "" self.real_type = "" self.category = "" self.is_ladder = False self.is_private = False self.map = None self.map_hash = "" self.gateway = "" self.events = list() self.events_by_type = defaultdict(list) self.teams, self.team = list(), dict() self.players, self.player = list(), utils.PersonDict() self.observers = list() #Unordered list of Observer self.people, self.humans = list(), list( ) #Unordered list of Players+Observers self.person = utils.PersonDict() #Maps pid to Player/Observer self.attributes = defaultdict(dict) self.messages = list() self.recorder = None # Player object self.packets = list() self.objects = {} self.active_units = {} self.game_fps = 16.0 # Bootstrap the readers. self.registered_readers = defaultdict(list) self.register_default_readers() # Bootstrap the datapacks. self.registered_datapacks = list() self.register_default_datapacks() # Unpack the MPQ and read header data if requested if load_level >= 0: try: self.archive = mpyq.MPQArchive(replay_file, listfile=False) except Exception as e: trace = sys.exc_info()[2] raise exceptions.MPQError("Unable to construct the MPQArchive", e), None, trace header_content = self.archive.header['user_data_header']['content'] header_data = BitPackedDecoder(header_content).read_struct() self.versions = header_data[1].values() self.frames = header_data[3] self.build = self.versions[4] self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) self.game_length = utils.Length(seconds=self.frames / 16) self.length = self.real_length = utils.Length( seconds=int(self.frames / self.game_fps)) # Load basic details if requested if load_level >= 1: for data_file in [ 'replay.initData', 'replay.details', 'replay.attributes.events' ]: self._read_data(data_file, self._get_reader(data_file)) self.load_details() self.datapack = self._get_datapack() # Can only be effective if map data has been loaded if options.get('load_map', False): self.load_map() # Load players if requested if load_level >= 2: for data_file in ['replay.message.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_messages() self.load_players() # Load events if requested if load_level >= 3: for data_file in ['replay.game.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_events() # Load tracker events if requested if load_level >= 4: for data_file in ['replay.tracker.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_tracker_events() for event in self.events: event.load_context(self) def load_details(self): if 'replay.attributes.events' in self.raw_data: # Organize the attribute data to be useful self.attributes = defaultdict(dict) attributesEvents = self.raw_data['replay.attributes.events'] for attr in attributesEvents: self.attributes[attr.player][attr.name] = attr.value # Populate replay with attributes self.speed = self.attributes[16]['Game Speed'] self.category = self.attributes[16]['Game Mode'] self.type = self.game_type = self.attributes[16]['Teams'] self.is_ladder = (self.category == "Ladder") self.is_private = (self.category == "Private") if 'replay.details' in self.raw_data: details = self.raw_data['replay.details'] self.map_name = details.map self.gateway = details.dependencies[0].server.lower() self.map_hash = details.dependencies[-1].hash self.map_file = details.dependencies[-1] #Expand this special case mapping if self.gateway == 'sg': self.gateway = 'sea' dependency_hashes = [d.hash for d in details.dependencies] if hashlib.sha256('Standard Data: Swarm.SC2Mod').hexdigest( ) in dependency_hashes: self.expansion = 'HotS' elif hashlib.sha256('Standard Data: Liberty.SC2Mod').hexdigest( ) in dependency_hashes: self.expansion = 'WoL' else: self.expansion = '' self.windows_timestamp = details.file_time self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp) self.end_time = datetime.utcfromtimestamp(self.unix_timestamp) # The utc_adjustment is either the adjusted windows timestamp OR # the value required to get the adjusted timestamp. We know the upper # limit for any adjustment number so use that to distinguish between # the two cases. if details.utc_adjustment < 10**7 * 60 * 60 * 24: self.time_zone = details.utc_adjustment / (10**7 * 60 * 60) else: self.time_zone = (details.utc_adjustment - details.file_time) / (10**7 * 60 * 60) self.game_length = self.length self.real_length = utils.Length( seconds=int(self.length.seconds / GAME_SPEED_FACTOR[self.speed])) self.start_time = datetime.utcfromtimestamp( self.unix_timestamp - self.real_length.seconds) self.date = self.end_time #backwards compatibility def load_map(self): self.map = self.factory.load_map(self.map_file, **self.opt) def load_players(self): #If we don't at least have details and attributes_events we can go no further if 'replay.details' not in self.raw_data: return if 'replay.attributes.events' not in self.raw_data: return if 'replay.initData' not in self.raw_data: return self.clients = list() self.client = dict() def createObserver(pid, name, attributes): # TODO: Make use of that attributes, new in HotS observer = Observer(pid, name) return observer def createPlayer(pid, pdata, attributes): # make sure to strip the clan tag out of the name # in newer replays, the clan tag can be separated from the # player name with a <sp/> symbol. It should also be stripped. name = pdata.name.split("]", 1)[-1].split(">", 1)[-1] player = Player(pid, name) # In some beta patches attribute information is missing # Just assign them to team 2 to keep the issue from being fatal team_number = int( attributes.get('Teams' + self.type, "Team 2")[5:]) if not team_number in self.team: self.team[team_number] = Team(team_number) self.teams.append(self.team[team_number]) self.team[team_number].players.append(player) player.team = self.team[team_number] # Do basic win/loss processing from details data if pdata.result == 1: player.team.result = "Win" self.winner = player.team elif pdata.result == 2: player.team.result = "Loss" else: player.team.result = None player.pick_race = attributes.get('Race', 'Unknown') player.play_race = LOCALIZED_RACES.get(pdata.race, pdata.race) player.difficulty = attributes.get('Difficulty', 'Unknown') player.is_human = (attributes.get('Controller', 'Computer') == 'User') player.uid = pdata.bnet.uid player.subregion = pdata.bnet.subregion player.gateway = GATEWAY_LOOKUP[pdata.bnet.gateway] player.handicap = pdata.handicap player.color = utils.Color(**pdata.color._asdict()) return player pid = 0 init_data = self.raw_data['replay.initData'] clients = [ d['name'] for d in init_data['player_init_data'] if d['name'] ] for index, pdata in enumerate(self.raw_data['replay.details'].players): pid += 1 attributes = self.attributes.get(pid, dict()) player = createPlayer(pid, pdata, attributes) self.player[pid] = player self.players.append(player) self.player[pid] = player self.people.append(player) self.person[pid] = player for cid, name in enumerate(clients): if name not in self.player._key_map: pid += 1 attributes = self.attributes.get(pid, dict()) client = createObserver(pid, name, attributes) self.observers.append(client) self.people.append(client) self.person[pid] = client else: client = self.player.name(name) client.cid = cid self.clients.append(client) self.client[cid] = client # replay.clients replaces replay.humans self.humans = self.clients #Create an store an ordered lineup string for team in self.teams: team.lineup = ''.join( sorted(player.play_race[0].upper() for player in team)) self.real_type = real_type(self.teams) # Assign the default region to computer players for consistency # We know there will be a default region because there must be # at least 1 human player or we wouldn't have a self. default_region = self.humans[0].region for player in self.players: if not player.is_human: player.region = default_region for obs in self.observers: obs.region = default_region if 'replay.message.events' in self.raw_data: # Figure out recorder self.packets = self.raw_data['replay.message.events'].packets packet_senders = set(map(lambda p: p.pid, self.packets)) human_pids = map(lambda p: p.pid, self.humans) recorders = list(set(human_pids) - set(packet_senders)) if len(recorders) == 1: self.recorder = self.person[recorders[0]] self.recorder.recorder = True else: self.recorder = None self.logger.error("{0} possible recorders remain: {1}".format( len(recorders), recorders)) player_names = sorted(map(lambda p: p.name, self.people)) hash_input = self.gateway + ":" + ','.join(player_names) self.people_hash = hashlib.sha256(hash_input).hexdigest() # The presence of observers and/or computer players makes this not actually ladder # This became an issue in HotS where Training, vs AI, Unranked, and Ranked # were all marked with "amm" => Ladder if len(self.observers) > 0 or len(self.humans) != len(self.players): self.is_ladder = False def load_messages(self): if 'replay.message.events' in self.raw_data: self.messages = self.raw_data['replay.message.events'].messages self.pings = self.raw_data['replay.message.events'].pings self.packets = self.raw_data['replay.message.events'].packets self.events += self.messages + self.pings + self.packets self.events = sorted(self.events, key=lambda e: e.frame) def load_events(self): # Copy the events over # TODO: the events need to be fixed both on the reader and processor side if 'replay.game.events' not in self.raw_data: return self.game_events = self.raw_data['replay.game.events'] self.events = sorted(self.game_events + self.events, key=lambda e: e.frame) # hideous hack for HotS 2.0.0.23925, see https://github.com/GraylinKim/sc2reader/issues/87 if self.events and self.events[-1].frame > self.frames: self.frames = self.events[-1].frame self.length = utils.Length(seconds=int(self.frames / self.game_fps)) def load_tracker_events(self): if 'replay.tracker.events' not in self.raw_data: return self.tracker_events = self.raw_data['replay.tracker.events'] self.events = sorted(self.tracker_events + self.events, key=lambda e: e.frame) def register_reader(self, data_file, reader, filterfunc=lambda r: True): """ Allows you to specify your own reader for use when reading the data files packed into the .SC2Replay archives. Datapacks are checked for use with the supplied filterfunc in reverse registration order to give user registered datapacks preference over factory default datapacks. Don't use this unless you know what you are doing. :param data_file: The full file name that you would like this reader to parse. :param reader: The :class:`Reader` object you wish to use to read the data file. :param filterfunc: A function that accepts a partially loaded :class:`Replay` object as an argument and returns true if the reader should be used on this replay. """ self.registered_readers[data_file].insert(0, (filterfunc, reader)) def register_datapack(self, datapack, filterfunc=lambda r: True): """ Allows you to specify your own datapacks for use when loading replays. Datapacks are checked for use with the supplied filterfunc in reverse registration order to give user registered datapacks preference over factory default datapacks. This is how you would add mappings for your favorite custom map. :param datapack: A :class:`BaseData` object to use for mapping unit types and ability codes to their corresponding classes. :param filterfunc: A function that accepts a partially loaded :class:`Replay` object as an argument and returns true if the datapack should be used on this replay. """ self.registered_datapacks.insert(0, (filterfunc, datapack)) # Override points def register_default_readers(self): """Registers factory default readers.""" self.register_reader('replay.details', readers.DetailsReader_Base(), lambda r: r.build < 22612) self.register_reader('replay.details', readers.DetailsReader_22612(), lambda r: r.build >= 22612 and r.versions[1] == 1) self.register_reader('replay.details', readers.DetailsReader_Beta(), lambda r: r.build < 24764 and r.versions[1] == 2) self.register_reader('replay.details', readers.DetailsReader_Beta_24764(), lambda r: r.build >= 24764) self.register_reader('replay.initData', readers.InitDataReader_Base(), lambda r: r.build < 23260) self.register_reader('replay.initData', readers.InitDataReader_23260(), lambda r: r.build >= 23260 and r.build < 24764) self.register_reader('replay.initData', readers.InitDataReader_24764(), lambda r: r.build >= 24764) self.register_reader('replay.message.events', readers.MessageEventsReader_Base(), lambda r: r.build < 24247 or r.versions[1] == 1) self.register_reader('replay.message.events', readers.MessageEventsReader_Beta_24247(), lambda r: r.build >= 24247 and r.versions[1] == 2) self.register_reader('replay.attributes.events', readers.AttributesEventsReader_Base(), lambda r: r.build < 17326) self.register_reader('replay.attributes.events', readers.AttributesEventsReader_17326(), lambda r: r.build >= 17326) self.register_reader('replay.game.events', readers.GameEventsReader_16117(), lambda r: 16117 <= r.build < 16561) self.register_reader('replay.game.events', readers.GameEventsReader_16561(), lambda r: 16561 <= r.build < 18574) self.register_reader('replay.game.events', readers.GameEventsReader_18574(), lambda r: 18574 <= r.build < 19595) self.register_reader('replay.game.events', readers.GameEventsReader_19595(), lambda r: 19595 <= r.build < 22612) self.register_reader( 'replay.game.events', readers.GameEventsReader_22612(), lambda r: r.versions[1] == 1 and 22612 <= r.build) # Last WoL self.register_reader( 'replay.game.events', readers.GameEventsReader_HotS_Beta(), lambda r: r.versions[1] == 2 and r.build < 24247) #HotS Beta self.register_reader( 'replay.game.events', readers.GameEventsReader_HotS(), lambda r: r.versions[1] == 2 and 24247 <= r.build) # First HotS self.register_reader('replay.tracker.events', readers.TrackerEventsReader_Base(), lambda r: True) def register_default_datapacks(self): """Registers factory default datapacks.""" self.register_datapack( datapacks['WoL']['16117'], lambda r: r.expansion == 'WoL' and 16117 <= r.build < 17326) self.register_datapack( datapacks['WoL']['17326'], lambda r: r.expansion == 'WoL' and 17326 <= r.build < 18092) self.register_datapack( datapacks['WoL']['18092'], lambda r: r.expansion == 'WoL' and 18092 <= r.build < 19458) self.register_datapack( datapacks['WoL']['19458'], lambda r: r.expansion == 'WoL' and 19458 <= r.build < 22612) self.register_datapack( datapacks['WoL']['22612'], lambda r: r.expansion == 'WoL' and 22612 <= r.build < 24944) self.register_datapack( datapacks['WoL']['24944'], lambda r: r.expansion == 'WoL' and 24944 <= r.build) self.register_datapack( datapacks['HotS']['base'], lambda r: r.expansion == 'HotS' and r.build < 23925) self.register_datapack( datapacks['HotS']['23925'], lambda r: r.expansion == 'HotS' and 23925 <= r.build < 24247) self.register_datapack( datapacks['HotS']['24247'], lambda r: r.expansion == 'HotS' and 24247 <= r.build <= 24764) self.register_datapack( datapacks['HotS']['24764'], lambda r: r.expansion == 'HotS' and 24764 <= r.build) # Internal Methods def _get_reader(self, data_file): for callback, reader in self.registered_readers[data_file]: if callback(self): return reader else: raise ValueError( "Valid {0} reader could not found for build {1}".format( data_file, self.build)) def _get_datapack(self): for callback, datapack in self.registered_datapacks: if callback(self): return datapack else: return None def _read_data(self, data_file, reader): data = utils.extract_data_file(data_file, self.archive) if data: self.raw_data[data_file] = reader(data, self) elif self.opt.debug and data_file not in [ 'replay.message.events', 'replay.tracker.events' ]: raise ValueError("{0} not found in archive".format(data_file))
def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() # The current load level of the replay self.load_level = None #default values, filled in during file read self.player_names = list() self.other_people = set() self.speed = "" self.type = "" self.game_type = "" self.real_type = "" self.category = "" self.is_ladder = False self.is_private = False self.map = None self.map_hash = "" self.gateway = "" self.events = list() self.events_by_type = defaultdict(list) self.teams, self.team = list(), dict() self.player = utils.PersonDict() self.observer = utils.PersonDict() self.human = utils.PersonDict() self.computer = utils.PersonDict() self.entity = utils.PersonDict() self.players = list() self.observers = list() # Unordered list of Observer self.humans = list() self.computers = list() self.entities = list() self.attributes = defaultdict(dict) self.messages = list() self.recorder = None # Player object self.packets = list() self.objects = {} self.active_units = {} self.game_fps = 16.0 self.tracker_events = list() self.game_events = list() # Bootstrap the readers. self.registered_readers = defaultdict(list) self.register_default_readers() # Bootstrap the datapacks. self.registered_datapacks = list() self.register_default_datapacks() # Unpack the MPQ and read header data if requested # Since the underlying traceback isn't important to most people, don't expose it in python2 anymore if load_level >= 0: self.load_level = 0 try: self.archive = mpyq.MPQArchive(replay_file, listfile=False) except Exception as e: raise exceptions.MPQError("Unable to construct the MPQArchive", e) header_content = self.archive.header['user_data_header']['content'] header_data = BitPackedDecoder(header_content).read_struct() self.versions = list(header_data[1].values()) self.frames = header_data[3] self.build = self.versions[4] self.base_build = self.versions[5] self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) self.game_length = utils.Length(seconds=self.frames / 16) self.length = self.real_length = utils.Length( seconds=int(self.frames / self.game_fps)) # Load basic details if requested if load_level >= 1: self.load_level = 1 for data_file in [ 'replay.initData', 'replay.details', 'replay.attributes.events' ]: self._read_data(data_file, self._get_reader(data_file)) self.load_details() self.datapack = self._get_datapack() # Can only be effective if map data has been loaded if options.get('load_map', False): self.load_map() # Load players if requested if load_level >= 2: self.load_level = 2 for data_file in ['replay.message.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_message_events() self.load_players() # Load tracker events if requested if load_level >= 3: self.load_level = 3 for data_file in ['replay.tracker.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_tracker_events() # Load events if requested if load_level >= 4: self.load_level = 4 for data_file in ['replay.game.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_game_events() # Run this replay through the engine as indicated if engine: engine.run(self)
def __init__(self, replay_file, filename=None, load_level=4, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() #default values, filled in during file read self.player_names = list() self.other_people = set() self.speed = "" self.type = "" self.game_type = "" self.real_type = "" self.category = "" self.is_ladder = False self.is_private = False self.map = None self.map_hash = "" self.gateway = "" self.events = list() self.events_by_type = defaultdict(list) self.teams, self.team = list(), dict() self.players, self.player = list(), utils.PersonDict() self.observers = list() #Unordered list of Observer self.people, self.humans = list(), list( ) #Unordered list of Players+Observers self.person = utils.PersonDict() #Maps pid to Player/Observer self.attributes = defaultdict(dict) self.messages = list() self.recorder = None # Player object self.packets = list() self.objects = {} self.active_units = {} self.game_fps = 16.0 # Bootstrap the readers. self.registered_readers = defaultdict(list) self.register_default_readers() # Bootstrap the datapacks. self.registered_datapacks = list() self.register_default_datapacks() # Unpack the MPQ and read header data if requested if load_level >= 0: try: self.archive = mpyq.MPQArchive(replay_file, listfile=False) except Exception as e: trace = sys.exc_info()[2] raise exceptions.MPQError("Unable to construct the MPQArchive", e), None, trace header_content = self.archive.header['user_data_header']['content'] header_data = BitPackedDecoder(header_content).read_struct() self.versions = header_data[1].values() self.frames = header_data[3] self.build = self.versions[4] self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) self.game_length = utils.Length(seconds=self.frames / 16) self.length = self.real_length = utils.Length( seconds=int(self.frames / self.game_fps)) # Load basic details if requested if load_level >= 1: for data_file in [ 'replay.initData', 'replay.details', 'replay.attributes.events' ]: self._read_data(data_file, self._get_reader(data_file)) self.load_details() self.datapack = self._get_datapack() # Can only be effective if map data has been loaded if options.get('load_map', False): self.load_map() # Load players if requested if load_level >= 2: for data_file in ['replay.message.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_messages() self.load_players() # Load events if requested if load_level >= 3: for data_file in ['replay.game.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_events() # Load tracker events if requested if load_level >= 4: for data_file in ['replay.tracker.events']: self._read_data(data_file, self._get_reader(data_file)) self.load_tracker_events() for event in self.events: event.load_context(self)