Example #1
0
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))
Example #2
0
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))
Example #3
0
    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)
Example #4
0
    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)