From dda41fbb378d137a67898140fa494a31df9de016 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 22 Sep 2013 15:18:08 -0400 Subject: [PATCH 01/53] Fix up event names and hierarchy. * PacketEvent is now ProgressEvent. * SetToHotkeyEvent is now SetControlGroupEvent. * AddToHotkeyEvent is now AddToControlGroupEvent. * GetFromHotkeyEvent is now GetControlGroupEvent. * PlayerAbilityEvent is no longer part of the event hierarchy. * event.name is no longer a class property; it can only be accessed from an event instance. --- CHANGELOG.rst | 14 +++- sc2reader/engine/engine.py | 40 ++++----- sc2reader/engine/plugins/apm.py | 12 ++- sc2reader/events/game.py | 112 +++++++++----------------- sc2reader/events/message.py | 31 ++++--- sc2reader/events/tracker.py | 43 +++++----- sc2reader/factories/plugins/replay.py | 2 +- sc2reader/readers.py | 2 +- 8 files changed, 117 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 00f0f90e..4d76d073 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,11 +1,23 @@ CHANGELOG ============ +0.7.0 - +--------------------------- + +* PacketEvent is now ProgressEvent. +* SetToHotkeyEvent is now SetControlGroupEvent. +* AddToHotkeyEvent is now AddToControlGroupEvent. +* GetFromHotkeyEvent is now GetControlGroupEvent. +* PlayerAbilityEvent is no longer part of the event hierarchy. +* event.name is no longer a class property; it can only be accessed from an event instance. + + + 0.6.4 - September 22nd 2013 --------------------------- * Fix bug in code for logging errors. -* Fix siege tank supply count +* Fix siege tank supply count. * Small improvements to message.events parsing. 0.6.3 - September 15th 2013 diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index fffc3246..2788bcc6 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -5,6 +5,7 @@ from sc2reader.events import * from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit + class GameEngine(object): """ GameEngine Specification -------------------------- @@ -56,9 +57,8 @@ def handleEventName(self, event, replay) * handleMessageEvent - called for events in replay.message.events * handleGameEvent - called for events in replay.game.events * handleTrackerEvent - called for events in replay.tracker.events - * handlePlayerActionEvent - called for all game events indicating player actions * handleAbilityEvent - called for all types of ability events - * handleHotkeyEvent - called for all player hotkey events + * handleControlGroupEvent - called for all player control group events Plugins may also handle optional ``InitGame`` and ``EndGame`` events generated by the GameEngine before and after processing all the events: @@ -196,26 +196,18 @@ def _get_event_handlers(self, event, plugins): def _get_plugin_event_handlers(self, plugin, event): handlers = list() - if isinstance(event, Event) and self._has_event_handler(plugin, Event): - handlers.append(self._get_event_handler(plugin, Event)) - if isinstance(event, MessageEvent) and self._has_event_handler(plugin, MessageEvent): - handlers.append(self._get_event_handler(plugin, MessageEvent)) - if isinstance(event, GameEvent) and self._has_event_handler(plugin, GameEvent): - handlers.append(self._get_event_handler(plugin, GameEvent)) - if isinstance(event, TrackerEvent) and self._has_event_handler(plugin, TrackerEvent): - handlers.append(self._get_event_handler(plugin, TrackerEvent)) - if isinstance(event, PlayerActionEvent) and self._has_event_handler(plugin, PlayerActionEvent): - handlers.append(self._get_event_handler(plugin, PlayerActionEvent)) - if isinstance(event, AbilityEvent) and self._has_event_handler(plugin, AbilityEvent): - handlers.append(self._get_event_handler(plugin, AbilityEvent)) - if isinstance(event, HotkeyEvent) and self._has_event_handler(plugin, HotkeyEvent): - handlers.append(self._get_event_handler(plugin, HotkeyEvent)) - if self._has_event_handler(plugin, event): - handlers.append(self._get_event_handler(plugin, event)) + if isinstance(event, Event) and hasattr(plugin, 'handleEvent'): + handlers.append(getattr(plugin, 'handleEvent', None)) + if isinstance(event, MessageEvent) and hasattr(plugin, 'handleMessageEvent'): + handlers.append(getattr(plugin, 'handleMessageEvent', None)) + if isinstance(event, GameEvent) and hasattr(plugin, 'handleGameEvent'): + handlers.append(getattr(plugin, 'handleGameEvent', None)) + if isinstance(event, TrackerEvent) and hasattr(plugin, 'handleTrackerEvent'): + handlers.append(getattr(plugin, 'handleTrackerEvent', None)) + if isinstance(event, AbilityEvent) and hasattr(plugin, 'handleAbilityEvent'): + handlers.append(getattr(plugin, 'handleAbilityEvent', None)) + if isinstance(event, ControlGroupEvent) and hasattr(plugin, 'handleControlGroupEvent'): + handlers.append(getattr(plugin, 'handleControlGroupEvent', None)) + if hasattr(plugin, 'handle'+event.name): + handlers.append(getattr(plugin, 'handle'+event.name, None)) return handlers - - def _has_event_handler(self, plugin, event): - return hasattr(plugin, 'handle'+event.name) - - def _get_event_handler(self, plugin, event): - return getattr(plugin, 'handle'+event.name, None) diff --git a/sc2reader/engine/plugins/apm.py b/sc2reader/engine/plugins/apm.py index 0d69b293..42d44185 100644 --- a/sc2reader/engine/plugins/apm.py +++ b/sc2reader/engine/plugins/apm.py @@ -7,7 +7,7 @@ class APMTracker(object): """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is - any Selection, Hotkey, or Ability event. + any Selection, ControlGroup, or Ability event. Also provides ``player.avg_apm`` which is defined as the sum of all the above actions divided by the number of seconds played by the player (not @@ -23,7 +23,15 @@ def handleInitGame(self, event, replay): human.aps = defaultdict(int) human.seconds_played = replay.length.seconds - def handlePlayerActionEvent(self, event, replay): + def handleControlGroupEvent(self, event, replay): + event.player.aps[event.second] += 1 + event.player.apm[int(event.second/60)] += 1 + + def handleSelectionEvent(self, event, replay): + event.player.aps[event.second] += 1 + event.player.apm[int(event.second/60)] += 1 + + def handleAbilityEvent(self, event, replay): event.player.aps[event.second] += 1 event.player.apm[int(event.second/60)] += 1 diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 720b23a9..fc5a2413 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -13,8 +13,6 @@ class GameEvent(Event): """ This is the base class for all game events. The attributes below are universally available. """ - name = 'GameEvent' - def __init__(self, frame, pid): #: The id of the player generating the event. This is 16 for global non-player events. #: Prior to Heart of the Swarm this was the player id. Since HotS it is @@ -34,6 +32,9 @@ def __init__(self, frame, pid): #: A flag indicating if it is a local or global event. self.is_local = (pid != 16) + #: Short cut string for event class name + self.name = self.__class__.__name__ + def _str_prefix(self): player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" return "%s\t%-15s " % (Length(seconds=int(self.frame/16)), player_name) @@ -47,9 +48,6 @@ class GameStartEvent(GameEvent): Recorded when the game starts and the frames start to roll. This is a global non-player event. """ - - name = 'GameStartEvent' - def __init__(self, frame, pid, data): super(GameStartEvent, self).__init__(frame, pid) @@ -58,9 +56,6 @@ class PlayerLeaveEvent(GameEvent): """ Recorded when a player leaves the game. """ - - name = 'PlayerLeaveEvent' - def __init__(self, frame, pid, data): super(PlayerLeaveEvent, self).__init__(frame, pid) @@ -70,9 +65,6 @@ class UserOptionsEvent(GameEvent): This event is recorded for each player at the very beginning of the game before the :class:`GameStartEvent`. """ - - name = 'UserOptionsEvent' - def __init__(self, frame, pid, data): super(UserOptionsEvent, self).__init__(frame, pid) #: @@ -118,12 +110,8 @@ def create_command_event(frame, pid, data): return SelfAbilityEvent(frame, pid, data) -class PlayerActionEvent(GameEvent): - name = 'PlayerActionEvent' - - @loggable -class AbilityEvent(PlayerActionEvent): +class AbilityEvent(GameEvent): """ Ability events are generated when ever a player in the game issues a command to a unit or group of units. They are split into three subclasses of ability, @@ -133,11 +121,6 @@ class AbilityEvent(PlayerActionEvent): See :class:`LocationAbilityEvent`, :class:`TargetAbilityEvent`, and :class:`SelfAbilityEvent` for individual details. """ - - name = 'AbilityEvent' - - is_player_action = True - def __init__(self, frame, pid, data): super(AbilityEvent, self).__init__(frame, pid) @@ -236,9 +219,6 @@ class LocationAbilityEvent(AbilityEvent): Note that like all AbilityEvents, the event will be recorded regardless of whether or not the command was successful. """ - - name = 'LocationAbilityEvent' - def __init__(self, frame, pid, data): super(LocationAbilityEvent, self).__init__(frame, pid, data) @@ -265,9 +245,6 @@ class TargetAbilityEvent(AbilityEvent): Note that all AbilityEvents are recorded regardless of whether or not the command was successful. """ - - name = 'TargetAbilityEvent' - def __init__(self, frame, pid, data): super(TargetAbilityEvent, self).__init__(frame, pid, data) @@ -318,9 +295,6 @@ class SelfAbilityEvent(AbilityEvent): Note that all AbilityEvents are recorded regardless of whether or not the command was successful. """ - - name = 'SelfAbilityEvent' - def __init__(self, frame, pid, data): super(SelfAbilityEvent, self).__init__(frame, pid, data) @@ -329,7 +303,7 @@ def __init__(self, frame, pid, data): @loggable -class SelectionEvent(PlayerActionEvent): +class SelectionEvent(GameEvent): """ Selection events are generated when ever the active selection of the player is updated. Unlike other game events, these events can also be @@ -340,10 +314,6 @@ class SelectionEvent(PlayerActionEvent): by non-player actions. When a player action updates a control group a :class:`HotkeyEvent` is generated. """ - - name = 'SelectionEvent' - is_player_action = True - def __init__(self, frame, pid, data): super(SelectionEvent, self).__init__(frame, pid) @@ -392,39 +362,32 @@ def __str__(self): def create_control_group_event(frame, pid, data): update_type = data['control_group_update'] if update_type == 0: - return SetToHotkeyEvent(frame, pid, data) + return SetControlGroupEvent(frame, pid, data) elif update_type == 1: - return AddToHotkeyEvent(frame, pid, data) + return AddToControlGroupEvent(frame, pid, data) elif update_type == 2: - return GetFromHotkeyEvent(frame, pid, data) + return GetControlGroupEvent(frame, pid, data) elif update_type == 3: # TODO: What could this be?!? - return HotkeyEvent(frame, pid, data) + return ControlGroupEvent(frame, pid, data) @loggable -class HotkeyEvent(PlayerActionEvent): +class ControlGroupEvent(GameEvent): """ - Hotkey events are recorded when ever a player action modifies a control - group. I know that calling control group events hotkey events doesn't make - sense but for backwards compatibility I haven't changed it yet. Sorry. - - There are three kinds of hotkey events, generated by each of the possible + ControlGroup events are recorded when ever a player action modifies or accesses a control + group. There are three kinds of events, generated by each of the possible player actions: - * :class:`SetToHotkeyEvent` - Recorded when a user sets a control group (ctrl+#). - * :class:`GetFromHotkeyEvent` - Recorded when a user retrieves a control group (#). - * :class:`AddToHotkeyEvent` - Recorded when a user adds to a control group (shift+ctrl+#) + * :class:`SetControlGroup` - Recorded when a user sets a control group (ctrl+#). + * :class:`GetControlGroup` - Recorded when a user retrieves a control group (#). + * :class:`AddToControlGroup` - Recorded when a user adds to a control group (shift+ctrl+#) All three events have the same set of data (shown below) but are interpretted differently. See the class entry for details. """ - - name = 'HotkeyEvent' - is_player_action = True - def __init__(self, frame, pid, data): - super(HotkeyEvent, self).__init__(frame, pid) + super(ControlGroupEvent, self).__init__(frame, pid) #: Index to the control group being modified self.control_group = data['control_group_index'] @@ -445,38 +408,33 @@ def __init__(self, frame, pid, data): self.mask_data = data['remove_mask'][1] -class SetToHotkeyEvent(HotkeyEvent): +class SetControlGroupEvent(ControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` This event does a straight forward replace of the current control group contents with the player's current selection. This event doesn't have masks set. """ - name = 'SetToHotkeyEvent' - -class AddToHotkeyEvent(HotkeyEvent): +class AddToControlGroupEvent(SetControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` This event adds the current selection to the control group. """ - name = 'AddToHotkeyEvent' - -class GetFromHotkeyEvent(HotkeyEvent): +class GetControlGroupEvent(ControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` + This event replaces the current selection with the contents of the control group. The mask data is used to limit that selection to units that are currently selectable. You might have 1 medivac and 8 marines on the control group but if the 8 marines are inside the medivac they cannot be part of your selection. """ - name = 'GetFromHotkeyEvent' - @loggable class CameraEvent(GameEvent): @@ -485,9 +443,6 @@ class CameraEvent(GameEvent): It does not matter why the camera changed, this event simply records the current state of the camera after changing. """ - - name = 'CameraEvent' - def __init__(self, frame, pid, data): super(CameraEvent, self).__init__(frame, pid) @@ -515,8 +470,10 @@ def __str__(self): @loggable class ResourceTradeEvent(GameEvent): - name = 'ResourceTradeEvent' - + """ + Generated when a player trades resources with another player. But not when fullfulling + resource requests. + """ def __init__(self, frame, pid, data): super(ResourceTradeEvent, self).__init__(frame, pid) @@ -552,8 +509,9 @@ def __str__(self): class ResourceRequestEvent(GameEvent): - name = 'ResourceRequestEvent' - + """ + Generated when a player creates a resource request. + """ def __init__(self, frame, pid, data): super(ResourceRequestEvent, self).__init__(frame, pid) @@ -577,8 +535,9 @@ def __str__(self): class ResourceRequestFulfillEvent(GameEvent): - name = 'ResourceRequestFulfillEvent' - + """ + Generated when a player accepts a resource request. + """ def __init__(self, frame, pid, data): super(ResourceRequestFulfillEvent, self).__init__(frame, pid) @@ -587,8 +546,9 @@ def __init__(self, frame, pid, data): class ResourceRequestCancelEvent(GameEvent): - name = 'ResourceRequestCancelEvent' - + """ + Generated when a player cancels their resource request. + """ def __init__(self, frame, pid, data): super(ResourceRequestCancelEvent, self).__init__(frame, pid) diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index 7608f8db..dc6e40ca 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -8,13 +8,17 @@ @loggable class MessageEvent(Event): - name = 'MessageEvent' - + """ + Parent class for all message events. + """ def __init__(self, frame, pid): self.pid = pid self.frame = frame self.second = frame >> 4 + #: Short cut string for event class name + self.name = self.__class__.__name__ + def _str_prefix(self): player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" return "%s\t%-15s " % (Length(seconds=int(self.frame/16)), player_name) @@ -25,8 +29,9 @@ def __str__(self): @loggable class ChatEvent(MessageEvent): - name = 'ChatEvent' - + """ + Records in-game chat events. + """ def __init__(self, frame, pid, target, text): super(ChatEvent, self).__init__(frame, pid) self.target = target @@ -37,18 +42,22 @@ def __init__(self, frame, pid, target, text): @loggable -class PacketEvent(MessageEvent): - name = 'PacketEvent' +class ProgressEvent(MessageEvent): + """ + Sent during the load screen to update load process for other clients. + """ + def __init__(self, frame, pid, progress): + super(ProgressEvent, self).__init__(frame, pid) - def __init__(self, frame, pid, info): - super(PacketEvent, self).__init__(frame, pid) - self.info = info + #: Marks the load progress for the player. Scaled 0-100. + self.progress = progress @loggable class PingEvent(MessageEvent): - name = 'PingEvent' - + """ + Records pings made by players in game. + """ def __init__(self, frame, pid, target, x, y): super(PingEvent, self).__init__(frame, pid) self.target = target diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index d9526aeb..893f0e55 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -15,6 +15,9 @@ def __init__(self, frames): self.frame = frames self.second = frames >> 4 + #: Short cut string for event class name + self.name = self.__class__.__name__ + def load_context(self, replay): pass @@ -39,9 +42,6 @@ class PlayerStatsEvent(TrackerEvent): events generated at the end of the game. One for leaving and one for the end of the game. """ - - name = 'PlayerStatsEvent' - def __init__(self, frames, data, build): super(PlayerStatsEvent, self).__init__(frames) @@ -223,9 +223,6 @@ class UnitBornEvent(TrackerEvent): it are the :class:`~sc2reader.event.game.AbilityEvent` game events where the ability is a train unit command. """ - - name = 'UnitBornEvent' - def __init__(self, frames, data, build): super(UnitBornEvent, self).__init__(frames) @@ -274,9 +271,6 @@ class UnitDiedEvent(TrackerEvent): Generated when a unit dies or is removed from the game for any reason. Reasons include morphing, merging, and getting killed. """ - - name = 'UnitDiedEvent' - def __init__(self, frames, data, build): super(UnitDiedEvent, self).__init__(frames) @@ -312,8 +306,10 @@ def __str__(self): class UnitOwnerChangeEvent(TrackerEvent): - name = 'UnitOwnerChangeEvent' - + """ + Generated when either ownership or control of a unit is changed. Neural Parasite is an example + of an action that would generate this event. + """ def __init__(self, frames, data, build): super(UnitOwnerChangeEvent, self).__init__(frames) @@ -346,8 +342,11 @@ def __str__(self): class UnitTypeChangeEvent(TrackerEvent): - name = 'UnitTypeChangeEvent' - + """ + Generated when the unit's type changes. This generally tracks upgrades to buildings (Hatch, + Lair, Hive) and mode switches (Sieging Tanks, Phasing prisms, Burrowing roaches). There may + be some other situations where a unit transformation is a type change and not a new unit. + """ def __init__(self, frames, data, build): super(UnitTypeChangeEvent, self).__init__(frames) @@ -371,8 +370,9 @@ def __str__(self): class UpgradeCompleteEvent(TrackerEvent): - name = 'UpgradeCompleteEvent' - + """ + Generated when a player completes an upgrade. + """ def __init__(self, frames, data, build): super(UpgradeCompleteEvent, self).__init__(frames) @@ -399,9 +399,6 @@ class UnitInitEvent(TrackerEvent): in game before they are finished. Primary examples being buildings and warp-in units. """ - - name = 'UnitInitEvent' - def __init__(self, frames, data, build): super(UnitInitEvent, self).__init__(frames) @@ -451,9 +448,6 @@ class UnitDoneEvent(TrackerEvent): when an initiated unit is completed. E.g. warp-in finished, building finished, morph complete. """ - - name = 'UnitDoneEvent' - def __init__(self, frames, data, build): super(UnitDoneEvent, self).__init__(frames) @@ -474,8 +468,11 @@ def __str__(self): class UnitPositionsEvent(TrackerEvent): - name = 'UnitPositionsEvent' - + """ + Generated every 15 seconds. Marks the positions of the first 255 units that were damaged in + the last interval. If more than 255 units were damaged, then the first 255 are reported and + the remaining units are carried into the next interval. + """ def __init__(self, frames, data, build): super(UnitPositionsEvent, self).__init__(frames) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 81a05c54..40401196 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -106,7 +106,7 @@ def APMTracker(replay): player.seconds_played = replay.length.seconds for event in player.events: - if event.name == 'SelectionEvent' or 'AbilityEvent' in event.name or 'Hotkey' in event.name: + if event.name == 'SelectionEvent' or 'AbilityEvent' in event.name or 'ControlGroup' in event.name: player.aps[event.second] += 1 player.apm[int(event.second/60)] += 1 diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 7aba7219..61f8d928 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -197,7 +197,7 @@ def __call__(self, data, replay): elif flag == 2: # Loading progress message progress = data.read_uint32()-2147483648 - packets.append(PacketEvent(frame, pid, progress)) + packets.append(ProgressEvent(frame, pid, progress)) elif flag == 3: # Server ping message pass From 24c0acf48036b67cb95ebcb88d84edd044753c3d Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 22 Sep 2013 15:22:10 -0400 Subject: [PATCH 02/53] Flesh out the ping class. --- CHANGELOG.rst | 6 +++++- sc2reader/events/message.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4d76d073..9ee1965f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,7 +10,11 @@ CHANGELOG * GetFromHotkeyEvent is now GetControlGroupEvent. * PlayerAbilityEvent is no longer part of the event hierarchy. * event.name is no longer a class property; it can only be accessed from an event instance. - +* PingEvents now have new attributes: + * event.to_all - true if ping seen by all + * event.to_allies - true if ping seen by allies + * event.to_observers - true if ping seen by observers + * event.location - tuple of (event.x, event.y) 0.6.4 - September 22nd 2013 diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index dc6e40ca..b5166abd 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -60,5 +60,24 @@ class PingEvent(MessageEvent): """ def __init__(self, frame, pid, target, x, y): super(PingEvent, self).__init__(frame, pid) + + #: The numerical target type. 0 = to all; 2 = to allies; 4 = to observers. self.target = target - self.x, self.y = x, y + + #: Flag marked true of message was to all. + self.to_all = (self.target == 0) + + #: Flag marked true of message was to allies. + self.to_allies = (self.target == 2) + + #: Flag marked true of message was to observers. + self.to_observers = (self.target == 4) + + #: The x coordinate of the target location + self.x = x + + #: The y coordinate of the target location + self.y = y + + #: The (x,y) coordinate of the target location + self.location = (self.x, self.y) From bb8f9d242b244b00d0b43150fddef24a4dc0f509 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 22 Sep 2013 15:22:40 -0400 Subject: [PATCH 03/53] Misc. documentation improvements. --- docs/source/events/game.rst | 1 - docs/source/events/message.rst | 3 -- docs/source/events/tracker.rst | 1 - sc2reader/events/game.py | 49 +++++++++++++++++------ sc2reader/events/message.py | 14 +++++++ sc2reader/events/tracker.py | 72 +++++++++++++++++++--------------- 6 files changed, 92 insertions(+), 48 deletions(-) diff --git a/docs/source/events/game.rst b/docs/source/events/game.rst index ec253718..e3dd87a0 100644 --- a/docs/source/events/game.rst +++ b/docs/source/events/game.rst @@ -11,4 +11,3 @@ computer player. .. automodule:: sc2reader.events.game :members: - :undoc-members: \ No newline at end of file diff --git a/docs/source/events/message.rst b/docs/source/events/message.rst index fbaf0ae9..ffc57226 100644 --- a/docs/source/events/message.rst +++ b/docs/source/events/message.rst @@ -3,8 +3,5 @@ Message Events =================== -Coming soon! - .. automodule:: sc2reader.events.message :members: - :undoc-members: diff --git a/docs/source/events/tracker.rst b/docs/source/events/tracker.rst index 77df8ccf..36671b51 100644 --- a/docs/source/events/tracker.rst +++ b/docs/source/events/tracker.rst @@ -9,4 +9,3 @@ are also periodically recorded to snapshot aspects of the current game state. .. automodule:: sc2reader.events.tracker :members: - :undoc-members: \ No newline at end of file diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index fc5a2413..584f8b1a 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -16,11 +16,12 @@ class GameEvent(Event): def __init__(self, frame, pid): #: The id of the player generating the event. This is 16 for global non-player events. #: Prior to Heart of the Swarm this was the player id. Since HotS it is - #: now the user id (uid), we still call it pid for backwards compatibility. + #: now the user id (uid), we still call it pid for backwards compatibility. You shouldn't + #: ever need to use this; use :attr:`player` instead. self.pid = pid #: A reference to the :class:`~sc2reader.objects.Player` object representing - #: this player in the replay. Not available for global events (:attr:`pid` = 16) + #: this player in the replay. Not available for global events (:attr:`is_local` = False) self.player = None #: The frame of the game that this event was recorded at. 16 frames per game second. @@ -51,6 +52,9 @@ class GameStartEvent(GameEvent): def __init__(self, frame, pid, data): super(GameStartEvent, self).__init__(frame, pid) + #: ??? + self.data = data + class PlayerLeaveEvent(GameEvent): """ @@ -59,6 +63,9 @@ class PlayerLeaveEvent(GameEvent): def __init__(self, frame, pid, data): super(PlayerLeaveEvent, self).__init__(frame, pid) + #: ??? + self.data = data + class UserOptionsEvent(GameEvent): """ @@ -80,7 +87,7 @@ def __init__(self, frame, pid, data): self.sync_checksumming_enabled = data['sync_checksumming_enabled'] #: - is_map_to_map_transition = data['is_map_to_map_transition'] + self.is_map_to_map_transition = data['is_map_to_map_transition'] #: self.use_ai_beacons = data['use_ai_beacons'] @@ -127,11 +134,31 @@ def __init__(self, frame, pid, data): #: Flags on the command??? self.flags = data['flags'] - #: A dictionary of possible ability flags. Flag names are: alternate, - #: queued, preempt, smart_click, smart_rally, subgroup, set_autocast, - #: set_autocast_on, user, data_a, data_b, data_passenger, data_abil_queue_order_id, - #: ai, ai_ignore_on_finish, is_order, script, homogenous_interruption, - #: minimap, repeat, dispatch_to_other_unit, and target_self + #: A dictionary of possible ability flags. Flags are: + #: + #: * alternate + #: * queued + #: * preempt + #: * smart_click + #: * smart_rally + #: * subgroup + #: * set_autocast, + #: * set_autocast_on + #: * user + #: * data_a + #: * data_b + #: * data_passenger + #: * data_abil_queue_order_id, + #: * ai + #: * ai_ignore_on_finish + #: * is_order + #: * script + #: * homogenous_interruption, + #: * minimap + #: * repeat + #: * dispatch_to_other_unit + #: * target_self + #: self.flag = dict( alternate=0x1 & self.flags != 0, queued=0x2 & self.flags != 0, @@ -446,13 +473,13 @@ class CameraEvent(GameEvent): def __init__(self, frame, pid, data): super(CameraEvent, self).__init__(frame, pid) - #: The x coordinate of the center? of the camera + #: The x coordinate of the center of the camera self.x = (data['target']['x'] if data['target'] is not None else 0)/256.0 - #: The y coordinate of the center? of the camera + #: The y coordinate of the center of the camera self.y = (data['target']['y'] if data['target'] is not None else 0)/256.0 - #: The location of the center? of the camera + #: The location of the center of the camera self.location = (self.x, self.y) #: The distance to the camera target ?? diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index b5166abd..cf99ac91 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -12,8 +12,13 @@ class MessageEvent(Event): Parent class for all message events. """ def __init__(self, frame, pid): + #: The user id (or player id for older replays) of the person that generated the event. self.pid = pid + + #: The frame of the game this event was applied self.frame = frame + + #: The second of the game (game time not real time) this event was applied self.second = frame >> 4 #: Short cut string for event class name @@ -34,10 +39,19 @@ class ChatEvent(MessageEvent): """ def __init__(self, frame, pid, target, text): super(ChatEvent, self).__init__(frame, pid) + #: The numerical target type. 0 = to all; 2 = to allies; 4 = to observers. self.target = target + + #: The text of the message. self.text = text + + #: Flag marked true of message was to all. self.to_all = (self.target == 0) + + #: Flag marked true of message was to allies. self.to_allies = (self.target == 2) + + #: Flag marked true of message was to observers. self.to_observers = (self.target == 4) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 893f0e55..646fb3f8 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -10,9 +10,14 @@ class TrackerEvent(Event): + """ + Parent class for all tracker events. + """ def __init__(self, frames): #: The frame of the game this event was applied self.frame = frames + + #: The second of the game (game time not real time) this event was applied self.second = frames >> 4 #: Short cut string for event class name @@ -30,17 +35,15 @@ def __str__(self): class PlayerStatsEvent(TrackerEvent): """ - Player Stats events are generated for all players that were in the game - even if they've since left every 10 seconds. An additional set of stats - events are generated at the end of the game. + Player Stats events are generated for all players that were in the game even if they've since + left every 10 seconds. An additional set of stats events are generated at the end of the game. - When a player leaves the game, a single PlayerStatsEvent is generated - for that player and no one else. That player continues to generate - PlayerStatsEvents at 10 second intervals until the end of the game. + When a player leaves the game, a single PlayerStatsEvent is generated for that player and no + one else. That player continues to generate PlayerStatsEvents at 10 second intervals until the + end of the game. - In 1v1 games, the above behavior can cause the losing player to have 2 - events generated at the end of the game. One for leaving and one for - the end of the game. + In 1v1 games, the above behavior can cause the losing player to have 2 events generated at the + end of the game. One for leaving and one for the end of the game. """ def __init__(self, frames, data, build): super(PlayerStatsEvent, self).__init__(frames) @@ -213,15 +216,14 @@ def __str__(self): class UnitBornEvent(TrackerEvent): """ - Generated when a unit is created in a finished state in the game. Examples - include the Marine, Zergling, and Zealot (when trained from a gateway). - Units that enter the game unfinished (all buildings, warped in units) generate - a :class:`UnitInitEvent` instead. - - Unfortunately, units that are born do not have events marking their beginnings - like :class:`UnitInitEvent` and :class:`UnitDoneEvent` do. The closest thing to - it are the :class:`~sc2reader.event.game.AbilityEvent` game events where the ability - is a train unit command. + Generated when a unit is created in a finished state in the game. Examples include the Marine, + Zergling, and Zealot (when trained from a gateway). Units that enter the game unfinished (all + buildings, warped in units) generate a :class:`UnitInitEvent` instead. + + Unfortunately, units that are born do not have events marking their beginnings like + :class:`UnitInitEvent` and :class:`UnitDoneEvent` do. The closest thing to it are the + :class:`~sc2reader.event.game.AbilityEvent` game events where the ability is a train unit + command. """ def __init__(self, frames, data, build): super(UnitBornEvent, self).__init__(frames) @@ -253,10 +255,12 @@ def __init__(self, frames, data, build): #: The player object that controls this unit. 0 means neutral unit self.unit_controller = None - #: The x coordinate of the location + #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.x = data[5] * 4 - #: The y coordinate of the location + #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.y = data[6] * 4 #: The map location of the unit birth @@ -292,10 +296,12 @@ def __init__(self, frames, data, build): #: The player object of the that killed the unit. Not always available. self.killer = None - #: The x coordinate of the location + #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.x = data[3] * 4 - #: The y coordinate of the location + #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.y = data[4] * 4 #: The map location the unit was killed at. @@ -394,10 +400,9 @@ def __str__(self): class UnitInitEvent(TrackerEvent): """ - The counter part to :class:`UnitDoneEvent`, generated by the game engine - when a unit is initiated. This applies only to units which are started - in game before they are finished. Primary examples being buildings and - warp-in units. + The counter part to :class:`UnitDoneEvent`, generated by the game engine when a unit is + initiated. This applies only to units which are started in game before they are finished. + Primary examples being buildings and warp-in units. """ def __init__(self, frames, data, build): super(UnitInitEvent, self).__init__(frames) @@ -429,10 +434,12 @@ def __init__(self, frames, data, build): #: The player object that controls this unit. 0 means neutral unit self.unit_controller = None - #: The x coordinate of the location + #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.x = data[5] * 4 - #: The y coordinate of the location + #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. + #: Location prior to rounding marks the center of the unit footprint. self.y = data[6] * 4 #: The map location the unit was started at @@ -444,9 +451,8 @@ def __str__(self): class UnitDoneEvent(TrackerEvent): """ - The counter part to the :class:`UnitInitEvent`, generated by the game engine - when an initiated unit is completed. E.g. warp-in finished, building finished, - morph complete. + The counter part to the :class:`UnitInitEvent`, generated by the game engine when an initiated + unit is completed. E.g. warp-in finished, building finished, morph complete. """ def __init__(self, frames, data, build): super(UnitDoneEvent, self).__init__(frames) @@ -485,7 +491,9 @@ def __init__(self, frames, data, build): #: A dict mapping of units that had their position updated to their positions self.units = dict() - #: A list of (unit_index, (x,y)) derived from the first_unit_index and items + #: A list of (unit_index, (x,y)) derived from the first_unit_index and items. Like the other + #: tracker events, these coordinates have 4 point resolution. (15,25) recorded as (12,24). + #: Location prior to rounding marks the center of the unit footprint. self.positions = list() unit_index = self.first_unit_index From 1d0328316905a6695abcc22862f70f3393e2ba0d Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 22 Sep 2013 18:21:42 -0400 Subject: [PATCH 04/53] First pass at replay resume support. refs #91 --- CHANGELOG.rst | 1 + sc2reader/engine/plugins/context.py | 5 +++++ sc2reader/events/game.py | 14 ++++++++++++++ sc2reader/readers.py | 4 ++-- sc2reader/resources.py | 10 ++++++++++ test_replays/test_all.py | 2 ++ 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9ee1965f..f3d95ef3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ CHANGELOG 0.7.0 - --------------------------- +* Added replay.resume_from_replay flag. See replay.resume_user_info for additional info. * PacketEvent is now ProgressEvent. * SetToHotkeyEvent is now SetControlGroupEvent. * AddToHotkeyEvent is now AddToControlGroupEvent. diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 099569a0..feb013c5 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -99,6 +99,11 @@ def handleResourceTradeEvent(self, event, replay): event.sender = event.player event.recipient = replay.players[event.recipient_id] + def handleHijackReplayGameEvent(self, event, replay): + replay.resume_from_replay = True + replay.resume_method = event.method + replay.resume_user_info = event.user_infos + def handlePlayerStatsEvent(self, event, replay): self.load_tracker_player(event, replay) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 584f8b1a..4f9d6735 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -581,3 +581,17 @@ def __init__(self, frame, pid, data): #: The id of the request being cancelled self.request_id = data['request_id'] + + +class HijackReplayGameEvent(GameEvent): + """ + Generated when players take over from a replay. + """ + def __init__(self, frame, pid, data): + super(HijackReplayGameEvent, self).__init__(frame, pid) + + #: The method used. Not sure what 0/1 represent + self.method = data['method'] + + #: Information on the users hijacking the game + self.user_infos = data['user_infos'] diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 61f8d928..ed9244ce 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1326,7 +1326,7 @@ def __init__(self): 21: (None, self.save_game_event), # New 22: (None, self.save_game_done_event), # Override 23: (None, self.load_game_done_event), # Override - 43: (None, self.hijack_replay_game_event), # New + 43: (HijackReplayGameEvent, self.hijack_replay_game_event), # New 62: (None, self.trigger_target_mode_update_event), # New 101: (PlayerLeaveEvent, self.game_user_leave_event), # New 102: (None, self.game_user_join_event), # New @@ -1356,7 +1356,7 @@ def load_game_done_event(self, data): def hijack_replay_game_event(self, data): return dict( user_infos=[dict( - game_unit_id=data.read_bits(4), + game_user_id=data.read_bits(4), observe=data.read_bits(2), name=data.read_aligned_string(data.read_uint8()), toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, diff --git a/sc2reader/resources.py b/sc2reader/resources.py index f9eeceaa..d9f8c7d4 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -184,6 +184,16 @@ class Replay(Resource): #: SC2 Expansion. One of 'WoL', 'HotS' expasion = str() + #: True of the game was resumed from a replay + resume_from_replay = False + + #: A flag marking which method was used to resume from replay. Unknown interpretation. + resume_method = None + + #: Lists info for each user that is resuming from replay. + resume_user_info = None + + 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 diff --git a/test_replays/test_all.py b/test_replays/test_all.py index dbbfd157..5040b4b9 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -227,6 +227,8 @@ def test_oracle_parsing(self): def test_resume_from_replay(self): replay = sc2reader.load_replay("test_replays/2.0.3.24764/resume_from_replay.SC2Replay") + self.assertTrue(replay.resume_from_replay) + self.assertEqual(replay.resume_method, 0) def test_clan_players(self): replay = sc2reader.load_replay("test_replays/2.0.4.24944/Lunar Colony V.SC2Replay") From e67216866e1d29e0d7b37e862165320a7f362155 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 17 Nov 2013 18:33:45 -0500 Subject: [PATCH 05/53] Remove unused player_names. closes #158. --- sc2reader/resources.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index d9f8c7d4..ba02eebd 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -203,7 +203,6 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.load_level = None #default values, filled in during file read - self.player_names = list() self.other_people = set() self.speed = "" self.type = "" From a3153db03a5ed478b64f3a33a0ea9dbdf3d54856 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 21:03:10 -0500 Subject: [PATCH 06/53] Remove map specific attributes from the listing. People that want map attribute information can run the relevant s2gs files through and get proper mappings for their use case. --- sc2reader/data/attributes.json | 32 +------------------------------- sc2reader/objects.py | 5 ++++- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index cc2149b2..d2d0eab4 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -1,35 +1,5 @@ { "attributes": { - "0001": [ - "Korhal (Urban)", - { - "0001": "Unclassified game type.", - "0002": "4v4 Team Monobattles", - "0003": "Work with your team to select a strong composition of units", - "0004": "Each player must select a single combat unit at the start of the game." - } - ], - "0002": [ - "Rated", - { - "0001": "Yes", - "0002": "No" - } - ], - "0003": [ - "Balance Teams", - { - "0001": "Yes", - "0002": "No" - } - ], - "0004": [ - "Ship Limit", - { - "0001": "No", - "0002": "Yes" - } - ], "0500": [ "Controller", { @@ -1046,4 +1016,4 @@ ] }, "decisions": "(dp0\nc__builtin__\nfrozenset\np1\n((lp2\nS'Hard'\np3\naVHarder\np4\na(I3004\nS'Hard'\np5\ntp6\natp7\nRp8\ng4\nsg1\n((lp9\n(I2001\nS'1v1'\np10\ntp11\naS'1 v 1'\np12\naV1v1\np13\natp14\nRp15\ng13\nsg1\n((lp16\n(I3104\nS'AB04'\np17\ntp18\naS'Agressive Push'\np19\naVAggressive Push\np20\natp21\nRp22\ng20\nsg1\n((lp23\nS'Agressive Push'\np24\naVAggressive Push\np25\na(I3199\nS'AB04'\np26\ntp27\natp28\nRp29\ng25\nsg1\n((lp30\nV6v6\np31\naS'6 v 6'\np32\na(I2001\nS'6v6'\np33\ntp34\natp35\nRp36\ng31\nsg1\n((lp37\nS'Agressive Push'\np38\na(I3102\nS'AB04'\np39\ntp40\naVAggressive Push\np41\natp42\nRp43\ng41\nsg1\n((lp44\nI2003\naVTeams2v2\np45\naS'Team'\np46\natp47\nRp48\ng45\nsg1\n((lp49\nVLadder\np50\na(I3009\nS'Amm'\np51\ntp52\naS'Automated Match Making'\np53\natp54\nRp55\ng50\nsg1\n((lp56\n(I2001\nS'5v5'\np57\ntp58\naS'5 v 5'\np59\naV5v5\np60\natp61\nRp62\ng60\nsg1\n((lp63\nI3141\naVAI Build (Terran)\np64\naS'AI Build'\np65\natp66\nRp67\ng64\nsg1\n((lp68\n(I2001\nS'3v3'\np69\ntp70\naS'3 v 3'\np71\naV3v3\np72\natp73\nRp74\ng72\nsg1\n((lp75\nI3142\naVAI Build (Terran)\np76\naS'AI Build'\np77\natp78\nRp79\ng76\nsg1\n((lp80\n(I3200\nS'AB04'\np81\ntp82\naS'Agressive Push'\np83\naVAggressive Push\np84\natp85\nRp86\ng84\nsg1\n((lp87\nVAI Build (Protoss)\np88\naI3174\naS'AI Build'\np89\natp90\nRp91\ng88\nsg1\n((lp92\nS'Very Hard'\np93\naVElite\np94\na(I3004\nS'VyHd'\np95\ntp96\natp97\nRp98\ng94\nsg1\n((lp99\nS'Agressive Push'\np100\naVAggressive Push\np101\na(I3167\nS'AB04'\np102\ntp103\natp104\nRp105\ng101\nsg1\n((lp106\nI3204\naVAI Build (Zerg)\np107\naS'AI Build'\np108\natp109\nRp110\ng107\nsg1\n((lp111\nVInsane\np112\naS'Cheater 3 (Insane)'\np113\na(I3004\nS'Insa'\np114\ntp115\natp116\nRp117\ng112\nsg1\n((lp118\n(I3007\nS'Watc'\np119\ntp120\naS'Observer'\np121\naS'Watcher'\np122\natp123\nRp124\ng121\nsg1\n((lp125\nI3205\naVAI Build (Zerg)\np126\naS'AI Build'\np127\natp128\nRp129\ng126\nsg1\n((lp130\nVTeams5v5\np131\naI2007\naS'Team'\np132\natp133\nRp134\ng131\nsg1\n((lp135\n(I2001\nS'FFA'\np136\ntp137\naVFFA\np138\naS'Free For All'\np139\natp140\nRp141\ng138\nsg1\n((lp142\nS'Unknown'\np143\naI2012\naS'Team'\np144\natp145\nRp146\ng144\nsg1\n((lp147\nI3206\naVAI Build (Zerg)\np148\naS'AI Build'\np149\natp150\nRp151\ng148\nsg1\n((lp152\n(I3168\nS'AB04'\np153\ntp154\naS'Agressive Push'\np155\naVAggressive Push\np156\natp157\nRp158\ng156\nsg1\n((lp159\nI3172\naVAI Build (Protoss)\np160\naS'AI Build'\np161\natp162\nRp163\ng160\nsg1\n((lp164\nS'Level 1 (Very Easy)'\np165\na(I3004\nS'VyEy'\np166\ntp167\naVVery Easy\np168\natp169\nRp170\ng168\nsg1\n((lp171\nS'Agressive Push'\np172\naVAggressive Push\np173\na(I3135\nS'AB04'\np174\ntp175\natp176\nRp177\ng173\nsg1\n((lp178\nV2v2\np179\naS'2 v 2'\np180\na(I2001\nS'2v2'\np181\ntp182\natp183\nRp184\ng179\nsg1\n((lp185\nS'Agressive Push'\np186\na(I3166\nS'AB04'\np187\ntp188\naVAggressive Push\np189\natp190\nRp191\ng189\nsg1\n((lp192\nVTeamsFFA\np193\naI2006\naS'Team'\np194\natp195\nRp196\ng193\nsg1\n((lp197\nVAI Build (Terran)\np198\naS'AI Build'\np199\naI3143\natp200\nRp201\ng198\nsg1\n((lp202\nVTeams7v7\np203\naI2011\naS'Team'\np204\natp205\nRp206\ng203\nsg1\n((lp207\nVMedium\np208\naS'Level 3 (Medium)'\np209\na(I3004\nS'Medi'\np210\ntp211\natp212\nRp213\ng208\nsg1\n((lp214\nI3140\naVAI Build (Terran)\np215\naS'AI Build'\np216\natp217\nRp218\ng215\nsg1\n((lp219\nS'Level 2 (Easy)'\np220\na(I3004\nS'Easy'\np221\ntp222\naVEasy\np223\natp224\nRp225\ng223\nsg1\n((lp226\n(I3136\nS'AB04'\np227\ntp228\naS'Agressive Push'\np229\naVAggressive Push\np230\natp231\nRp232\ng230\nsg1\n((lp233\nI2008\naVTeams6v6\np234\naS'Team'\np235\natp236\nRp237\ng234\nsg1\n((lp238\nS'Agressive Push'\np239\naVAggressive Push\np240\na(I3103\nS'AB04'\np241\ntp242\natp243\nRp244\ng240\nsg1\n((lp245\nV4v4\np246\naS'4 v 4'\np247\na(I2001\nS'4v4'\np248\ntp249\natp250\nRp251\ng246\nsg1\n((lp252\nS'Agressive Push'\np253\na(I3134\nS'AB04'\np254\ntp255\naVAggressive Push\np256\natp257\nRp258\ng256\nsg1\n((lp259\nVTeams1v1\np260\naI2002\naS'Team'\np261\natp262\nRp263\ng260\nsg1\n((lp264\nI3139\naVAI Build (Terran)\np265\naS'AI Build'\np266\natp267\nRp268\ng265\nsg1\n((lp269\nVAI Build (Zerg)\np270\naS'AI Build'\np271\naI3207\natp272\nRp273\ng270\nsg1\n((lp274\nI3171\naVAI Build (Protoss)\np275\naS'AI Build'\np276\natp277\nRp278\ng275\nsg1\n((lp279\nI3173\naS'AI Build'\np280\naVAI Build (Protoss)\np281\natp282\nRp283\ng281\nsg1\n((lp284\nVTeams3v3\np285\naI2004\naS'Team'\np286\natp287\nRp288\ng285\nsg1\n((lp289\nVAI Build (Protoss)\np290\naS'AI Build'\np291\naI3175\natp292\nRp293\ng290\nsg1\n((lp294\nVTeams4v4\np295\naI2005\naS'Team'\np296\natp297\nRp298\ng295\nsg1\n((lp299\nI3203\naVAI Build (Zerg)\np300\naS'AI Build'\np301\natp302\nRp303\ng300\nsg1\n((lp304\nS'Agressive Push'\np305\na(I3198\nS'AB04'\np306\ntp307\naVAggressive Push\np308\natp309\nRp310\ng308\ns." -} \ No newline at end of file +} diff --git a/sc2reader/objects.py b/sc2reader/objects.py index a0557145..e5391b66 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -62,6 +62,7 @@ def __repr__(self): return str(self) +@log_utils.loggable class Attribute(object): def __init__(self, header, attr_id, player, value): @@ -70,7 +71,9 @@ def __init__(self, header, attr_id, player, value): self.player = player if self.id not in LOBBY_PROPERTIES: - raise ValueError("Unknown attribute id: "+self.id) + self.logger.info("Unknown attribute id: {0}".format(self.id)) + self.name = "Unknown" + self.value = None else: self.name, lookup = LOBBY_PROPERTIES[self.id] self.value = lookup[value.strip("\x00 ")[::-1]] From 6fb12e825f421533ec11a48a5cdcc1ab996ebbb7 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 21:15:20 -0500 Subject: [PATCH 07/53] Raise an informative error on corrupt tracker file This way people can reliably catch this issue and deal with it as they wish. --- sc2reader/exceptions.py | 4 ++++ sc2reader/resources.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sc2reader/exceptions.py b/sc2reader/exceptions.py index 15845101..caf13b86 100644 --- a/sc2reader/exceptions.py +++ b/sc2reader/exceptions.py @@ -10,6 +10,10 @@ class SC2ReaderLocalizationError(SC2ReaderError): pass +class CorruptTrackerFileError(SC2ReaderError): + pass + + class MPQError(SC2ReaderError): pass diff --git a/sc2reader/resources.py b/sc2reader/resources.py index ba02eebd..0a806ca3 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -16,7 +16,7 @@ from sc2reader import readers from sc2reader import exceptions from sc2reader.data import builds as datapacks -from sc2reader.exceptions import SC2ReaderLocalizationError +from sc2reader.exceptions import SC2ReaderLocalizationError, CorruptTrackerFileError from sc2reader.objects import Participant, Observer, Computer, Team, PlayerSummary, Graph, BuildEntry, MapInfo from sc2reader.constants import REGIONS, GAME_SPEED_FACTOR, LOBBY_PROPERTIES @@ -194,7 +194,7 @@ class Replay(Resource): resume_user_info = None - def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, **options): + def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, do_tracker_events=True, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() @@ -289,13 +289,12 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.load_players() # Load tracker events if requested - if load_level >= 3: + if load_level >= 3 and do_tracker_events: 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 @@ -305,6 +304,12 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en # Run this replay through the engine as indicated if engine: + resume_events = [ev for ev in self.game_events if ev.name == 'HijackReplayGameEvent'] + if self.base_build <= 26490 and self.tracker_events and len(resume_events) > 0: + raise CorruptTrackerFileError( + "Cannot run engine on resumed games with tracker events. Run again with the " + + "do_tracker_events=False option to generate context without tracker events.") + engine.run(self) def load_details(self): From caeb6ca36492092f81e1d6c0c8cfc2ecb1fdd939 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 21:18:50 -0500 Subject: [PATCH 08/53] Fix python3 bug in plugin error handling routine. --- sc2reader/engine/engine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index 2788bcc6..b72b80b0 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -178,11 +178,11 @@ def run(self, replay): else: new_events.appendleft(new_event) except Exception as e: - if event_handler.im_self.name in ['ContextLoader']: + if event_handler.__self__.name in ['ContextLoader']: # Certain built in plugins should probably still cause total failure raise # Maybe?? else: - new_event = PluginExit(event_handler.im_self, code=1, details=dict(error=e)) + new_event = PluginExit(event_handler.__self__, code=1, details=dict(error=e)) new_events.append(new_event) event_queue.extendleft(new_events) From 2b35c07e09eceafa47a0d1c9470bf3e5ffe18725 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 22:04:23 -0500 Subject: [PATCH 09/53] Fixes #160 with non back compatible changes. Renames all ability events as following: * AbilityEvent -> CommandEvent * AbilityEvent -> BasicCommandEvent * TargetAbilityEvent -> TargetUnitCommandEvent * LocationAbilityEvent -> TargetPointCommandEvent As such, all references to these classes, statements that check the event name, and engine plugin event handlers need to be renamed. Its not ideal but it is much better than being wrong. --- sc2reader/data/__init__.py | 2 +- sc2reader/engine/engine.py | 18 +++---- sc2reader/engine/plugins/apm.py | 4 +- sc2reader/engine/plugins/context.py | 4 +- sc2reader/events/game.py | 67 ++++++++++++++++----------- sc2reader/events/tracker.py | 2 +- sc2reader/factories/plugins/replay.py | 4 +- sc2reader/scripts/sc2parse.py | 2 +- sc2reader/scripts/sc2replayer.py | 4 +- 9 files changed, 61 insertions(+), 46 deletions(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index fd345a34..c9427572 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -62,7 +62,7 @@ def __init__(self, unit_id): self.killed_by = None #: The unique in-game id for this unit. The id can sometimes be zero because - #: TargetAbilityEvents will create a new unit with id zero when a unit + #: TargetUnitCommandEvents will create a new unit with id zero when a unit #: behind the fog of war is targetted. self.id = unit_id diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index b72b80b0..d527db36 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -18,14 +18,14 @@ class GameEngine(object): Example Usage:: class Plugin1(): - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): pass class Plugin2(): def handleEvent(self, event, replay): pass - def handleTargetAbilityEvent(self, event, replay): + def handleTargetUnitCommandEvent(self, event, replay): pass ... @@ -35,11 +35,11 @@ def handleTargetAbilityEvent(self, event, replay): engine.reigster_plugin(Plugin(5)) engine.run(replay) - Calls functions in the following order for a ``TargetAbilityEvent``:: + Calls functions in the following order for a ``TargetUnitCommandEvent``:: - Plugin1.handleAbilityEvent(event, replay) + Plugin1.handleCommandEvent(event, replay) Plugin2.handleEvent(event, replay) - Plugin2.handleTargetAbilityEvent(event, replay) + Plugin2.handleTargetUnitCommandEvent(event, replay) Plugin Specification @@ -57,7 +57,7 @@ def handleEventName(self, event, replay) * handleMessageEvent - called for events in replay.message.events * handleGameEvent - called for events in replay.game.events * handleTrackerEvent - called for events in replay.tracker.events - * handleAbilityEvent - called for all types of ability events + * handleCommandEvent - called for all types of command events * handleControlGroupEvent - called for all player control group events Plugins may also handle optional ``InitGame`` and ``EndGame`` events generated @@ -90,7 +90,7 @@ def handleEvent(self, event, replay): return ... - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): try: possibly_throwing_error() catch Error as e: @@ -204,8 +204,8 @@ def _get_plugin_event_handlers(self, plugin, event): handlers.append(getattr(plugin, 'handleGameEvent', None)) if isinstance(event, TrackerEvent) and hasattr(plugin, 'handleTrackerEvent'): handlers.append(getattr(plugin, 'handleTrackerEvent', None)) - if isinstance(event, AbilityEvent) and hasattr(plugin, 'handleAbilityEvent'): - handlers.append(getattr(plugin, 'handleAbilityEvent', None)) + if isinstance(event, CommandEvent) and hasattr(plugin, 'handleCommandEvent'): + handlers.append(getattr(plugin, 'handleCommandEvent', None)) if isinstance(event, ControlGroupEvent) and hasattr(plugin, 'handleControlGroupEvent'): handlers.append(getattr(plugin, 'handleControlGroupEvent', None)) if hasattr(plugin, 'handle'+event.name): diff --git a/sc2reader/engine/plugins/apm.py b/sc2reader/engine/plugins/apm.py index 42d44185..137e1458 100644 --- a/sc2reader/engine/plugins/apm.py +++ b/sc2reader/engine/plugins/apm.py @@ -7,7 +7,7 @@ class APMTracker(object): """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is - any Selection, ControlGroup, or Ability event. + any Selection, ControlGroup, or Command event. Also provides ``player.avg_apm`` which is defined as the sum of all the above actions divided by the number of seconds played by the player (not @@ -31,7 +31,7 @@ def handleSelectionEvent(self, event, replay): event.player.aps[event.second] += 1 event.player.apm[int(event.second/60)] += 1 - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): event.player.aps[event.second] += 1 event.player.apm[int(event.second/60)] += 1 diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index feb013c5..b6b67af9 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -20,7 +20,7 @@ def handleGameEvent(self, event, replay): def handleMessageEvent(self, event, replay): self.load_message_game_player(event, replay) - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): if not replay.datapack: return @@ -43,7 +43,7 @@ def handleAbilityEvent(self, event, replay): elif event.other_unit_id is not None: self.logger.error("Other unit {0} not found".format(event.other_unit_id)) - def handleTargetAbilityEvent(self, event, replay): + def handleTargetUnitCommandEvent(self, event, replay): if not replay.datapack: return diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 4f9d6735..29945a4e 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -105,31 +105,31 @@ def __init__(self, frame, pid, data): def create_command_event(frame, pid, data): ability_type = data['data'][0] if ability_type == 'None': - return AbilityEvent(frame, pid, data) + return BasicCommandEvent(frame, pid, data) elif ability_type == 'TargetUnit': - return TargetAbilityEvent(frame, pid, data) + return TargetUnitCommandEvent(frame, pid, data) elif ability_type == 'TargetPoint': - return LocationAbilityEvent(frame, pid, data) + return TargetPointCommandEvent(frame, pid, data) elif ability_type == 'Data': - return SelfAbilityEvent(frame, pid, data) + return DataCommandEvent(frame, pid, data) @loggable -class AbilityEvent(GameEvent): +class CommandEvent(GameEvent): """ Ability events are generated when ever a player in the game issues a command to a unit or group of units. They are split into three subclasses of ability, each with their own set of associated data. The attributes listed below are shared across all ability event types. - See :class:`LocationAbilityEvent`, :class:`TargetAbilityEvent`, and :class:`SelfAbilityEvent` - for individual details. + See :class:`TargetPointCommandEvent`, :class:`TargetUnitCommandEvent`, and + :class:`DataCommandEvent` for individual details. """ def __init__(self, frame, pid, data): - super(AbilityEvent, self).__init__(frame, pid) + super(CommandEvent, self).__init__(frame, pid) #: Flags on the command??? self.flags = data['flags'] @@ -235,25 +235,38 @@ def __str__(self): return string -class LocationAbilityEvent(AbilityEvent): +class BasicCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` + + This event is recorded for events that have no extra information recorded. + + Note that like all CommandEvents, the event will be recorded regardless + of whether or not the command was successful. + """ + def __init__(self, frame, pid, data): + super(TargetPointCommandEvent, self).__init__(frame, pid, data) + + +class TargetPointCommandEvent(CommandEvent): + """ + Extends :class:`CommandEvent` This event is recorded when ever a player issues a command that targets a location and NOT a unit. Commands like Psistorm, Attack Move, Fungal Growth, and EMP fall under this category. - Note that like all AbilityEvents, the event will be recorded regardless + Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ def __init__(self, frame, pid, data): - super(LocationAbilityEvent, self).__init__(frame, pid, data) + super(TargetPointCommandEvent, self).__init__(frame, pid, data) #: The x coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.x = self.ability_type_data['point'].get('x', 0)/4096.0 + self.x = self.ability_type_data['point'].get('x', 0) / 4096.0 #: The y coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.y = self.ability_type_data['point'].get('y', 0)/4096.0 + self.y = self.ability_type_data['point'].get('y', 0) / 4096.0 #: The z coordinate of the target. Available for TargetPoint and TargetUnit type events. self.z = self.ability_type_data['point'].get('z', 0) @@ -262,18 +275,19 @@ def __init__(self, frame, pid, data): self.location = (self.x, self.y, self.z) -class TargetAbilityEvent(AbilityEvent): +class TargetUnitCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` - TargetAbilityEvents are recorded when ever a player issues a command that targets a unit. + This event is recorded when ever a player issues a command that targets a unit. The location of the target unit at the time of the command is also recorded. Commands like Chronoboost, Transfuse, and Snipe fall under this category. - Note that all AbilityEvents are recorded regardless of whether or not the command was successful. + Note that like all CommandEvents, the event will be recorded regardless + of whether or not the command was successful. """ def __init__(self, frame, pid, data): - super(TargetAbilityEvent, self).__init__(frame, pid, data) + super(TargetUnitCommandEvent, self).__init__(frame, pid, data) #: Flags set on the target unit. Available for TargetUnit type events self.target_flags = self.ability_type_data.get('flags', None) @@ -301,10 +315,10 @@ def __init__(self, frame, pid, data): self.upkeep_player_id = self.ability_type_data.get('upkeep_player_id', None) #: The x coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.x = self.ability_type_data['point'].get('x', 0)/4096.0 + self.x = self.ability_type_data['point'].get('x', 0) / 4096.0 #: The y coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.y = self.ability_type_data['point'].get('y', 0)/4096.0 + self.y = self.ability_type_data['point'].get('y', 0) / 4096.0 #: The z coordinate of the target. Available for TargetPoint and TargetUnit type events. self.z = self.ability_type_data['point'].get('z', 0) @@ -313,17 +327,18 @@ def __init__(self, frame, pid, data): self.location = (self.x, self.y, self.z) -class SelfAbilityEvent(AbilityEvent): +class DataCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` - SelfAbilityEvents are recorded when ever a player issues a command that has no target. Commands + DataCommandEvent are recorded when ever a player issues a command that has no target. Commands like Burrow, SeigeMode, Train XYZ, and Stop fall under this category. - Note that all AbilityEvents are recorded regardless of whether or not the command was successful. + Note that like all CommandEvents, the event will be recorded regardless + of whether or not the command was successful. """ def __init__(self, frame, pid, data): - super(SelfAbilityEvent, self).__init__(frame, pid, data) + super(DataCommandEvent, self).__init__(frame, pid, data) #: Other target data. Available for Data type events. self.target_data = self.ability_type_data.get('data', None) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 646fb3f8..b8967150 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -222,7 +222,7 @@ class UnitBornEvent(TrackerEvent): Unfortunately, units that are born do not have events marking their beginnings like :class:`UnitInitEvent` and :class:`UnitDoneEvent` do. The closest thing to it are the - :class:`~sc2reader.event.game.AbilityEvent` game events where the ability is a train unit + :class:`~sc2reader.event.game.CommandEvent` game events where the command is a train unit command. """ def __init__(self, frames, data, build): diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 40401196..ad4c3efb 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -94,7 +94,7 @@ def toDict(replay): def APMTracker(replay): """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is - any Selection, Hotkey, or Ability event. + any Selection, Hotkey, or Command event. Also provides ``player.avg_apm`` which is defined as the sum of all the above actions divided by the number of seconds played by the player (not @@ -106,7 +106,7 @@ def APMTracker(replay): player.seconds_played = replay.length.seconds for event in player.events: - if event.name == 'SelectionEvent' or 'AbilityEvent' in event.name or 'ControlGroup' in event.name: + if event.name == 'SelectionEvent' or 'CommandEvent' in event.name or 'ControlGroup' in event.name: player.aps[event.second] += 1 player.apm[int(event.second/60)] += 1 diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index b39fd432..d0808340 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -48,7 +48,7 @@ def main(): human.pids = set([human.pid for human in replay.humans]) event_pids = set([event.player.pid for event in replay.events if getattr(event, 'player', None)]) player_pids = set([player.pid for player in replay.players if player.is_human]) - ability_pids = set([event.player.pid for event in replay.events if 'AbilityEvent' in event.name]) + ability_pids = set([event.player.pid for event in replay.events if 'CommandEvent' in event.name]) if human.pids != event_pids: print('Event Pid problem! pids={pids} but event pids={event_pids}'.format(pids=human.pids, event_pids=event_pids)) print(' with {path}: {build} - {real_type} on {map_name} - Played {start_time}'.format(path=path, **replay.__dict__)) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index cf373ed1..7a72c7ed 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -45,7 +45,7 @@ def getch(): def main(): parser = argparse.ArgumentParser( description="""Step by step replay of game events; shows only the - Initialization, Ability, and Selection events by default. Press any + Initialization, Command, and Selection events by default. Press any key to advance through the events in sequential order.""" ) @@ -77,7 +77,7 @@ def main(): # Loop through the events for event in events: - if isinstance(event, AbilityEvent) or \ + if isinstance(event, CommandEvent) or \ isinstance(event, SelectionEvent) or \ isinstance(event, PlayerLeaveEvent) or \ isinstance(event, GameStartEvent) or \ From 72da6aeae6ef1bbf9dc45fa544879d50d1fb87f6 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 22:19:08 -0500 Subject: [PATCH 10/53] Remove some old % style string formatting. --- sc2reader/decoders.py | 2 +- sc2reader/events/game.py | 6 +++--- sc2reader/events/message.py | 2 +- sc2reader/objects.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index bb0bbd4c..63324649 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -374,6 +374,6 @@ def read_struct(self, datatype=None): data = self.read_vint() else: - raise TypeError("Unknown Data Structure: '%s'" % datatype) + raise TypeError("Unknown Data Structure: '{0}'".format(datatype)) return data diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 29945a4e..998940ef 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -38,7 +38,7 @@ def __init__(self, frame, pid): def _str_prefix(self): player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" - return "%s\t%-15s " % (Length(seconds=int(self.frame/16)), player_name) + return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) def __str__(self): return self._str_prefix() + self.name @@ -547,7 +547,7 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}" % (self.minerals, self.vespene, self.terrazine, self.custom, self.reciever) + return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format(self.minerals, self.vespene, self.terrazine, self.custom, self.recipient) class ResourceRequestEvent(GameEvent): @@ -573,7 +573,7 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom" % (self.minerals, self.vespene, self.terrazine, self.custom) + return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format(self.minerals, self.vespene, self.terrazine, self.custom) class ResourceRequestFulfillEvent(GameEvent): diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index cf99ac91..e9859733 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -26,7 +26,7 @@ def __init__(self, frame, pid): def _str_prefix(self): player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" - return "%s\t%-15s " % (Length(seconds=int(self.frame/16)), player_name) + return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) def __str__(self): return self._str_prefix() + self.name diff --git a/sc2reader/objects.py b/sc2reader/objects.py index e5391b66..784ef9e2 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -82,7 +82,7 @@ def __repr__(self): return str(self) def __str__(self): - return "[%s] %s: %s" % (self.player, self.name, self.value) + return "[{0}] {1}: {2}".format(self.player, self.name, self.value) class Entity(object): From 6d13524ba8fbde2fa457189811eb0828666bf4c4 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 22:22:13 -0500 Subject: [PATCH 11/53] And this is why you run tests before pushing. refs #160 --- sc2reader/events/game.py | 2 +- test_replays/test_all.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 998940ef..e3948f26 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -245,7 +245,7 @@ class BasicCommandEvent(CommandEvent): of whether or not the command was successful. """ def __init__(self, frame, pid, data): - super(TargetPointCommandEvent, self).__init__(frame, pid, data) + super(BasicCommandEvent, self).__init__(frame, pid, data) class TargetPointCommandEvent(CommandEvent): diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 5040b4b9..ba7c0ea8 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -197,20 +197,20 @@ def test_hots_pids(self): replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") player_pids = set([player.pid for player in replay.players if player.is_human]) - ability_pids = set([event.player.pid for event in replay.events if "AbilityEvent" in event.name]) + ability_pids = set([event.player.pid for event in replay.events if "CommandEvent" in event.name]) self.assertEqual(ability_pids, player_pids) def test_wol_pids(self): replay = sc2reader.load_replay("test_replays/1.5.4.24540/ggtracker_1471849.SC2Replay") self.assertEqual(replay.expansion, "WoL") - ability_pids = set([event.player.pid for event in replay.events if "AbilityEvent" in event.name]) + ability_pids = set([event.player.pid for event in replay.events if "CommandEvent" in event.name]) player_pids = set([player.pid for player in replay.players]) self.assertEqual(ability_pids, player_pids) def test_hots_hatchfun(self): replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") player_pids = set([ player.pid for player in replay.players]) - spawner_pids = set([ event.player.pid for event in replay.events if "TargetAbilityEvent" in event.name and event.ability.name == "SpawnLarva"]) + spawner_pids = set([ event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva"]) self.assertTrue(spawner_pids.issubset(player_pids)) def test_hots_vs_ai(self): @@ -331,7 +331,7 @@ def test_gameheartnormalizer_plugin(self): # Not a GameHeart game! replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") player_pids = set([ player.pid for player in replay.players]) - spawner_pids = set([ event.player.pid for event in replay.events if "TargetAbilityEvent" in event.name and event.ability.name == "SpawnLarva"]) + spawner_pids = set([ event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva"]) self.assertTrue(spawner_pids.issubset(player_pids)) replay = sc2reader.load_replay("test_replays/gameheart/gameheart.SC2Replay") From 665cc1245f60f6c2b0bd837a664390b719ab5bd5 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 1 Dec 2013 22:28:11 -0500 Subject: [PATCH 12/53] Make note of the additional changes in the log. --- CHANGELOG.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f3d95ef3..2f99f148 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,12 +4,19 @@ CHANGELOG 0.7.0 - --------------------------- +* Now a CorruptTrackerFileError is raised when the tracker file is corrupted (generally only older resume_from_replay replays) * Added replay.resume_from_replay flag. See replay.resume_user_info for additional info. * PacketEvent is now ProgressEvent. * SetToHotkeyEvent is now SetControlGroupEvent. * AddToHotkeyEvent is now AddToControlGroupEvent. * GetFromHotkeyEvent is now GetControlGroupEvent. * PlayerAbilityEvent is no longer part of the event hierarchy. +* AbilityEvent doubled as both an abstract and concrete class (very bad, see #160). Now split into: + * AbilityEvent is now CommandEvent + * AbilityEvent is now BasicCommandEvent +* TargetAbilityEvent is now TargetUnitCommandEvent +* LocationAbilityEvent is now TargetPointCommandEvent +* Removed the defunct replay.player_names attribute. * event.name is no longer a class property; it can only be accessed from an event instance. * PingEvents now have new attributes: * event.to_all - true if ping seen by all From 1d9c141e4db4a972bea2ec2058f3e410de1e9894 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Thu, 5 Dec 2013 22:12:31 -0800 Subject: [PATCH 13/53] adding a missing change from the list --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f99f148..cc80d239 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,7 @@ CHANGELOG * AbilityEvent is now BasicCommandEvent * TargetAbilityEvent is now TargetUnitCommandEvent * LocationAbilityEvent is now TargetPointCommandEvent +* SelfAbilityEvent is now DataCommandEvent * Removed the defunct replay.player_names attribute. * event.name is no longer a class property; it can only be accessed from an event instance. * PingEvents now have new attributes: From fe86668a8dd67c1bd7ea24d0ad035629c05b7306 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 21:22:04 -0500 Subject: [PATCH 14/53] Remove a couple more defunct replay attributes. --- CHANGELOG.rst | 3 +++ sc2reader/resources.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index cc80d239..0de49ee4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,9 @@ CHANGELOG * LocationAbilityEvent is now TargetPointCommandEvent * SelfAbilityEvent is now DataCommandEvent * Removed the defunct replay.player_names attribute. +* Removed the defunct replay.events_by_type attribute. +* Removed the defunct replay.other_people attribute. + * event.name is no longer a class property; it can only be accessed from an event instance. * PingEvents now have new attributes: * event.to_all - true if ping seen by all diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 0a806ca3..9ad0ed26 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -203,7 +203,6 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.load_level = None #default values, filled in during file read - self.other_people = set() self.speed = "" self.type = "" self.game_type = "" @@ -215,7 +214,6 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.map_hash = "" self.gateway = "" self.events = list() - self.events_by_type = defaultdict(list) self.teams, self.team = list(), dict() self.player = utils.PersonDict() From 9283c33842f924e3e1c781f0fe0c62b4c92d02a3 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 22:46:19 -0500 Subject: [PATCH 15/53] Use generic UnitType and Ability classes for data. The dynamically created classes don't play well with pickle and were unncessarily complex. The only change here is that you can't use this anymore. unit._type_class.__class__.__name__ Instead you can use the shorter: unit._type_class.name No problem. --- CHANGELOG.rst | 1 + sc2reader/data/__init__.py | 77 ++++++++++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0de49ee4..2e7705b5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ CHANGELOG 0.7.0 - --------------------------- +* Use generic UnitType and Ability classes for data. This means no more unit._type_class.__class__.__name__. But hopefully people were not doing that anyway. * Now a CorruptTrackerFileError is raised when the tracker file is corrupted (generally only older resume_from_replay replays) * Added replay.resume_from_replay flag. See replay.resume_user_info for additional info. * PacketEvent is now ProgressEvent. diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index c9427572..9d51723b 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -33,10 +33,7 @@ class Unit(object): - """ - Represents an in-game unit. - """ - + """Represents an in-game unit.""" def __init__(self, unit_id): #: A reference to the player that currently owns this unit. Only available for 2.0.8+ replays. self.owner = None @@ -195,22 +192,64 @@ def __repr__(self): return str(self) +class UnitType(object): + """ Represents an in game unit type """ + def __init__(self, type_id, str_id=None, name=None, title=None, race=None, minerals=0, + vespene=0, supply=0, is_building=False, is_worker=False, is_army=False): + #: The internal integer id representing this unit type + self.id = type_id + + #: The internal string id representing this unit type + self.str_id = str_id + + #: The name of this unit type + self.name = name + + #: The printable title of this unit type; has spaces and possibly punctuation + self.title = title + + #: The race this unit type belongs to + self.race = race + + #: The mineral cost of this unit type + self.minerals = minerals + + #: The vespene cost of this unit type + self.vespene = vespene + + #: The supply cost of this unit type + self.supply = supply + + #: Boolean flagging this unit type as a building + self.is_building = is_building + + #: Boolean flagging this unit type as a worker + self.is_worker = is_worker + + #: Boolean flagging this unit type as an army unit + self.is_army = is_army + + class Ability(object): + """ Represents an in-game ability """ + def __init__(self, id, name=None, title=None, is_build=False, build_time=0, build_unit=None): + #: The internal integer id representing this ability. + self.id = id - #: The internal integer id representing this ability. - id = None + #: The name of this ability + self.name = name - #: The name of this ability - name = "" + #: The printable title of this ability; has spaces and possibly punctuation. + self.title = title - #: Boolean flagging this ability as creating a new unit. - is_build = False + #: Boolean flagging this ability as creating a new unit. + self.is_build = is_build - #: The number of seconds required to build this unit. 0 if not ``is_build``. - build_time = 0 + #: The number of seconds required to build this unit. 0 if not ``is_build``. + self.build_time = build_time - #: A reference to the :class:`Unit` type built by this ability. None if not ``is_build``. - build_unit = None + #: A reference to the :class:`UnitType` type built by this ability. Default to None. + self.build_unit = build_unit @loggable @@ -260,20 +299,20 @@ def change_type(self, unit, new_type, frame): self.logger.error("Unable to change type of {0} to {1} [frame {2}]; unit type not found in build {3}".format(unit, new_type, frame, self.id)) def add_ability(self, ability_id, name, title=None, is_build=False, build_time=None, build_unit=None): - ability = type(str(name), (Ability,), dict( - id=ability_id, + ability = Ability( + ability_id, name=name, title=title or name, is_build=is_build, build_time=build_time, build_unit=build_unit - )) + ) setattr(self, name, ability) self.abilities[ability_id] = ability def add_unit_type(self, type_id, str_id, name, title=None, race='Neutral', minerals=0, vespene=0, supply=0, is_building=False, is_worker=False, is_army=False): - unit = type(str(name), (Unit,), dict( - id=type_id, + unit = UnitType( + type_id, str_id=str_id, name=name, title=title or name, From 4d068bf69e1a2eb03385b979a2cf3d03b7e170d0 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 22:51:20 -0500 Subject: [PATCH 16/53] datapacks is a better word than builds here. --- sc2reader/data/__init__.py | 2 +- sc2reader/resources.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 9d51723b..6edf14a7 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -388,4 +388,4 @@ def load_build(expansion, version): for version in ('base', '23925', '24247', '24764'): hots_builds[version] = load_build('HotS', version) -builds = {'WoL': wol_builds, 'HotS': hots_builds} +datapacks = builds = {'WoL': wol_builds, 'HotS': hots_builds} diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 9ad0ed26..c89571fe 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -4,7 +4,6 @@ from collections import defaultdict, namedtuple from datetime import datetime import hashlib -import sys from xml.etree import ElementTree import zlib @@ -15,7 +14,7 @@ from sc2reader import log_utils from sc2reader import readers from sc2reader import exceptions -from sc2reader.data import builds as datapacks +from sc2reader.data import datapacks from sc2reader.exceptions import SC2ReaderLocalizationError, CorruptTrackerFileError from sc2reader.objects import Participant, Observer, Computer, Team, PlayerSummary, Graph, BuildEntry, MapInfo from sc2reader.constants import REGIONS, GAME_SPEED_FACTOR, LOBBY_PROPERTIES From 0e6286fc868a6dabf7b33e8a8ff5d6e52891416b Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:20:57 -0500 Subject: [PATCH 17/53] Replace all references to gateway with region. --- CHANGELOG.rst | 1 + sc2reader/constants.py | 2 +- sc2reader/factories/plugins/replay.py | 2 +- sc2reader/objects.py | 13 +-------- sc2reader/resources.py | 40 +++++++++++++-------------- sc2reader/utils.py | 2 +- 6 files changed, 25 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2e7705b5..37482c05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ CHANGELOG 0.7.0 - --------------------------- +* All references to the gateway attribute have been replaced in favor of region; e.g. replay.region * Use generic UnitType and Ability classes for data. This means no more unit._type_class.__class__.__name__. But hopefully people were not doing that anyway. * Now a CorruptTrackerFileError is raised when the tracker file is corrupted (generally only older resume_from_replay replays) * Added replay.resume_from_replay flag. See replay.resume_user_info for additional info. diff --git a/sc2reader/constants.py b/sc2reader/constants.py index fa65d1c0..7b701dee 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -108,7 +108,7 @@ COLOR_CODES_INV = dict(zip(COLOR_CODES.values(), COLOR_CODES.keys())) -REGIONS = { +SUBREGIONS = { # United States 'us': { 1: 'us', diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index ad4c3efb..f6b19e79 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -62,7 +62,7 @@ def toDict(replay): # Consolidate replay metadata into dictionary return { - 'gateway': getattr(replay, 'gateway', None), + 'region': getattr(replay, 'region', None), 'map_name': getattr(replay, 'map_name', None), 'file_time': getattr(replay, 'file_time', None), 'filehash': getattr(replay, 'filehash', None), diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 784ef9e2..e7e096b3 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -124,9 +124,6 @@ def __init__(self, sid, slot_data): #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[int(parts[0])] - #: Deprecated, see Entity.region - self.gateway = self.region - #: The Battle.net subregion the entity is registered to self.subregion = int(parts[2]) @@ -198,9 +195,6 @@ def __init__(self, pid, detail_data, attribute_data): #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[detail_data['bnet']['region']] - #: Deprecated, see `Player.region` - self.gateway = self.region - #: The Battle.net subregion the entity is registered to self.subregion = detail_data['bnet']['subregion'] @@ -346,12 +340,7 @@ class PlayerSummary(): #: Subregion id of player subregion = int() - #: The player's gateway, such as us, eu - gateway = str() - - #: The player's region, such as na, la, eu or ru. This is - # provided for convenience, but as of 20121018 is strictly a - # function of gateway and subregion. + #: The player's region, such as us, eu, sea region = str() #: unknown1 diff --git a/sc2reader/resources.py b/sc2reader/resources.py index c89571fe..80fbb0e3 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -17,7 +17,7 @@ from sc2reader.data import datapacks from sc2reader.exceptions import SC2ReaderLocalizationError, CorruptTrackerFileError from sc2reader.objects import Participant, Observer, Computer, Team, PlayerSummary, Graph, BuildEntry, MapInfo -from sc2reader.constants import REGIONS, GAME_SPEED_FACTOR, LOBBY_PROPERTIES +from sc2reader.constants import GAME_SPEED_FACTOR, LOBBY_PROPERTIES class Resource(object): @@ -120,8 +120,8 @@ class Replay(Resource): #: 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() + #: The region the game was played on: us, eu, sea, etc + region = str() #: An integrated list of all the game events events = list() @@ -211,7 +211,7 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.is_private = False self.map = None self.map_hash = "" - self.gateway = "" + self.region = "" self.events = list() self.teams, self.team = list(), dict() @@ -329,13 +329,13 @@ def load_details(self): self.map_name = details['map_name'] - self.gateway = details['cache_handles'][0].server.lower() + self.region = 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' + if self.region == 'sg': + self.region = 'sea' dependency_hashes = [d.hash for d in details['cache_handles']] if hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: @@ -467,7 +467,7 @@ def get_team(team_id): self.recorder = None entity_names = sorted(map(lambda p: p.name, self.entities)) - hash_input = self.gateway+":"+','.join(entity_names) + hash_input = self.region+":"+','.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 @@ -616,17 +616,17 @@ class Map(Resource): #: The map description as written by author description = str() - def __init__(self, map_file, filename=None, gateway=None, map_hash=None, **options): + def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): super(Map, self).__init__(map_file, filename, **options) #: The unique hash used to identify this map on bnet's depots. self.hash = map_hash - #: The gateway this map was posted to. Maps must be posted individually to each gateway. - self.gateway = gateway + #: The region this map was posted to. Maps must be posted individually to each region. + self.region = region #: A URL reference to the location of this map on bnet's depots. - self.url = Map.get_url(gateway, map_hash) + self.url = Map.get_url(self.region, map_hash) #: The opened MPQArchive for this map self.archive = mpyq.MPQArchive(map_file) @@ -672,12 +672,12 @@ def __init__(self, map_file, filename=None, gateway=None, map_hash=None, **optio self.dependencies.append(dependency_node.text) @classmethod - def get_url(cls, gateway, map_hash): + def get_url(cls, region, map_hash): """Builds a download URL for the map from its components.""" - if gateway and map_hash: + if region and map_hash: # it seems like sea maps are stored on us depots. - gateway = 'us' if gateway == 'sea' else gateway - return cls.url_template.format(gateway, map_hash) + region = 'us' if region == 'sea' else region + return cls.url_template.format(region, map_hash) else: return None @@ -837,8 +837,8 @@ def load_translations(self): files.append(utils.DepotFile(file_hash)) self.localization_urls[language] = files - # Grab the gateway from the one of the files - self.gateway = list(self.localization_urls.values())[0][0].server.lower() + # Grab the region from the one of the files + self.region = list(self.localization_urls.values())[0][0].server.lower() # Each of the localization urls points to an XML file with a set of # localization strings and their unique ids. After reading these mappings @@ -1008,9 +1008,9 @@ def load_players(self): settings = self.player_settings[index] player.is_ai = not isinstance(struct[0][1], dict) if not player.is_ai: - player.gateway = self.gateway + player.region = self.region player.subregion = struct[0][1][0][2] - player.region = REGIONS[player.gateway].get(player.subregion, 'Unknown') + player.region = REGIONS[player.region].get(player.subregion, 'Unknown') player.bnetid = struct[0][1][0][3] player.unknown1 = struct[0][1][0] player.unknown2 = struct[0][1][1] diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 94fb5637..e728c983 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -328,7 +328,7 @@ def toDict(replay): # Consolidate replay metadata into dictionary return { - 'gateway': getattr(replay, 'gateway', None), + 'region': getattr(replay, 'region', None), 'map_name': getattr(replay, 'map_name', None), 'file_time': getattr(replay, 'file_time', None), 'filehash': getattr(replay, 'filehash', None), From d9df519232dd9ac73ba83ed69d9388f4561eaf13 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:21:45 -0500 Subject: [PATCH 18/53] Fix typo. --- sc2reader/data/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 6edf14a7..f4e095f4 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -323,7 +323,7 @@ def add_unit_type(self, type_id, str_id, name, title=None, race='Neutral', miner is_building=is_building, is_worker=is_worker, is_army=is_army, - )) + ) setattr(self, name, unit) self.units[type_id] = unit self.units[str_id] = unit From bd2a8cf23b6450cccbbff7512ef19f118427189e Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:22:28 -0500 Subject: [PATCH 19/53] Use a regular dict for raw data results. --- sc2reader/readers.py | 4 ++-- sc2reader/resources.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index ed9244ce..4ac04c3a 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -8,7 +8,7 @@ from sc2reader.events.game import * from sc2reader.events.message import * from sc2reader.events.tracker import * -from sc2reader.utils import AttributeDict, DepotFile +from sc2reader.utils import DepotFile from sc2reader.decoders import BitPackedDecoder, ByteDecoder @@ -204,7 +204,7 @@ def __call__(self, data, replay): data.byte_align() - return AttributeDict(pings=pings, messages=messages, packets=packets) + return dict(pings=pings, messages=messages, packets=packets) class GameEventsReader_Base(object): diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 80fbb0e3..ff4c3db4 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -480,9 +480,9 @@ 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.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) From f58daf7365ef04a041eb934de58701cd592bba00 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:23:27 -0500 Subject: [PATCH 20/53] Remove stray reference. --- sc2reader/resources.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index ff4c3db4..58e861a1 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1010,7 +1010,6 @@ def load_players(self): if not player.is_ai: player.region = self.region player.subregion = struct[0][1][0][2] - player.region = REGIONS[player.region].get(player.subregion, 'Unknown') player.bnetid = struct[0][1][0][3] player.unknown1 = struct[0][1][0] player.unknown2 = struct[0][1][1] From b968b577f8771eaf6c32c07e5e588259cb47a8f6 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:24:12 -0500 Subject: [PATCH 21/53] Misc import and documentation improvements. --- sc2reader/__init__.py | 56 +++++++++++++++++++++++++++++++-- sc2reader/events/__init__.py | 5 ++- sc2reader/factories/__init__.py | 2 +- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 60b5eead..040febcb 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -1,4 +1,23 @@ # -*- coding: utf-8 -*- +""" + sc2reader + ~~~~~~~~~~~ + + A library for loading data from Starcraft II game resources. + + SC2Factory methods called on the package will be delegated to the default + SC2Factory. To default to a cached factory set one or more of the following + variables in your environment: + + SC2READER_CACHE_DIR = '/absolute/path/to/existing/cache/directory/' + SC2READER_CACHE_MAX_SIZE = MAXIMUM_CACHE_ENTRIES_TO_HOLD_IN_MEMORY + + You can also set the default factory via setFactory, useFileCache, useDictCache, + or useDoubleCache functions. + + :copyright: (c) 2011 by Graylin Kim. + :license: MIT, see LICENSE for more details. +""" from __future__ import absolute_import, print_function, unicode_literals, division __version__ = "0.6.4" @@ -13,12 +32,25 @@ # setup the library logging log_utils.setup() -# For backwards compatibility +# For backwards compatibility, goes away in 0.7 SC2Reader = factories.SC2Factory def setFactory(factory): - # Expose a nice module level interface + """ + :param factory: The new default factory for the package. + + Links the following sc2reader global methods to the specified factory:: + + * sc2reader.load_replay(s) + * sc2reader.load_map(s) + * sc2reader.load_game_summar(y|ies) + * sc2reader.configure + * sc2reader.reset + * sc2reader.register_plugin + + These methods when called will delegate to the factory for execution. + """ module = sys.modules[__name__] module.load_replays = factory.load_replays module.load_replay = factory.load_replay @@ -35,14 +67,34 @@ def setFactory(factory): def useFileCache(cache_dir, **options): + """ + :param cache_dir: Absolute path to the existing cache directory + + Set the default factory to a new FileCachedSC2Factory with the given cache_dir. + All remote resources are saved to the file system for faster access times. + """ setFactory(factories.FileCachedSC2Factory(cache_dir, **options)) def useDictCache(cache_max_size=0, **options): + """ + :param cache_max_size: The maximum number of cache entries to hold in memory + + Set the default factory to a new DictCachedSC2Factory with the given cache_dir. + A limited number of remote resources are cached in memory for faster access times. + """ setFactory(factories.DictCachedSC2Factory(cache_max_size, **options)) def useDoubleCache(cache_dir, cache_max_size=0, **options): + """ + :param cache_dir: Absolute path to the existing cache directory + :param cache_max_size: The maximum number of cache entries to hold in memory + + Set the default factory to a new DoubleCachedSC2Factory with the given cache_dir. + A limited number of remote resources are cached in memory for faster access times. + All remote resources are saved to the file system for faster access times. + """ setFactory(factories.DoubleCachedSC2Factory(cache_dir, cache_max_size, **options)) diff --git a/sc2reader/events/__init__.py b/sc2reader/events/__init__.py index db2cc164..6ceaa632 100644 --- a/sc2reader/events/__init__.py +++ b/sc2reader/events/__init__.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals, division -import sc2reader.events.base -import sc2reader.events.game -import sc2reader.events.message +# Export all events of all types to the package interface +from sc2reader.events import base, game, message, tracker from sc2reader.events.base import * from sc2reader.events.game import * from sc2reader.events.message import * diff --git a/sc2reader/factories/__init__.py b/sc2reader/factories/__init__.py index 583b67ea..c6c469f6 100644 --- a/sc2reader/factories/__init__.py +++ b/sc2reader/factories/__init__.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import absolute_import, print_function, unicode_literals, division from sc2reader.factories.sc2factory import SC2Factory from sc2reader.factories.sc2factory import FileCachedSC2Factory From 81809e5888c10aeb80b0e0ba9ae9958b28367843 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:29:04 -0500 Subject: [PATCH 22/53] Don't forget the tests. --- test_replays/test_all.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index ba7c0ea8..c91f8dbf 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -245,7 +245,7 @@ def test_send_resources(self): def test_cn_replays(self): replay = sc2reader.load_replay("test_replays/2.0.5.25092/cn1.SC2Replay") - self.assertEqual(replay.gateway, "cn") + self.assertEqual(replay.region, "cn") self.assertEqual(replay.expansion, "WoL") def test_unit_types(self): @@ -320,7 +320,7 @@ def test_factory_plugins(self): self.assertEqual(result["release"], "2.0.5.25092") self.assertEqual(result["game_length"], 986) self.assertEqual(result["real_length"], 704) - self.assertEqual(result["gateway"], "cn") + self.assertEqual(result["region"], "cn") self.assertEqual(result["game_fps"], 16.0) self.assertTrue(result["is_ladder"]) From 03bbdae1d5979c661d4eae8568ce405694ca86ca Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Tue, 10 Dec 2013 23:32:36 -0500 Subject: [PATCH 23/53] Our readers and lambdas can't be pickled. Remove registered readers/datapacks on picking. We won't need them anymore, the replay should already be loaded. --- CHANGELOG.rst | 1 + sc2reader/resources.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 37482c05..fa3cef51 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ CHANGELOG 0.7.0 - --------------------------- +* Replays can now be pickled and stored for later consumption. * All references to the gateway attribute have been replaced in favor of region; e.g. replay.region * Use generic UnitType and Ability classes for data. This means no more unit._type_class.__class__.__name__. But hopefully people were not doing that anyway. * Now a CorruptTrackerFileError is raised when the tracker file is corrupted (generally only older resume_from_replay replays) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 58e861a1..0fc5d5dd 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -603,6 +603,12 @@ def _read_data(self, data_file, reader): 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 __getstate__(self): + state = self.__dict__.copy() + del state['registered_readers'] + del state['registered_datapacks'] + return state + class Map(Resource): url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma' From b421a7cd3e70ee05feeaa6c8f138b16c4eacfd00 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Wed, 11 Dec 2013 00:23:36 -0500 Subject: [PATCH 24/53] Initial pass at PTR 2.1 support. --- sc2reader/events/tracker.py | 121 +++++++++++++++++++++++++----------- sc2reader/readers.py | 43 +++++++++++++ sc2reader/resources.py | 4 +- 3 files changed, 131 insertions(+), 37 deletions(-) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index b8967150..8eaaf7a1 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -27,12 +27,30 @@ def load_context(self, replay): pass def _str_prefix(self): - return "{0}\t ".format(Length(seconds=int(self.frame/16))) + return "{0}\t ".format(Length(seconds=int(self.frame / 16))) def __str__(self): return self._str_prefix() + self.name +class PlayerSetupEvent(TrackerEvent): + """ Sent during game setup to help us organize players better """ + def __init__(self, frames, data, build): + super(PlayerSetupEvent, self).__init__(frames) + + #: The player id of the player we are setting up + self.pid = data[0] + + #: The type of this player. One of 1=human, 2=cpu, 3=neutral, 4=hostile + self.type = data[1] + + #: The user id of the player we are setting up. None of not human + self.uid = data[2] + + #: The slot id of the player we are setting up. None if not playing + self.sid = data[3] + + class PlayerStatsEvent(TrackerEvent): """ Player Stats events are generated for all players that were in the game even if they've since @@ -181,10 +199,10 @@ def __init__(self, frames, data, build): self.resources_killed = self.minerals_killed + self.vespene_killed #: The food supply currently used - self.food_used = clamp(self.stats[29])/4096.0 + self.food_used = clamp(self.stats[29]) / 4096.0 #: The food supply currently available - self.food_made = clamp(self.stats[30])/4096.0 + self.food_made = clamp(self.stats[30]) / 4096.0 #: The total mineral value of all active forces self.minerals_used_active_forces = clamp(self.stats[31]) @@ -211,7 +229,7 @@ def __init__(self, frames, data, build): self.ff_vespene_lost_technology = clamp(self.stats[38]) if build >= 26490 else None def __str__(self): - return self._str_prefix()+"{0: >15} - Stats Update".format(self.player) + return self._str_prefix() + "{0: >15} - Stats Update".format(self.player) class UnitBornEvent(TrackerEvent): @@ -255,19 +273,24 @@ def __init__(self, frames, data, build): #: The player object that controls this unit. 0 means neutral unit self.unit_controller = None - #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.x = data[5] * 4 + #: The x coordinate of the center of the born unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.x = data[5] - #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.y = data[6] * 4 + #: The y coordinate of the center of the born unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.y = data[6] #: The map location of the unit birth self.location = (self.x, self.y) + if build < 27950: + self.x = self.x * 4 + self.y = self.y * 4 + self.location = (self.x, self.y) + def __str__(self): - return self._str_prefix()+"{0: >15} - Unit born {1}".format(self.unit_upkeeper, self.unit) + return self._str_prefix() + "{0: >15} - Unit born {1}".format(self.unit_upkeeper, self.unit) class UnitDiedEvent(TrackerEvent): @@ -296,19 +319,39 @@ def __init__(self, frames, data, build): #: The player object of the that killed the unit. Not always available. self.killer = None - #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.x = data[3] * 4 + #: The x coordinate of the center of the dying unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.x = data[3] - #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.y = data[4] * 4 + #: The y coordinate of the center of the dying unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.y = data[4] #: The map location the unit was killed at. self.location = (self.x, self.y) + #: The index portion of the killing unit's id. Available for build 27950+ + self.killer_unit_index = None + + #: The recycle portion of the killing unit's id. Available for build 27950+ + self.killer_unit_recycle = None + + #: The unique id of the unit doing the killing. Available for build 27950+ + self.killer_unit_id = None + + if build < 27950: + self.x = self.x * 4 + self.y = self.y * 4 + self.location = (self.x, self.y) + else: + # Starcraft patch 2.1 introduced killer unit indexes + self.killer_unit_index = data[5] + self.killer_unit_recycle = data[6] + if self.killer_unit_index: + self.killer_unit_id = self.killer_unit_index << 18 | self.killer_unit_recycle + def __str__(self): - return self._str_prefix()+"{0: >15} - Unit died {1}.".format(self.unit.owner, self.unit) + return self._str_prefix() + "{0: >15} - Unit died {1}.".format(self.unit.owner, self.unit) class UnitOwnerChangeEvent(TrackerEvent): @@ -344,7 +387,7 @@ def __init__(self, frames, data, build): self.unit_controller = None def __str__(self): - return self._str_prefix()+"{0: >15} took {1}".format(self.unit_upkeeper, self.unit) + return self._str_prefix() + "{0: >15} took {1}".format(self.unit_upkeeper, self.unit) class UnitTypeChangeEvent(TrackerEvent): @@ -372,7 +415,7 @@ def __init__(self, frames, data, build): self.unit_type_name = data[2].decode('utf8') def __str__(self): - return self._str_prefix()+"{0: >15} - Unit {0} type changed to {1}".format(self.unit.owner, self.unit, self.unit_type_name) + return self._str_prefix() + "{0: >15} - Unit {0} type changed to {1}".format(self.unit.owner, self.unit, self.unit_type_name) class UpgradeCompleteEvent(TrackerEvent): @@ -395,7 +438,7 @@ def __init__(self, frames, data, build): self.count = data[2] def __str__(self): - return self._str_prefix()+"{0: >15} - {1}upgrade completed".format(self.player, self.upgrade_type_name) + return self._str_prefix() + "{0: >15} - {1}upgrade completed".format(self.player, self.upgrade_type_name) class UnitInitEvent(TrackerEvent): @@ -434,19 +477,24 @@ def __init__(self, frames, data, build): #: The player object that controls this unit. 0 means neutral unit self.unit_controller = None - #: The x coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.x = data[5] * 4 + #: The x coordinate of the center of the init unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.x = data[5] - #: The y coordinate of the location with 4 point resolution. E.g. 13.75 recorded as 12. - #: Location prior to rounding marks the center of the unit footprint. - self.y = data[6] * 4 + #: The y coordinate of the center of the init unit's footprint. Only 4 point resolution + #: prior to Starcraft Patch 2.1. + self.y = data[6] #: The map location the unit was started at self.location = (self.x, self.y) + if build < 27950: + self.x = self.x * 4 + self.y = self.y * 4 + self.location = (self.x, self.y) + def __str__(self): - return self._str_prefix()+"{0: >15} - Unit initiated {1}".format(self.unit_upkeeper, self.unit) + return self._str_prefix() + "{0: >15} - Unit initiated {1}".format(self.unit_upkeeper, self.unit) class UnitDoneEvent(TrackerEvent): @@ -470,7 +518,7 @@ def __init__(self, frames, data, build): self.unit = None def __str__(self): - return self._str_prefix()+"{0: >15} - Unit {1} done".format(self.unit.owner, self.unit) + return self._str_prefix() + "{0: >15} - Unit {1} done".format(self.unit.owner, self.unit) class UnitPositionsEvent(TrackerEvent): @@ -491,17 +539,20 @@ def __init__(self, frames, data, build): #: A dict mapping of units that had their position updated to their positions self.units = dict() - #: A list of (unit_index, (x,y)) derived from the first_unit_index and items. Like the other - #: tracker events, these coordinates have 4 point resolution. (15,25) recorded as (12,24). - #: Location prior to rounding marks the center of the unit footprint. + #: A list of (unit_index, (x,y)) derived from the first_unit_index and items. Prior to + #: Starcraft Patch 2.1 the coordinates have 4 point resolution. (15,25) recorded as (12,24). + #: Location prior to any rounding marks the center of the unit footprint. self.positions = list() unit_index = self.first_unit_index for i in range(0, len(self.items), 3): unit_index += self.items[i] - x = self.items[i+1]*4 - y = self.items[i+2]*4 + x = self.items[i + 1] + y = self.items[i + 2] + if build < 27950: + x = x * 4 + y = y * 4 self.positions.append((unit_index, (x, y))) def __str__(self): - return self._str_prefix()+"Unit positions update" + return self._str_prefix() + "Unit positions update" diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 4ac04c3a..94da5a9f 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -19,6 +19,7 @@ def __call__(self, data, replay): user_initial_data=[dict( name=data.read_aligned_string(data.read_uint8()), clan_tag=data.read_aligned_string(data.read_uint8()) if replay.base_build >= 24764 and data.read_bool() else None, + clan_logo=data.read_aligned_string(40) if replay.base_build >= 27950 and data.read_bool() else None, highest_league=data.read_uint8() if replay.base_build >= 24764 and data.read_bool() else None, combined_race_levels=data.read_uint32() if replay.base_build >= 24764 and data.read_bool() else None, random_seed=data.read_uint32(), @@ -74,6 +75,7 @@ def __call__(self, data, replay): default_difficulty=data.read_bits(6), default_ai_build=data.read_bits(7) if replay.base_build >= 23925 else None, cache_handles=[DepotFile(data.read_aligned_bytes(40)) for i in range(data.read_bits(6 if replay.base_build >= 21955 else 4))], + has_extension_mod=data.read_bool() if replay.base_build >= 27950 else None, is_blizzardMap=data.read_bool(), is_premade_ffa=data.read_bool(), is_coop_mode=data.read_bool() if replay.base_build >= 23925 else None, @@ -610,6 +612,7 @@ def camera_update_event(self, data): distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, + reason=None, ) def trigger_abort_mission_event(self, data): @@ -1361,6 +1364,7 @@ def hijack_replay_game_event(self, data): name=data.read_aligned_string(data.read_uint8()), toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, + clan_logo=None, ) for i in range(data.read_bits(5))], method=data.read_bits(1), ) @@ -1381,6 +1385,7 @@ def game_user_join_event(self, data): name=data.read_aligned_string(data.read_bits(8)), toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, + clan_log=None, ) @@ -1430,6 +1435,43 @@ def trigger_mouse_moved_event(self, data): ) +class GameEventsReader_27950(GameEventsReader_26490): + + def hijack_replay_game_event(self, data): + return dict( + user_infos=[dict( + game_user_id=data.read_bits(4), + observe=data.read_bits(2), + name=data.read_aligned_string(data.read_uint8()), + toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, + clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, + clan_logo=data.read_aligned_string(40) if data.read_bool() else None, + ) for i in range(data.read_bits(5))], + method=data.read_bits(1), + ) + + def camera_update_event(self, data): + return dict( + target=dict( + x=data.read_uint16(), + y=data.read_uint16(), + ), + distance=data.read_uint16() if data.read_bool() else None, + pitch=data.read_uint16() if data.read_bool() else None, + yaw=data.read_uint16() if data.read_bool() else None, + reason=data.read_uint8() - 128 if data.read_bool() else None, + ) + + def game_user_join_event(self, data): + return dict( + observe=data.read_bits(2), + name=data.read_aligned_string(data.read_bits(8)), + toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, + clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, + clan_logo=data.read_aligned_string(40) if data.read_bool() else None, + ) + + class TrackerEventsReader(object): def __init__(self): @@ -1443,6 +1485,7 @@ def __init__(self): 6: UnitInitEvent, 7: UnitDoneEvent, 8: UnitPositionsEvent, + 9: PlayerSetupEvent, } def __call__(self, data, replay): diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 0fc5d5dd..325be41b 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -564,10 +564,10 @@ def register_default_readers(self): 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_26490(), lambda r: 26490 <= r.base_build < 27950) + self.register_reader('replay.game.events', readers.GameEventsReader_27950(), lambda r: 27950 <= 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) From 38dfa77d7512b52bdf3e659fcae88e6a87dc556c Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Wed, 11 Dec 2013 20:54:39 -0500 Subject: [PATCH 25/53] Fix small bugs in PTR support. --- sc2reader/readers.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 94da5a9f..cd438c6b 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1369,6 +1369,18 @@ def hijack_replay_game_event(self, data): method=data.read_bits(1), ) + def camera_update_event(self, data): + return dict( + target=dict( + x=data.read_uint16(), + y=data.read_uint16(), + ) if data.read_bool() else None, + distance=data.read_uint16() if data.read_bool() else None, + pitch=data.read_uint16() if data.read_bool() else None, + yaw=data.read_uint16() if data.read_bool() else None, + reason=None, + ) + def trigger_target_mode_update_event(self, data): return dict( ability_link=data.read_uint16(), @@ -1399,7 +1411,7 @@ def user_options_event(self, data): sync_checksumming_enabled=data.read_bool(), is_map_to_map_transition=data.read_bool(), starting_rally=data.read_bool(), - debug_pause_enabled=None, + debug_pause_enabled=data.read_bool(), base_build_num=data.read_uint32(), use_ai_beacons=None, ) @@ -1455,7 +1467,7 @@ def camera_update_event(self, data): target=dict( x=data.read_uint16(), y=data.read_uint16(), - ), + ) if data.read_bool() else None, distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, From ba404a50251f8a3074e84a2e6a8da383ea3fec67 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Wed, 11 Dec 2013 21:30:24 -0500 Subject: [PATCH 26/53] Clarify the highest leauge user attribute. --- sc2reader/objects.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index e7e096b3..684c8180 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -228,7 +228,8 @@ def __init__(self, uid, init_data): #: The user's combined Battle.net race levels self.combined_race_levels = init_data['combined_race_levels'] - #: The user's highest leauge in the current season + #: The highest 1v1 league achieved by the user in the current season with 1 as Bronze and + #: 7 as Grandmaster. 8 seems to indicate that there is no current season 1v1 ranking. self.highest_league = init_data['highest_league'] #: A flag indicating if this person was the one who recorded the game. From 94263a519a036f2bae43e18ce99f131c30dd4014 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Thu, 12 Dec 2013 21:36:34 -0500 Subject: [PATCH 27/53] The clan logo is a DepotFile location, not a string. --- sc2reader/readers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index cd438c6b..20dc8b8f 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -19,7 +19,7 @@ def __call__(self, data, replay): user_initial_data=[dict( name=data.read_aligned_string(data.read_uint8()), clan_tag=data.read_aligned_string(data.read_uint8()) if replay.base_build >= 24764 and data.read_bool() else None, - clan_logo=data.read_aligned_string(40) if replay.base_build >= 27950 and data.read_bool() else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) if replay.base_build >= 27950 and data.read_bool() else None, highest_league=data.read_uint8() if replay.base_build >= 24764 and data.read_bool() else None, combined_race_levels=data.read_uint32() if replay.base_build >= 24764 and data.read_bool() else None, random_seed=data.read_uint32(), @@ -1457,7 +1457,7 @@ def hijack_replay_game_event(self, data): name=data.read_aligned_string(data.read_uint8()), toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=data.read_aligned_string(40) if data.read_bool() else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None, ) for i in range(data.read_bits(5))], method=data.read_bits(1), ) @@ -1480,7 +1480,7 @@ def game_user_join_event(self, data): name=data.read_aligned_string(data.read_bits(8)), toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=data.read_aligned_string(40) if data.read_bool() else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None, ) From f0125666b6e07e931a42e6c7f73019e29fe0379e Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Fri, 13 Dec 2013 00:51:45 -0500 Subject: [PATCH 28/53] Fixes #165, properly parse mouse click events. --- sc2reader/readers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 20dc8b8f..e835123f 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -936,7 +936,7 @@ def trigger_mouse_clicked_event(self, data): position_world=dict( x=data.read_bits(20), y=data.read_bits(20), - z=data.read_uint32()-2147483648, + z=data.read_uint32() - 2147483648, ), ) @@ -1421,15 +1421,15 @@ def trigger_mouse_clicked_event(self, data): button=data.read_uint32(), down=data.read_bool(), position_ui=dict( - x=data.read_uint32(), - y=data.read_uint32(), + x=data.read_bits(11), + y=data.read_bits(11), ), position_world=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, + x=data.read_bits(20) - 2147483648, + y=data.read_bits(20) - 2147483648, + z=data.read_uint32() - 2147483648, ), - flags=data.read_uint8()-128, + flags=data.read_uint8() - 128, ) def trigger_mouse_moved_event(self, data): @@ -1441,9 +1441,9 @@ def trigger_mouse_moved_event(self, data): position_world=dict( x=data.read_bits(20), y=data.read_bits(20), - z=data.read_uint32()-2147483648, + z=data.read_uint32() - 2147483648, ), - flags=data.read_uint8()-128, + flags=data.read_uint8() - 128, ) From 4b45b622638b4a9e4c1b594d4113eeea3a39b953 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Fri, 13 Dec 2013 02:29:38 -0500 Subject: [PATCH 29/53] Fix initdata parsing for base_build 23260 replays. --- sc2reader/readers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index e835123f..e3434fd7 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -105,7 +105,7 @@ def __call__(self, data, replay): is_single_player=data.read_bool(), game_duration=data.read_uint32(), default_difficulty=data.read_bits(6), - default_ai_build=data.read_bits(7) if replay.base_build >= 23260 else None, + default_ai_build=data.read_bits(7) if replay.base_build >= 24764 else None, ), ) if not data.done(): From 53e56784508932c7aa967bf400cdb366ed9ccce4 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Thu, 19 Dec 2013 00:02:44 -0500 Subject: [PATCH 30/53] Add killer logic to ContextLoader, refine the API. --- sc2reader/data/__init__.py | 16 +++++++++++++++- sc2reader/engine/plugins/context.py | 20 ++++++++++++++------ sc2reader/events/tracker.py | 27 ++++++++++++++++++--------- 3 files changed, 47 insertions(+), 16 deletions(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index f4e095f4..0fe1b712 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -53,10 +53,24 @@ def __init__(self, unit_id): #: Specifically, it is the frame that the :class:`~sc2reader.events.tracker.UnitDiedEvent` is received. self.died_at = None + #: Deprecated, see :attr:`self.killing_player` + self.killed_by = None + #: A reference to the player that killed this unit. Only available for 2.0.8+ replays. #: This value is not set if the killer is unknown or not relevant (morphed into a #: different unit or used to create a building, etc) - self.killed_by = None + self.killing_player = None + + #: A reference to the unit that killed this unit. Only available for 2.1+ replays. + #: This value is not set if the killer is unknown or not relevant (morphed into a + #: different unit or used to create a building, etc). If the killing unit dies before + #: the killed unit dies, a bug may cause the killing unit to be None. This can occur + #: due because of projectile speeds. + self.killing_unit = None + + #: A list of units that this unit has killed. Only available for 2.1+ replays. + #: The unit only gets credit for the kills that it gets the final blow on. + self.killed_units = list() #: The unique in-game id for this unit. The id can sometimes be zero because #: TargetUnitCommandEvents will create a new unit with id zero when a unit diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index b6b67af9..d9d1241f 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -146,13 +146,21 @@ def handleUnitDiedEvent(self, event, replay): else: self.logger.error("Unit {0} died at {1} [{2}] before it was born!".format(event.unit_id, Length(seconds=event.second), event.frame)) - if event.killer_pid in replay.player: - event.killer = replay.player[event.killer_pid] + if event.killing_player_id in replay.player: + event.killing_player = event.killer = replay.player[event.killing_player_id] if event.unit: - event.unit.killed_by = event.killer - event.killer.killed_units.append(event.unit) - elif event.killer_pid: - self.logger.error("Unknown killer pid {0} at {1} [{2}]".format(event.killer_pid, Length(seconds=event.second), event.frame)) + event.unit.killing_player = event.unit.killed_by = event.killing_player + event.killing_player.killed_units.append(event.unit) + elif event.killing_player_id: + self.logger.error("Unknown killing player id {0} at {1} [{2}]".format(event.killing_player_id, Length(seconds=event.second), event.frame)) + + if event.killing_unit_id in replay.objects: + event.killing_unit = replay.objects[event.killing_unit_id] + if event.unit: + event.unit.killing_unit = event.killing_unit + event.killing_unit.killed_units.append(event.unit) + elif event.killing_unit_id: + self.logger.error("Unknown killing unit id {0} at {1} [{2}]".format(event.killing_unit_id, Length(seconds=event.second), event.frame)) def handleUnitOwnerChangeEvent(self, event, replay): self.load_tracker_controller(event, replay) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 8eaaf7a1..62e25099 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -313,12 +313,18 @@ def __init__(self, frames, data, build): #: The unit object that died self.unit = None - #: The id of the player that killed this unit. None when not available. + #: Deprecated, see :attr:`killing_player_id` self.killer_pid = data[2] - #: The player object of the that killed the unit. Not always available. + #: Deprecated, see :attr:`killing_player` self.killer = None + #: The id of the player that killed this unit. None when not available. + self.killing_player_id = data[2] + + #: The player object of the that killed the unit. Not always available. + self.killing_player = None + #: The x coordinate of the center of the dying unit's footprint. Only 4 point resolution #: prior to Starcraft Patch 2.1. self.x = data[3] @@ -331,13 +337,16 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) #: The index portion of the killing unit's id. Available for build 27950+ - self.killer_unit_index = None + self.killing_unit_index = None #: The recycle portion of the killing unit's id. Available for build 27950+ - self.killer_unit_recycle = None + self.killing_unit_recycle = None #: The unique id of the unit doing the killing. Available for build 27950+ - self.killer_unit_id = None + self.killing_unit_id = None + + #: A reference to the :class:`Unit` that killed this :class:`Unit` + self.killing_unit = None if build < 27950: self.x = self.x * 4 @@ -345,10 +354,10 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) else: # Starcraft patch 2.1 introduced killer unit indexes - self.killer_unit_index = data[5] - self.killer_unit_recycle = data[6] - if self.killer_unit_index: - self.killer_unit_id = self.killer_unit_index << 18 | self.killer_unit_recycle + self.killing_unit_index = data[5] + self.killing_unit_recycle = data[6] + if self.killing_unit_index: + self.killing_unit_id = self.killing_unit_index << 18 | self.killing_unit_recycle def __str__(self): return self._str_prefix() + "{0: >15} - Unit died {1}.".format(self.unit.owner, self.unit) From e5001a3a56aeb3097cdfed85ebf0a74f29b0db81 Mon Sep 17 00:00:00 2001 From: David Joerg Date: Sat, 4 Jan 2014 22:51:33 -0400 Subject: [PATCH 31/53] added a replay thats failing for GGTracker, but surprisingly it turns out not failing for sc2reader. mpyq issue --- .../2.0.11.26825/DaedalusPoint.SC2Replay | Bin 0 -> 65070 bytes test_replays/test_all.py | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 test_replays/2.0.11.26825/DaedalusPoint.SC2Replay diff --git a/test_replays/2.0.11.26825/DaedalusPoint.SC2Replay b/test_replays/2.0.11.26825/DaedalusPoint.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..140a80a9f2112fd8dbd387c79d5f5f2b524a27ff GIT binary patch literal 65070 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 zflII Date: Wed, 8 Jan 2014 13:28:18 -0500 Subject: [PATCH 32/53] Bump mpyq version. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0f2fc7a7..c9dee97d 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ ] }, - install_requires=['mpyq>=0.2.3', 'argparse', 'ordereddict', 'unittest2'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.3'], + install_requires=['mpyq>=0.2.4', 'argparse', 'ordereddict', 'unittest2'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.4'], packages=setuptools.find_packages(), include_package_data=True, zip_safe=True From 18ca54c09f215efb13ca236f2eb00d9dbaeadfa6 Mon Sep 17 00:00:00 2001 From: Eddie Yan Date: Wed, 29 Jan 2014 22:50:14 -0800 Subject: [PATCH 33/53] fix name of 'ControlGroupEvent' in comments --- sc2reader/events/game.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index e3948f26..03d69bba 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -354,7 +354,7 @@ class SelectionEvent(GameEvent): Starting in Starcraft 2.0.0, selection events targetting control group buffers are also generated when control group selections are modified by non-player actions. When a player action updates a control group - a :class:`HotkeyEvent` is generated. + a :class:`ControlGroupEvent` is generated. """ def __init__(self, frame, pid, data): super(SelectionEvent, self).__init__(frame, pid) From 9277ec45eca06085ccda35c7b6e9c84c0b002845 Mon Sep 17 00:00:00 2001 From: Emmanuel Hadoux Date: Sun, 13 Apr 2014 15:20:32 +0200 Subject: [PATCH 34/53] No more lints. --- sc2reader/data/create_lookup.py | 26 ++++++++++++-------------- sc2reader/engine/plugins/context.py | 2 +- sc2reader/resources.py | 9 ++++----- sc2reader/scripts/sc2printer.py | 18 +++++++++--------- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/sc2reader/data/create_lookup.py b/sc2reader/data/create_lookup.py index 459685f2..73623836 100755 --- a/sc2reader/data/create_lookup.py +++ b/sc2reader/data/create_lookup.py @@ -1,16 +1,14 @@ - - abilities = dict() with open('hots_abilities.csv', 'r') as f: - for line in f: - num, ability = line.strip('\r\n ').split(',') - abilities[ability] = [""]*32 - -with open('command_lookup.csv','r') as f: - for line in f: - ability, commands = line.strip('\r\n ').split('|',1) - abilities[ability] = commands.split('|') - -with open('new_lookup.csv','w') as out: - for ability, commands in sorted(abilities.items()): - out.write(','.join([ability]+commands)+'\n') + for line in f: + num, ability = line.strip('\r\n ').split(',') + abilities[ability] = [""]*32 + +with open('command_lookup.csv', 'r') as f: + for line in f: + ability, commands = line.strip('\r\n ').split('|', 1) + abilities[ability] = commands.split('|') + +with open('new_lookup.csv', 'w') as out: + for ability, commands in sorted(abilities.items()): + out.write(','.join([ability]+commands)+'\n') diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index d9d1241f..e978ea1f 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -8,7 +8,7 @@ @loggable class ContextLoader(object): - name='ContextLoader' + name = 'ContextLoader' def handleInitGame(self, event, replay): replay.units = set() diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 325be41b..0a31b939 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals, division from collections import defaultdict, namedtuple @@ -192,7 +192,6 @@ class Replay(Resource): #: Lists info for each user that is resuming from replay. resume_user_info = None - def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, do_tracker_events=True, **options): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None @@ -201,7 +200,7 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en # The current load level of the replay self.load_level = None - #default values, filled in during file read + # default values, filled in during file read self.speed = "" self.type = "" self.game_type = "" @@ -333,7 +332,7 @@ def load_details(self): self.map_hash = details['cache_handles'][-1].hash self.map_file = details['cache_handles'][-1] - #Expand this special case mapping + # Expand this special case mapping if self.region == 'sg': self.region = 'sea' @@ -367,7 +366,7 @@ 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 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: diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 52b39c1e..5a1bf94e 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -84,31 +84,31 @@ def main(): description="""Prints basic information from Starcraft II replay and game summary files or directories.""") parser.add_argument('--recursive', action="store_true", default=True, - help="Recursively read through directories of Starcraft II files [default on]") + help="Recursively read through directories of Starcraft II files [default on]") required = parser.add_argument_group('Required Arguments') required.add_argument('paths', metavar='filename', type=str, nargs='+', - help="Paths to one or more Starcraft II files or directories") + help="Paths to one or more Starcraft II files or directories") shared_args = parser.add_argument_group('Shared Arguments') shared_args.add_argument('--date', action="store_true", default=True, - help="print(game date [default on]") + help="print(game date [default on]") shared_args.add_argument('--length', action="store_true", default=False, - help="print(game duration mm:ss in game time (not real time) [default off]") + help="print(game duration mm:ss in game time (not real time) [default off]") shared_args.add_argument('--map', action="store_true", default=True, - help="print(map name [default on]") + help="print(map name [default on]") shared_args.add_argument('--teams', action="store_true", default=True, - help="print(teams, their players, and the race matchup [default on]") + help="print(teams, their players, and the race matchup [default on]") replay_args = parser.add_argument_group('Replay Options') replay_args.add_argument('--messages', action="store_true", default=False, - help="print(in-game player chat messages [default off]") + help="print(in-game player chat messages [default off]") replay_args.add_argument('--version', action="store_true", default=True, - help="print(the release string as seen in game [default on]") + help="print(the release string as seen in game [default on]") s2gs_args = parser.add_argument_group('Game Summary Options') s2gs_args.add_argument('--builds', action="store_true", default=False, - help="print(player build orders (first 64 items) [default off]") + help="print(player build orders (first 64 items) [default off]") arguments = parser.parse_args() for path in arguments.paths: From 214cd7c0f88d794453a4592bbd0e74d00cd3ef83 Mon Sep 17 00:00:00 2001 From: Emmanuel Hadoux Date: Wed, 23 Apr 2014 23:34:00 +0200 Subject: [PATCH 35/53] Fixes SelectionTracker plugin with new handler names. --- sc2reader/engine/plugins/selection.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 79e9d995..69aa12a7 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -40,17 +40,17 @@ def handleSelectionEvent(self, event, replay): if error: event.player.selection_errors += 1 - def handleGetFromHotkeyEvent(self, event, replay): + def handleGetControlGroupEvent(self, event, replay): selection = event.player.selection[event.control_group] new_selection, error = self._deselect(selection, event.mask_type, event.mask_data) event.player.selection[10] = new_selection if error: event.player.selection_errors += 1 - def handleSetToHotkeyEvent(self, event, replay): + def handleSetControlGroupEvent(self, event, replay): event.player.selection[event.control_group] = event.player.selection[10] - def handleAddToHotkeyEvent(self, event, replay): + def handleAddToControlGroupEvent(self, event, replay): selection = event.player.selection[event.control_group] new_selection, error = self._deselect(selection, event.mask_type, event.mask_data) new_selection = self._select(new_selection, event.player.selection[10]) From b35d88c774ee9c90c592a98d6342979f5ed4a5c3 Mon Sep 17 00:00:00 2001 From: David Joerg Date: Mon, 30 Jun 2014 07:57:03 -0400 Subject: [PATCH 36/53] new failing test for replays that started by loading a saved game --- .../Habitation Station LE (54).SC2Replay | Bin 0 -> 11543 bytes test_replays/test_all.py | 4 ++++ 2 files changed, 4 insertions(+) create mode 100755 test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay diff --git a/test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay b/test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay new file mode 100755 index 0000000000000000000000000000000000000000..1b46fd1411cb9324b2980ceb0d9ee37d907f968f GIT binary patch literal 11543 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 literal 0 HcmV?d00001 diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 4d11301c..81c5eadd 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -366,6 +366,10 @@ def test_replay_event_order(self): def test_daedalus_point(self): replay = sc2reader.load_replay("test_replays/2.0.11.26825/DaedalusPoint.SC2Replay") + def test_reloaded(self): + replay = sc2reader.load_replay("test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay") + + class TestGameEngine(unittest.TestCase): class TestEvent(object): From c860d8e23d6fbd3a9258deff9fcbe8796da6c900 Mon Sep 17 00:00:00 2001 From: David Joerg Date: Mon, 30 Jun 2014 08:00:33 -0400 Subject: [PATCH 37/53] fix for replays that started with a load from a saved game --- sc2reader/events/tracker.py | 5 +++-- sc2reader/resources.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 62e25099..5466d4d5 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -15,10 +15,11 @@ class TrackerEvent(Event): """ def __init__(self, frames): #: The frame of the game this event was applied - self.frame = frames + #: Ignore all but the lowest 32 bits of the frame + self.frame = frames % 2**32 #: The second of the game (game time not real time) this event was applied - self.second = frames >> 4 + self.second = self.frame >> 4 #: Short cut string for event class name self.name = self.__class__.__name__ diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 0a31b939..44a26c7a 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -496,7 +496,7 @@ def load_game_events(self): 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: + if self.base_build == 23925 and 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)) From 89f4eeed7d0208525a0895e23726749dfb304405 Mon Sep 17 00:00:00 2001 From: David Joerg Date: Fri, 26 Sep 2014 12:07:17 -0400 Subject: [PATCH 38/53] amended author info --- test_replays/2.1.4/Catallena LE.SC2Replay | Bin 0 -> 38284 bytes test_replays/test_all.py | 3 +++ 2 files changed, 3 insertions(+) create mode 100755 test_replays/2.1.4/Catallena LE.SC2Replay diff --git a/test_replays/2.1.4/Catallena LE.SC2Replay b/test_replays/2.1.4/Catallena LE.SC2Replay new file mode 100755 index 0000000000000000000000000000000000000000..2bc76c617acdf42b9fed7eb420dbe812fdeb6910 GIT binary patch literal 38284 zcmeEtb8seKyX_m>w(aB%C$?>8V%xTD+nSgY+qOCJ#Gc&wopb7br%v6f`}eK;t?J(D zXRqD;RPFBF{q&MkRKf#*0ssJT06^ex1_28IrB`t=auRVeGIJr8k|K68b+9q=#A9Ir zg@6aZfP%t+gTsJA!azgAw7!7B{*!>ifJ4IEg@Qms{w4S~{_TN(d*J^W56CGh;m-X% z+n7WE01N;D=#B#bx)T5Z;=gSO|KlqDvH1tpvHzK?{a5}U_m96#|M2{Gi2qjw1mwR8 zf&c(r&0aR^&ZN+fd2ny@qdb+gS8&7HdqrK}5xpqu4-e}9-v6JniNfihOzMM;aZIKN zyEK&`jz%`{eeg_nJ#a;OP=G^FcYs;WOt-=}R9sEjq2!q8|JncF^?!Td-yZn?#{&?g z0B{fhI0QNXlu}1kK|+t$*@1~)LPFKa$k^J{2^0eQpEV2=0HDJ9cNqf>6LBO54vMJg zWbb0{>-VW=Ttc%fmyVPOF9Fk>KyFaTs2P*iXb7{EW=s4C_zilz<@ z{|Cmr@GsE+10?_)CIkfXFZ+LRg8pw#@P9Z#K|!%aj9iRtY)tKph-JmVK>?5upa4)z zIZHcBTO$WX7jq+MNDx?<#+cAQa~l`4VDK>QF)uL%3xA7YAUuz7+{@KbL#3GaQ2|S?SYnWIFVh(8V!vYMXH|MkMIAo6vmG#xHrl+WgshZ z*8B4dwc13OnoQrz({E??SF3L^;=e4-V7OROLsV|Qx;%UW+7_?-YXg5C>jsZA@091r zD{6!P%hE|HVN}8*z-w9q=L|9_F@|L0jsYKefj7+59g{!|!cK=%KS@#Kr_j ztYuOojna?68E8kdIiI$n&Y$D7|FYymbDoneaQ|BBRXv(1Q~t;`n$|sha(FqRUXuKB zP}9JK1YpBJF!5E^c?T4!WoZ3}J|AuUQ3t@9O2 z8UP5n|JT>oj{mi`|HZD_h_kN@O6b$)}1?Agg7TEO?)+ESfAOF>s{(9J{=Q7yty!;Q#<22|%C; z@IRlbAO&-C2)K|jSvUX-Tm;;|LnftISXs$COq+!RvOMDpl(UKfKm6Z@0lewUUe(<{$s`Ch3R9v`5S7=;<0eKu5YD5vRVm}HK zB$KjCrsyC{%#K7V2lR;4t58!6`UqJbRJ#ibFh@2o5BQLMz*en?gt?B}rN?>8lnIzp z&^10bi_j4SA;=U6Ed+pi=)77!RcjE6m z{&n9npHQo-IVf_ffml&!Pgvg*tPB1WV@BrV z{e8C^RDFrzDfe5$h_bTs$9|^xsXo4$YF)b%hOfUSy`Uy|72CzYP}Qc!?!l4NaDNps z_Ej@)OzBllen!3Ibaf@EJi1qWvA-Y>FChRnUvPDX_sq6ZtWJcbjl6toitIKSCy2(8 z#3hm9BLS7UCm*#rBG5jlw-4tm;D}FyBEZBpXye72zSy(pR>TvAqQMt6{MOt`nfz1S z`u(A;QW|!B&Dj*NX-#sw3;uSQhoUCPDjBP+0RwVSzo%WdePKC?aVaMJGp}v!+_|`J`4IPmfHm3F;Ozfx z+E=APp#qS4|4#G&3-+i9pC=7rhm{&}XpYE#>M+I2B; zZ?Smc4BBN?NL*C~fxILP!kiIHRRF;&BAYz4kYAQ%Wm|-6vS5ot{uF?KCbc&=pI88& z5hD}?iH1@p^=oLpSSc1aThz-WPY$Rc$4WUy6XG%`i*kZZWllC*I9pumq%4*NN%dhGZBdL* zc9^j2L9WQkWJ9h3DEfCmSvfQS+B|SrLH2Kn{E>wZsG#(gm6wsFK$*M^&(rc4C&`XTjG;QdE_W~6i{NdRZuF6Sz;+N881^%L;xzGcZ(w(w@6G{^zL7682E z6~GOVrANYI5jmX70+0d#WR_O|U|KX*SaxYBTiILMUUb2es`A`c!IDquEXm@Qsu1X@ zK(nu)Vwp^nc+5wcWnqEEHASl^~!baFlll#n^JyM39GjyA^)wMRsaTKTF9qe zM)>}^9Ii|MYGFdkYQ7O^m64f+VZ_8=fl+@sb*n+KIXd8d(yJmGROMPnYqT z`_u{8ndvGEbzzb;AV5jG0AC9R%Ik_nE-LAO)Zd;o+z{9p(Pm+`#w8SE7`)u$z(58t zk`$7FAIEXqu#xY{o75^3EWkjKu9%{RNaszdZ7)3+F6jgZ?%X$8Z_=@Ih=8!Gspj$E zfn)RHw|CegisV%c_mQHi8x!-#73=)i z*g0nGt(T$0pM2B)Y?YaoPU0;Z1i|+`p*@hM=ZbT77-eNJK|O>*&`)X6?@u#w{nT$V zw%^dRq#Vb@(*xzV8^=E(Go$HzbtENeMgYqR3yTqgVNb2768*@w)WO)YWKQRz8r>ez zvoa%AUU#v(&jt`E>hFv}{$iO6p-*8pJKxGCAFk4QTgb?P*d&Jyxe_~uGPSv|A)k`u zrv$(5JxHM*cpz^aQwpFrX(t>PMDhzfEFmok(G(XRU-M}=K9;X;PlmHQo&r(fJPP-O z!h#dR7>F7Y>cDmsam{WiH6bgITu__K0YO@Nl^N73%x50)Mh3Hx1lmRp%+hiw zhkDg=r2=jxVxXecuAN8E%?e96tt#q?V8a0P)+Z0O2xZnp84R?&=ByyZ-+?(mbG~yJ zL31CPMcA3bJT%lL2MP`;a&i5@h7=AyR1D_FR3^*0T21+KtkjH|rBlgRJR%7Ecl@X_ z%vNb=fEhMG3RDsV8q}y1;S3dDK(9$b0{Dk8-ZV1#yG0&4>&x{Db6vBeuZB@xKmpx} zFz6lz?2FABC)XyOCjI%O%p_M6)d6-N`%PGJ&U4G<4%#7|l-Lq6Qy;PC+1T{nqrP

EX<1HN(TW)3ro+?Kfg#D(IIj7SLA7R**K=l5rJ>@ArM!~ZGH~-O_dBKR++T`!s*O@oRRGzEfVBqQOO#UiuJG04t zRqP)d*JnZ4KfawVHEkC+r76Kb_Y(mFsf&;$i0B**2&j|qfaLg`B5&TLw}15jg=924 zQe?ZuM|3x=%iAMW^e*>z{hTfvSo`AwQ`@jJx;lA8l{x=*_g%e4yFA4Hg6f~f?-P}) zMg_4qiRNEeyg83lhE;p&<0}HODh!G!dE>jpR|u4)$aLV>G<*cBC`(-89!s33Jr=|* zjeB5P3b~(iwdG?A`}$D2h@@!O)P;r0@19MMp0$0CNr>@?W=Y(2KCWvr#9;W6B*~!9 zv7i=XBBY{-NfCWeNlc;-X?crDeZ--*gJ{55$dECeVi4?BQACX~ovgxa!Agse)osEU z{87PGFq$eshcPI-Kq!~boEUJvnA9B&mG=l!aO?hQR);}<#U30VvZ8U`dUdAEw6(eJ ziP5=5KJK7!pwFd0)!`;TJo!0szjR;Yn0E+Mv8tsC)D6aA4!`JnR{#e899`UB7FXf1LKv}Cb1!me;2>Ept0!9P+yVTaT5(1zgigWK6R&1ewE=NhnOdFX7VJl5Fs|mdGDy((bX}03o<*gFQ=)0)~vbgJESQ58u4TL89^iMMSMh^O})mMRWf4IMXBTh+PvENFx4yfqrc+$bRWzXURO% zEl@tEsZ)K&+mGX)aKUH*wp0I^hzVRm4sgD=PRThQz9{#8AcF@w7d8gxfAIBn{7eos zLER{TZ^Ow#M(K3&nAxU~B+M7AxHr}qFes;>jdo0T5%qWuBmJ7`1 zG@Y!4t4@_y%xy*8Nfpe$LzSa{i0nM5k)MkDiFs?|Oyo4^r;f*2@?6g5|FIg)DR)ZU?hf zS+CO=&@K+oSP)AYYt4uYF1xU&SW<)AKiolrhW?xf0+!fw zR`bsZ!j2x51~4BV510EjST}z8BXvrI-(Jk~i{X-q-NQ-!BY&IX=Y_WQ zRi^?6h(!w_8~mQZi5htdG7{qS6Jxj_BT)sgv)ptj$%X0Sr?+U@ymi^zoKKz#@&{yy z)R4A!Co75Rxw5eQG5p~=x5TnS7wSL(Dv?%VYEjX)mjLGvV;8ooI{Br)aKR`1TW zy!Fm`RysD|zD952fht8nJ)q>dS+M6`P|?HlrhH)SPyW(`+3LtPsN`>OY6$*0t*j!B8a@0zF)Wm8^ojq5dV4#vM0A6 zJ?lA!MPCYl;W-?vHcL!&Roob#Vg#Y)w8!oVMedX0awi2%zLVcX zQ-LQ1lc(+q5TbfZcO-6zXDzR8YGSAq&x=#qqfY z&3q5koBm&0Zy8ia6SeK`4I6iNcXxMp3lQAh-EBi~hu{$0AwY0GLKU zUMuGDPgM(PF`eJ~GA(U``u{B^hVbRCcCVtUtKEA z6LE?l3s*q%iErD-{J;`t1~{f@F+6We(y`X?We{0GkzPh3C9%axJ_3U3H~y}4+LwTj zxkLU|lavQ7#M)Pux&45$(-W0J(s(72F4}5{hrbZ4tn6@>6%G9x>)Q>3`&T4dG1Aed zQoK*2?rHmKI*|(@Z*4lbtyV87QshX3>!~bNgJ0GWX~(w2@6g*9+_7|2n_;F?D!V}2 zsV2wi6F27%0sODehI+fb!m41T=@&V{YA(20R-1ar^MA`{t0U|F&2zu;B)YY{7ytx6 z{$W96>oAh4#i9g}i;IIDal&y+U!g}g2?^-6A0BrO4$K-`Ggn2+gxwb=_M58tT948t zxck)`=qWS#fK$S)c-DMy`KvG%jlq<2oREmwJHX^k^q*g!T?PCH|+XhGMGcXqX5RA{@*bN zu@J(^A|d6CT?x;Yrt|Usv}wKqq%G`#97bn z=t0TWy7}-}7IwO}3rt@R8}=eH)65BPx4L|{s^_d!M@+KCP3+@yYAg|fevt#%2Tvb1hc@sm|HQn^116TiK^ z+J#?j6*L4q7Mc8P7&s^sfB&H*=7yvsiHJaLSd&}rwAy0!OKRRyHYK1|=rae?88Idl zFu2J86km#t{#@k#FBBRCT%SoXQ4Iv9)$p$pvhCA4L>tfI=v9dM{l?8e1|0lhXQZAT zohX(1)>fC-pluosWkdh%&3l)KY1SPH+ z%C5;Hj*M>$M^7RXl(2W=!ZjftQh~(SJk|wH zr%xe81xuGV7!8NXu7ePA1$(zpPd;rDYM+tpp%X`U(pF&7{ z@u$u5d8#B%!X=eUN0U^Uf#qt-UP}r2-<*# zI_BU$Ed)_lauZ-9gr+z#-yhSVtofsbdTqi4cI&ZvQzTf^s)6vg(%DN$pQICMCmsi- zl8ct@P8ZQ3Hgamxf0F!*_vKE$-r0#L7~g&&qpIMt(MTdMAR-v4m$&&^0j7>8k!`}^ zw{S&i;e+`VMO|DVX8VCjKWtVVCz__4vux@-agBM%u=;T!jAOz+b8m5D5IkX$kLNKw z-jnSURvUj#yQt#{d!7V4ri^MA|oQ;IeI{pEZw^pN&17M*_~SSE1a4x zwO}ewrIVJg0NaWtw5|t%x+5+wFL8or_TB6}FFIc=PG?n3;ZxL6LXqjUa+APo@adda z%?>UI>900h!gmMv8HOD!_yaN=FyT>;R4v$zR`QfPi>RRP99dhp<*;+!r2bLt%S)N% zEg9yRYj4OXVi%o`Jx|ALt|9aXWYUaD(~3QV^>%O{i#zr^p-K!XV&dpv$oTaZj@X#V zHpG0gfSv7WQla4q&&vvtp6ck>h$uvoz)J9U=&(I|Ycd;06EmAFmzLav4emkF-5xKm zKwHa#jEo_r;j7d6C@-W@bF#Y8X-uNhB4K2s_)yiX}Z(^^?%SwaF%%wXiD z@lS#8GMSmVmeSDJrr29xVL?d+9V8#3mw2tB7FKV_d}h=nNGl4_H5{*&Ew61ba}HAZ zIcr~_GFDQ0?hlXn^ALN@m9Af0ql4QV z`DM-z)KvdENhEd~5UUEU+F^_u;QzjSJ#jB8%zEkSsD~tDmvP*S4vo~4$H*hWwAW`_ z3wLMTU1sW7F>-cXR^Qx}4xinAcwf@CtHdwpi}dQ{Erx}9RVD)Q$L--}67A-Da~}~r zS8wChTMe+Z3Fix`FQ~>ymgJMiBCA;Nq3P?CL!x z?XNp0&KG_*Is{-jabD(u2i;#^5_wwzQVil{_~JjloOF@r{9A}Q=#Tt<({b)>gfg_@ zy^&5K*~kVPer=dm+ACwk&=EKm$|AnU5U+^qnZ7=!8`sM+qMTyksgk&|#7MMi^+;)b zZI?3Xq!$t^2_Rp~EK1KM?{H?m@WJ)Q#f299`At3V93@}J2$u#z)Il#loa}3ugx^Fu z<@niQSAUCs^&$H|>CQ?^kqt1a;0Ql5vce}Jx^mn2iJ4fYQea#2#oHXLZq~6nb7OO0 zD2c8FPokiR78EUc&m*UU5@ z1DKCcMZ4Zo>qwM0$ik>Or@ zeO&8p#6$g@9b%TG-AXXMl0Ix?Vk(MvNGylT8sKoS*xWQzY;bSwoHpLORW>H38{`!l z7!}F}dO|;O;V{cEf8}sAOCmx32U(2%7g_>Fv9W6cWYcd(291iIzrc{;LKGEgvv87i zTFpMnfNQ?@yOjDTh!je!zzVPBYu!El&7a+HXehGLf&%KsxGyQFVW1{8U^$4mTfdE9 z9=NnbcxF)(*U7Q&&kE$^7CQ5a3T*23V2T926;4YKHx%Fb zjlwg}aNSw2T6|$v9Kxbp?bq*CbG!YKDr@uoWrZ(Ni9BreHy(=37rvfXb4Zv;h%Nw@ zKr&8bhDMHTrRB|IGenLZHt{(~m!6rxSDvC)!g+meBfRcgrKWhkt3D=IGE$z(?40Qy zG^I;R-e$yl8UTFeoj5}xgG?{8q7k$n}j$4aydDNe<7Z2GUg1A9L~V?G^X6RE+86hcYZ z5&w_i-AJOn*s_(**K>&=(80w3=ja}3(98$%wlwi&Pgz6@>0f3Gc?ZEkzHTrI*xUBc%yd?PFLWC4wdhh; z@zj3t)UrIJQ>V=mrNLf8&0pceC&-=dhE@&;@a4zh@}YIu6l$vV_;=~Ia?lp`eYPyK z52HevHv2&g!``2|z}k=yvkJy*ZngX6$@L%%dSQul(i#pFYFJcN-2m4+5CW|P;@V#I zn<{AH?1Ywr^WfwUJ)uUv-^yMZHo4EEuvZTF-X`$=nwuvQbG8j{Nf-&ONMAvvPGiQu zKGk9tP|%)w)g640q$ul8i898ed$*u?*MqWj#uQg? zY|LSg+x$Fb2I=kJLj?fP{}XLD7u~NDlmdX2c?5jf^kJl5+$D7(d3ZyuFd;GyaIyLt zI?-(2H`9k-oNXgDtju3Rr!IR`?O_Rbo9c%F)dIAew6v?U$N z#-J(5;3&Z$3UUz^603;d;XEVuB@tw6stsIO=!adzRlPF=7Z&=_-_`Y@w6UB58~n#> zH?&VbC$5?EoB{{eq-nXc$5RerFln_C1a>lxX{H(75?1W4O`h| z9N#{kJy;EIl#Z2IeU-C|f+PKj1cjbhFRX0Rc~H}R96;j~=l|&&N_{zHV$cw27wAN%H_a0>_`CVm1Np};PbraLr_ZwH+Wn-&H%#SX%nJJ9 zdn6M6V0lYxu`t?{{xq6yK&6-}Zzn?HWGeyRsOQ5~0~Yb$E~Vlv8a}96eA$Un1t-kT zs+H)cB+LLsLl5~vC@jq+)lfWJT)W)f{PL*+2E<^CG?;@=358(NzVex?aZ&Up_c8OX zkFNlf{Tt%3rd*jqI6;qG=Z=d9FfT<}cm8}qIwr1ak0$+$0z*kw#3=(VDkBk8i$kQk zEwuVI#z}}kcyJ65Zci@{*Tb9c$3A>39GK9s$xx-e$)cop2*}qb4GQ^pk@44cW|hW< z5`$4mA_QC1OVk8kG`%!dVfcF5^|u`x{vitswXKZ=M{0_9_zRLCUlXmGtfwF90R(EV z)0r7wbW8YNzcOajYuYeAdj*p09Y&&5bq_@ZmNv|Kqzw*IY7a4t_gyO_!^n%fcA6J% z=H^RugAA*Fyq%bnqgN8cordVlJX{r%_U;uOUmrC?ciyz)zaL@+3qkbbG9)G(xFgunawq` z=ndy&S#fqaSYFuUs@>e3@{n`3!9IO!!Ym49(NWT0M7FV9hRXYi`#WhqFrwt7kE!8^ z`&Y)UNl5`ZdrZ5RloI5N6xAytOb;ESD;@WPEDfZpy0+H z5gKt%NQGFqqkvQ)e>DHVqdbEvKKi85u`k1eX=vZfTejIx#t<0m7S<%S6+^X4ng_^Z zuq9*7@koc+jEBvQ*%88h-g|z){ZSchB8h;UGj5HnxGh`hhV_1$$h|9g#H%ZkcjKt9 zp3R1$ma0y3s5!&ph_VrP>J&oP$pUz8U_hXfe1RauT|A?8_Ij;2`bV4xp}BP4_eu!| zU2Tf_08x$*ar9znwRNhGX193b#vPd;Gh@zA#xe*2I3bD{IMo|ip*BH-*>F97!LcNw zJh&2y$MknMm)^D26x1areZhF^jdcK+eMHx004}!)5kS4;)QXQQDJECp<=G-8%eZ{|m0{rt$$5G5Ct>lO#@Q2KV8LIz+20>ybFem~hruvv# z6O;CwzP5k)aSTJlK;<%ARuvCpnk0%Ejlf>~s>H*K=!kTBPT|YVWO=%P08JHVx3295U!5w$eEt7&?T`FXk6Pl=Xxu72ZcqEn)1XouaoZoZEQr@y5F3} zL0*7{M|%>Hl9j52yP{=*6Q0nJw}At7gvpX9DGtvc z9SZpWo-xHIgT6^8?SmJTsKG$CRq%qm4BbKt)y4z-=HxjM5YjTY-@Kzh<$!vYN(xQX zYbPGiGUd7DwyTAw?;VNuk(s%7qe-3KaTr&s<@FAN*?bTinZI#2-1G3Eg}k^|89$!& zhAMwCt8YErOWLPty`{wqW|klN7shQJ1dj(h8$UJ$TU6=`dZS-y=xZd)=ffLE8NuO;#5yLxGa1S1A$mB@xw^VHN4 zb&LF!CkTtl?oX-A2;n3VP^^hUysiRsT`(4G22UFesw1Z$j0$`OL&nTqgdl?2GmyE% zuj=i!hmXpqr3DPDlv={VqrsG?cOxn0(wrX;3(+DM1g7r?X2vTAyM1 z>lg3sm_L7Cw<7~`%Gxk+X!c*t9hWzTd56D6EGBjeLl`{%q*&hWMmFp|0fMi+vmqLf z{qds$O!i06a zs^1)mVGydAo$1l2N7ua3ZicR#U#D9Ak&BCNvAvc&eva>EGx60Ygbhf4utefKg`=uT z_V-tFGH9x$Ga>0a!%(ubF&xYj+ZzZZtXzD^{d<`dSr^n-npfT2AV_Cfp#d9>ZB`CiobS&6KrQh{w3Fq+&E7pyo-Qd%+*GTHN*utx1jdG>a>(lveGxj;pptCtdrx;aZt4b$vY9qtM5qA7~ zPT6w7`Q$0c>Ez!gg$z)yMIh2SCYT?)1Xe^NeCa+^gH9s;yet~T=eJpAKnebw`2|s; ziLQPgi16nL_^q>}eOb5aS!q`x0~0n1fCt1Dg^(f*^BDy2EP6p8#_2hjG2ifRD9b`f)@#Ko`{)lEz!l`BGS+F{a2!Ph(|a;Z!F3vO&={KXt@ z9|Z%Yos4dWWaC;)j!xH}5?2xSi(sy+tN&3U<13RndUh(Al%`nI0?8y_B3X{&xi&7q zpC3iA4oVY>Te95gr z91O>c4*pX%_kz$1skMA|?+wt&?&Zt3;`15Rx%9(%ER2~q6m(Tp!QFNl*^My!{2iaq zy%|uFma?f5VK8#!bO8??3;I)p4x31+P2P)vA<}?UJlkJ%L#+xyhq!njg#-bfc%*3o z0fkqSG8qtIeEtdO7nSxZ`2-r36xi56oH%o2cOw<9|H-OSl8>&|+Hh3k*$_B+^S2~D zik6cshI+=@p@4e&2&85~I6yGD9EUfos60jU%onB;UKE4JH06{RO9TuY7no){YJvC+ zjY#J1N?wAQw-L|ngal2jVhow<woT7$(3J4#N<4Y4b z+=bN|A#6D6{{m%EtLg^S^|p!7cTCct-ce$<&hHJmE$`XFK+!VxjHr|#%{~l@P*c+} zJPnfHG*tcFl1}UsvjS-eAgvw8d-dYOx@Vhk~HaH_##Fc!APX%vP>N`{&xovaXwHDXmzCbgv- zYdJYkrm5lf>t{~D+tWp?QC>>!xmYjh7msM%BSYH>TE`epL`@3zqj|H908rT9-)yh? z&b->IU^wBcu}OO`^>#8$CJrq&`u*20m~oPfTgkx=kz<(Akg%hP$&yj>1QA&dwil2p z37x+jqBYNaoG~oQE}~Nf8myJft40^<2UJpzF1-dOJ-*Xm`bzT)&)xya@PvSm(?Sq% zyapb_%}H#J8RlmWdPaOQ) zbdN?Z%~k<1E^~ha!QU_adW=eiF25Zx;v~jXYh;4Bzd|W6k{L$lYMWV;;FiU$=7pu9PCB*^_lg%05Qcq zbFBusaCLV$c7Yj)uR@oKV*^WIA9@SZjD$WmHOc%lx2xvfE{@e zx5h>{F!~w^=e7H_X{qW3=?B~E461!pffz^G^Tsa!HJIp@A1Y#8Ivm04JA;ayK#Vz+A~yAg`btJZ!fCmr`1 z8P~XCFpHhSHPN1^lxU>CArc1;t(b(t=<#JamE%!>tWhdTH|M!2o6;6Ku!^r!R7j@G zh@jx+Kt4q_Bk+X#9ZXBea$~q%FURy{15Rr^}?@_{o3cyI~># z>gu>8UU%wQLxac9{HrrcxpMoCqVtUO!?Wut+0sVQ#t?-wX9LrZWCW3mRr;mQA$EK! z1AD@CH}6!HyoW=LhWkxvj5vho2O`WDZPLi7i& zs36X@?0vxWw+sw2Gzd}>hKoOLcVOTjPtX1!g6E)Rn!_J~fp^T_D0gtMVzCr(aNvDv z-a-)SnNcLpEC-FTP_sxg2?Y*Jt*5x}!-b@1$T@eP5cj*gA{r`rS&z^42i4sOi846f z7BmYNYNQa;D~04J>~R+L2!K2VpwuIi$dkiJ@#mx5V(pSiUr=(c2UMWvw}PgH$KX(O;hNO=|S_7%3Yj`+Wf5n3< zu*LvABfkxGA`mKy6b>-d^YY!2F#3sn+6~Yn)ABDPAs03bjI}GogYbZ&Pl32MC&Z$} zhK2hyxnL0X+m!gUU!y$B!{ zV;~n@3n`$@-2{`7lnM-KqS&E^5Jmx74YBz6Wx{gSdoMaAMs=!IUa6sZr)DfX)yu3) zE?W5)x)-bMgkj~M`YcSoA5ynV{CKH&;X)F8Y;0w6qo>g2r?l-PCLrpYl*q`-lm+<$Jh{I$9rcpEiNwMhq$wVlpd2*i407#$+7`E(T!lafIql?1% zA>m0>XjAqErcau9S5Weu7I-dP2=iXh@(t}y)fG^m+XNvcnW&MZN(l!g8%;9@>=(z+ z^d|dY2ZR}5%KiD7djiuPu_E?Vl_t=4A7K^IsyHYN0QguP-h_kiiI+jDk8lFz0HnSf zx^qSERY}Esp}LkCBEMMCUL>&(lyTR8f|A#u4PhZT=m^8*`3Wozbp+Lq17FFbe9#?k z1SSG0FyHIXsBj3WJ|Ri9cV!$a7C3Q*`YIyAw!khh2cGB0o-ml3BnZF(_=h>W?!gD! zN`2xHw>UQ_ex+Bt>upN46P{@742x8p^$X#IG!X!s4hTyn1rV>07;D2LDW^mKQv8yi zmRpSC>&>v6BzJ=gl-Dsrn*}Gscu4{TzAx6mpTK(7MUh6466a5;jz0Br7lml!4-Txl z5G6FIi*)_L07CvLxWbsFIr5C`A%(eXEc}^(k|1-mg^5jse8M!Uw7|N{M$HlNUt7Tw`~S z14z^}f)m5$T^SO?Sc!mrgs7MZ`iXG7ie!F4J_#)-Gn}WC`>WKyGUSinGN?`IDZhsBGf< z{RO1r;>GZV?5Y}q!AH<#sBKH!)7J@*cn+b`iJ5Hh(hPzy% zssaL%6Il@%1)9nv5PQaylFn`_8WPW>wg=2k9iE;Jwu8P^=`NygXVKP)_pBd=Twi zK(<476P;^m`pjSXmcElhaW7Bf5$soxeB+t?ZqE+#8EBoqocUjXSru-Ta{zQz(EsAg zetZNOZNGnfyly+a-)|bIX#IEcZ3huftXGW`0RIWuh6KQD0H6yos8Kau;~=R-V8P_>Iw$W?vke;eQ!uER z=$QOzWnwouAte8ULmWz~#wQOgM?U{PH^^iOyPehDF7k;aex(s5zjGYf%_LWKj`Hds zTqsJcBD43_7oE91YZZLs7+3RZ*E8s(0WEf&maI*T;bb@V$|e<`fLz)-<$##_p8~k2 z+owjCY9;L>N}DlVOzRRQjWlAxIZPAj8q9=7UhgOR&Q+QSGbUWJS9|2 zGlNr>{dBEQ|;iue4({n`Gq!hN#JvWoKuh;ul^V5 zww+#1(^mjv{YUHQvRHv9>pVL{;+`Pdc1fdpmXk!XcjI^!;w;MuBR2oH-LGxD;dkgbCsU_ABQJoBTh$Mv(je&3l2F-0TbzDa<|ruQ;bnu1|DR#yJP*36T~&eeV&0PR0lCgXVuXtM; zmjt>OWho+6dUe{gOTu7cb7s9UCz+(9k_J_9Xa^m)BgU`fz6F~QcqMu>Z9=|v3dROZ zH9JH*ZCWP+B{{@Q?W9oslY8rq&yW8ma}F7~lyA%%;9W7uaM8ZhsNgWwbu#;8@L0VY zmLGYoT&Q(UFOsd08*hXqH2aZ-aWJMnX0`%|R+<9V{x^$(1!JRK1-*xFvM5$5C9PTS zj+qHGsq12*NeP53yu5^c0ntp$dG2|{KzN1omBs~?`a6#AWoGPHDPj2$k- zqPNh5V4hupZV|QQ=leUX2^PQPqPla9rBu}RxDObu2+ae6EDC;2w&nw%hgNw|pJ( zFbw0}XnlK%&I}Q`*?n!{LN{x__2MYGDdDcKA)!WD=|-2HBlRsmAt%MqeZr84Ffwu8 zY`fd4Ci!e+i2gW}O-&KBLj%WcI{J^;NSmb(&(2wVOq;6^cku+s%FNbwT)-Hx`1K0! z`?qg{hpj3J{$&VJb!fkhY6dC%m&s?k2Lqbh5c^Lu;sPfFj;}K6u{Wm6%)U$JR;Pzn zd?}H!BCd1aq(2F2*WqXu452}tdz>y#Mf|}QOGkgonGXj zD4ZFw)GHjY^Svz<4iAJm`?_Y`FA#z|itts*oPcbN+XTOmae0~n`+B%&(q29JekFVz zB~Z&suOf#@7NU*R{%=O2AERrfm42UAOke~uH#^Un6B@kxZ&+v4ax_I1QfX62N`4ml z^#wbT5j)VdPzwW!T|ca;;aOF*RC_4pg4--fZ9lu|_s$e^S1pQVTk@V@HrkBuu70pM zo+rf?WZ%8#zN@cxe@C_L>E`7$|Hoj7&(b}^oyxzy_-n#8ZoK1WyeS*2na>`xG!M;w ztMzH9vKAhg%sPjf2;(wILl)sU9wH7A(f1yl)9XBe z>FtaJzg_c@g9(b0%WZCX+9DP-72~rXITs(7yKGhd)j&FSSTZfUNeDMKiVZk<#$qjU0RQ@-TJYM% zw5zvSh_7gz@O-9Qv;taosZugD5lq}_dHMTOhjBK$6JR3!=u72641tw`An%xWtieEo z@=fh-awbfaHnQ2zeLKvk2~#DZ>~rOSPVTd)P@lhgIRl6irP!Hqm15eAs6E142V>%B6M_8jKb33^>(px>B&_% z`H3XvCs~c_BY&YTYAF3^@Azwz3%;rkQ_FA3~$!<-pD(993Iwy@jpuY66^i6EgX4O0jf2}oCeINEz&22)K$ zyu?wuC*wW@)ZD%+9U?jv)SCae6N=!;Kdrvd(4#)J=|8Eku6Z^vFqtOMFT3 zb@vNGfFX!}dAw9u^15FBr3el=z7*oQizfFho>G?5Yf`%+cLZKt1Q#Q8H#r4TOGk0U3qRIA423sH= zhr5OXcr2=U|H4WqlPz73HnMnb3z!R6E~KCgB8%Bi#@%fbt$GL~EuX%51ir4p#q!*{x9*r~{ z4oo8p2S~w3iIkhoyE{KB_y~#LQ0o>n>N^F_1t8-R<)sNU88*&baL+r`xcp%%wX$gfRW{{_pmbhJ|y2RCLB znSd%mLFuOiB5raQN1<#8enzYi6BR$+6DVU60e_GT(gS~#wleKEd_`sxpy02y8V&&s z@(^yPviZtfXc`;7r3I-k8V5kaQJan&$eXMOXbmlC4mByxC6WcKtU#8#d}5p-@$f}s z+nm`z$e7{~445S;fK_I(!`?oPQJkDjTy%0M&*(~0Guc8!4bUJIpb##)JU^H|SyJSG zlon~Oecswr1Dm<=GWxhaCS+0{ENDm+*nal zT(VMWv_v6k)DI2kuIRR(5KYz~_TLx`&0=yDfbinbFc-?`!mrOMnqY^V^kBx4$lYF*Fwe52eM7RbZ9OtiqVnIq!CuPh*JeZmf z3n-6D%6@0{Y6(X!DbGF;e?D<@YAo-$3tW(?)vxLd-E+?|TcOr5@rTVu$F`B)(<*d< zumttOqFWJ)!lGyJW7rq%4urLPD&vPZ01i}CP>u(xHvq_#=frt&N3@dY0BtWem$) zVNs@o_nIXmGGdpOCY#F2CM5Iq|2%^D12d~eb%@(qpWS(g&Gs$2vH$~$`RCX|eU{HP ziAk>NPQix|OWwy*+QbNQLArG)43l*e0k;3?-djg+azP*XVKB+*bgr)|Wmiqi?SJjP zayMz`N{~b8D1CC$LvzBXrLKiCU5p-G=kY`Hjq7MuQK3c%y{ZzEvmNU5!yv%t=mz_K|zr)1Q+z*J^%N1 zyh)IC1RoD#9L-uZFK@)jF(vlYz+3>td;{|?jt zj`januROJ;Mo<#F==58HLM8lYlaNqav*r?w@W zAL9QvEZS%6EK6CToCVnNy;ZKKzwSKNkWbVc%2*YFM3D=;adc>#KcExaE>Z<8T!mJ4 zd!ZpQr^@VoSOk8uZcEjGfnDUN)so*%>F z(P`}UD9C?cdT%E)rCHv_32A7B68ew9>xMt*wcbYK?Ax~6%c}%n3N1I3sKEIqd+q2N z>~>QoqG$zC*l}@7O7tkvmz{pU#@t=68@XbRL#!BvW3Tcg5t0ex`i7*Md{_WMp%QIs zpAvv9JMVfo+4h=mTI*5%6H&7(MbkYe+E(P)%~>8yMbiqGjvHb7&e(kXd{hEWtbHqV z@p05nt8{dgY33~-Caq`q$%kl{EnUJ{&g#Izxj3OE^@6E>o@KSx9G+7(%A^SGy(&jw z26}8@opa8WK)tbtC;k#@$b0aL5537DzXxxt%yXG4-0rvBaeM>5_Alc?JvJ~KAFx7Y zN9QxnaJhuV%0o5kHoTG2@4rUfZ2W3Hk4n;{a_GXdg(I6D%=H`Ugyp8n2A~K zr|@CY>ru;G$>2&ky9oVROs}kI(}p`6ICQnNa#zRs1$T(AU{~pjFxA3Pz}XC&E!?9^ zeqws-60lBQwd{rBHB?c^UNcz2%@oMrwJ7{Ygm{NZ;_gs;REZLmlCtf~k z(KWT>XUhMT*t`8vP$S%OrsU!Jt)>`9%B<_bospiNKR9Ypo%p8ju6us(w8Y};k9d0O z5?vk{XAkZarA(2p)S0~i<>4E)u#VPybn2=yju|d>uY`g?fpY5jU)s3#x>-UupsmW( zF>|_PNQ3IR2+D`S5B!yV6v-`L`g595b}M_t=r%?5YocA>t`L@M96~}5<mNn@B`*d3)4^REU`Wst;!%2E(vO*GTu;US$+)ta8GN<9?Ld{pj zY37W Date: Fri, 18 Nov 2016 15:58:36 -0800 Subject: [PATCH 52/53] fixing tests from graylinkim/ggtracker merge --- sc2reader/engine/plugins/creeptracker.py | 38 +++++++++++++----------- test_replays/test_all.py | 7 +++-- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 25ead4af..14120770 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -5,8 +5,12 @@ from sets import Set except ImportError: Set = set -from PIL.Image import open as PIL_open -from PIL.Image import ANTIALIAS +try: + # required for CreepTracker, but CreepTracker is optional + from PIL.Image import open as PIL_open + from PIL.Image import ANTIALIAS +except ImportError: + pass try: from StringIO import StringIO except ImportError: @@ -43,7 +47,7 @@ def handleUnitDiedEvent(self, event, replay): except Exception as e: print("Whoa! {}".format(e)) pass - + def handleUnitInitEvent(self,event,replay): try: @@ -53,7 +57,7 @@ def handleUnitInitEvent(self,event,replay): except Exception as e: print("Whoa! {}".format(e)) pass - + def handleUnitBornEvent(self,event,replay): try: if event.unit_type_name== "Hatchery": @@ -81,14 +85,14 @@ def handleEndGame(self, event, replay): pass -## The class used to used to calculate the creep spread +## The class used to used to calculate the creep spread class creep_tracker(): def __init__(self,replay): #if the debug option is selected, minimaps will be printed to a file - ##and a stringIO containing the minimap image will be saved for - ##every minite in the game and the minimap with creep highlighted + ##and a stringIO containing the minimap image will be saved for + ##every minite in the game and the minimap with creep highlighted ## will be printed out. - self.debug = replay.opt.debug + self.debug = replay.opt['debug'] ##This list contains creep spread area for each player self.creep_spread_by_minute = dict() ## this list contains a minimap highlighted with creep spread for each player @@ -125,7 +129,7 @@ def __init__(self,replay): # resize height to MAPHEIGHT, and compute new width that # would preserve aspect ratio self.map_width = int(cropsize[0] * (float(self.map_height) / cropsize[1])) - self.mapSize =self.map_height * self.map_width + self.mapSize =self.map_height * self.map_width ## the following parameters are only needed if minimaps have to be printed # minimapSize = ( self.map_width,int(self.map_height) ) @@ -138,10 +142,10 @@ def __init__(self,replay): imageCenter = [(self.map_width/2), self.map_height/2] # this is the scaling factor to go from the SC2 coordinate # system to pixel coordinates - self.image_scale = float(self.map_height) / cropsize[1] + self.image_scale = float(self.map_height) / cropsize[1] self.transX =imageCenter[0] + self.image_scale * (mapCenter[0]) self.transY = imageCenter[1] + self.image_scale * (mapCenter[1]) - + def radius_to_map_positions(self,radius): ## this function converts all radius into map coordinates ## centred around the origin that the creep can exist @@ -180,7 +184,7 @@ def add_to_list(self,player_id,unit_id,unit_location,unit_type,event_time): def remove_from_list(self,unit_id,time_frame): ## This function searches is given a unit ID for every unit who died ## the unit id will be searched in cgu_gen_units for matches - ## if there are any, that unit will be removed from active CGUs + ## if there are any, that unit will be removed from active CGUs ## and appended as a new time frame for player_id in self.creep_gen_units: length_cgu_list = len(self.creep_gen_units[player_id]) @@ -194,7 +198,7 @@ def remove_from_list(self,unit_id,time_frame): self.creep_gen_units_times[player_id].append(time_frame) def cgu_gen_times_to_chunks(self,cgu_time_list): - ## this function returns the index and value of every cgu time + ## this function returns the index and value of every cgu time maximum_cgu_time = max(cgu_time_list) for i in range(0, maximum_cgu_time): a = list(filter(lambda x_y: x_y[1]//60==i , enumerate(cgu_time_list))) @@ -211,7 +215,7 @@ def cgu_in_min_to_cgu_units(self,player_id,cgu_in_minutes): cgu_units.append(self.creep_gen_units[player_id][index]) cgu_max_in_minute = max(cgu_units,key = len) yield cgu_max_in_minute - + def reduce_cgu_per_minute(self,player_id): #the creep_gen_units_lists contains every single time frame #where a CGU is added, @@ -224,7 +228,7 @@ def reduce_cgu_per_minute(self,player_id): self.creep_gen_units_times[player_id] = list(minutes) def get_creep_spread_area(self,player_id): - ## iterates through all cgus and and calculate the area + ## iterates through all cgus and and calculate the area for index,cgu_per_player in enumerate(self.creep_gen_units[player_id]): # convert cgu list into centre of circles and radius cgu_radius = map(lambda x: (x[1], self.unit_name_to_radius[x[2]]),\ @@ -234,7 +238,7 @@ def get_creep_spread_area(self,player_id): creep_area_positions = self.cgu_radius_to_map_positions(cgu_radius,self.radius_to_coordinates) cgu_event_time = self.creep_gen_units_times[player_id][index] cgu_event_time_str=str(int(cgu_event_time//60))+":"+str(cgu_event_time%60) - if self.debug: + if self.debug: self.print_image(creep_area_positions,player_id,cgu_event_time_str) creep_area = len(creep_area_positions) self.creep_spread_by_minute[player_id][cgu_event_time]=\ @@ -251,7 +255,7 @@ def cgu_radius_to_map_positions(self,cgu_radius,radius_to_coordinates): point = cgu[0] radius = cgu[1] ## subtract all radius_to_coordinates with centre of - ## cgu radius to change centre of circle + ## cgu radius to change centre of circle cgu_map_position = map( lambda x:(x[0]+point[0],x[1]+point[1])\ ,self.radius_to_coordinates[radius]) total_points_on_map= total_points_on_map | Set(cgu_map_position) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index f0f9e671..37bc8d38 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -13,6 +13,7 @@ import unittest import sc2reader +from sc2reader.exceptions import CorruptTrackerFileError sc2reader.log_utils.log_to_console("INFO") @@ -395,8 +396,10 @@ def test_creepTracker(self): assert replay.player[2].creep_spread_by_minute[780] == 22.42 def test_bad_unit_ids(self): - replay = sc2reader.load_replay("test_replays/2.0.11.26825/bad_unit_ids_1.SC2Replay", load_level=4) - replay = sc2reader.load_replay("test_replays/2.0.9.26147/bad_unit_ids_2.SC2Replay", load_level=4) + with self.assertRaises(CorruptTrackerFileError): + replay = sc2reader.load_replay("test_replays/2.0.11.26825/bad_unit_ids_1.SC2Replay", load_level=4) + with self.assertRaises(CorruptTrackerFileError): + replay = sc2reader.load_replay("test_replays/2.0.9.26147/bad_unit_ids_2.SC2Replay", load_level=4) def test_daedalus_point(self): replay = sc2reader.load_replay("test_replays/2.0.11.26825/DaedalusPoint.SC2Replay") From c86eb5a6076913373b438e9080424f7f1b546fd7 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 28 Nov 2016 14:01:28 -0800 Subject: [PATCH 53/53] fixing more references from the CHANGELOG --- sc2reader/factories/plugins/replay.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 492052b1..ad465609 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -140,13 +140,13 @@ def SelectionTracker(replay): if debug: logger.info("[{0}] {1} selected {2} units: {3}".format(Length(seconds=event.second), person.name, len(selections[0x0A].objects), selections[0x0A])) - elif event.name == 'SetToHotkeyEvent': + elif event.name == 'SetControlGroupEvent': selections = player_selections[event.frame] selections[event.control_group] = selections[0x0A].copy() if debug: logger.info("[{0}] {1} set hotkey {2} to current selection".format(Length(seconds=event.second), person.name, event.hotkey)) - elif event.name == 'AddToHotkeyEvent': + elif event.name == 'AddToControlGroupEvent': selections = player_selections[event.frame] control_group = selections[event.control_group].copy() error = not control_group.deselect(event.mask_type, event.mask_data) @@ -155,7 +155,7 @@ def SelectionTracker(replay): if debug: logger.info("[{0}] {1} added current selection to hotkey {2}".format(Length(seconds=event.second), person.name, event.hotkey)) - elif event.name == 'GetFromHotkeyEvent': + elif event.name == 'GetControlGroupEvent': selections = player_selections[event.frame] control_group = selections[event.control_group].copy() error = not control_group.deselect(event.mask_type, event.mask_data)

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%0D2_Sa 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 IlGYFY3sV=Xi2wiq literal 0 HcmV?d00001 diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 81c5eadd..2b3b28f1 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -15,6 +15,7 @@ sc2reader.log_utils.log_to_console("INFO") +print("YO YO YO THIS IS FOR REAL") class TestReplays(unittest.TestCase): @@ -369,6 +370,8 @@ def test_daedalus_point(self): def test_reloaded(self): replay = sc2reader.load_replay("test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay") + def test_214(self): + replay = sc2reader.load_replay("test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4) class TestGameEngine(unittest.TestCase): From 6f30900c70a4d5ac95abb11bf98fe8a61f949cad Mon Sep 17 00:00:00 2001 From: David Joerg Date: Fri, 26 Sep 2014 12:09:07 -0400 Subject: [PATCH 39/53] fix for 2.1.4 --- sc2reader/readers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index e3434fd7..65cc3d5e 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -95,6 +95,7 @@ def __call__(self, data, replay): ai_build=data.read_bits(7) if replay.base_build >= 23925 else None, handicap=data.read_bits(7), observe=data.read_bits(2), + logo_index=data.read_uint32() if replay.base_build >= 32283 else None, working_set_slot_id=data.read_uint8() if replay.base_build >= 24764 and data.read_bool() else None, rewards=[data.read_uint32() for i in range(data.read_bits(6 if replay.base_build >= 24764 else 5))], toon_handle=data.read_aligned_string(data.read_bits(7)) if replay.base_build >= 17266 else None, From 6580ebb3584bdbdbd65f8feb9a9089d68b809cb9 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 28 Dec 2014 23:26:42 -0500 Subject: [PATCH 40/53] Fix various issues related to issue #180. --- sc2reader/engine/plugins/context.py | 4 ++-- sc2reader/scripts/sc2parse.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index e978ea1f..4bead0b9 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -28,7 +28,7 @@ def handleCommandEvent(self, event, replay): if not getattr(replay, 'marked_error', None): replay.marked_error = True event.logger.error(replay.filename) - event.logger.error("Release String: "+replay.release_string) + event.logger.error("Release String: " + replay.release_string) for player in replay.players: event.logger.error("\t{0}".format(player)) @@ -254,7 +254,7 @@ def load_message_game_player(self, event, replay): event.player = replay.human[event.pid] event.player.events.append(event) elif event.pid != 16: - self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frames)) + self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frame)) else: pass # This is a global event diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index d0808340..8885c671 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -66,10 +66,10 @@ def main(): print("") print(path) print('{build} - {real_type} on {map_name} - Played {start_time}'.format(**e.replay.__dict__)) - print('[ERROR]', e.message) + print('[ERROR] {}', e) for event in e.game_events[-5:]: print('{0} - {1}'.format(hex(event.type), event.bytes.encode('hex'))) - print(e.buffer.read_range(e.location, e.location+50).encode('hex')) + print(e.buffer.read_range(e.location, e.location + 50).encode('hex')) print except Exception as e: print("") @@ -77,20 +77,20 @@ def main(): try: replay = sc2reader.load_replay(path, debug=True, load_level=2) print('{build} - {real_type} on {map_name} - Played {start_time}'.format(**replay.__dict__)) - print('[ERROR] {0}'.format(e.message)) + print('[ERROR] {0}'.format(e)) for pid, attributes in replay.attributes.items(): print("{0} {1}".format(pid, attributes)) - for pid, info in enumerate(replay.raw_data['replay.details'].players): + for pid, info in enumerate(replay.players): print("{0} {1}".format(pid, info)) - for message in replay.raw_data['replay.message.events'].messages: + for message in replay.messages: print("{0} {1}".format(message.pid, message.text)) traceback.print_exc() print("") except Exception as e2: replay = sc2reader.load_replay(path, debug=True, load_level=0) print('Total failure parsing {release_string}'.format(**replay.__dict__)) - print('[ERROR] {0}'.format(e.message)) - print('[ERROR] {0}'.format(e2.message)) + print('[ERROR] {0}'.format(e)) + print('[ERROR] {0}'.format(e2)) traceback.print_exc() print From 0dbe56191137c773ff717842ccbc9b28b3b4d677 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 29 Dec 2014 02:29:05 -0500 Subject: [PATCH 41/53] Fix gameheart plugin handling of teams and observers. Closes #174. --- sc2reader/engine/plugins/gameheart.py | 3 +++ sc2reader/objects.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/gameheart.py b/sc2reader/engine/plugins/gameheart.py index 2812f5b1..08bfc67e 100644 --- a/sc2reader/engine/plugins/gameheart.py +++ b/sc2reader/engine/plugins/gameheart.py @@ -69,6 +69,9 @@ def fix_events(self, replay, start_frame): def fix_entities(self, replay, actual_players): # Change the players that aren't playing into observers for p in [p for p in replay.players if p.pid not in actual_players]: + # Fix the slot data to be accurate + p.slot_data['observe'] = 1 + p.slot_data['team_id'] = None obs = Observer(p.sid, p.slot_data, p.uid, p.init_data, p.pid) # Because these obs start the game as players the client diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 684c8180..711203a6 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -101,7 +101,9 @@ def __init__(self, sid, slot_data): self.handicap = slot_data['handicap'] #: The entity's team number. None for observers - self.team_id = slot_data['team_id']+1 + self.team_id = None + if slot_data['team_id'] is not None: + self.team_id = slot_data['team_id'] + 1 #: A flag indicating if the person is a human or computer #: Really just a shortcut for isinstance(entity, User) From 83d38092ad2c80f86a1b4324022038f1a4f044e2 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 29 Dec 2014 02:30:38 -0500 Subject: [PATCH 42/53] Add option to print observers to sc2printer. --- sc2reader/scripts/sc2printer.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 5a1bf94e..c780d16e 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -28,6 +28,11 @@ def printReplay(filepath, arguments): print(" Team {0}\t{1} ({2})".format(team.number, team.players[0].name, team.players[0].pick_race[0])) for player in team.players[1:]: print(" \t{0} ({1})".format(player.name, player.pick_race[0])) + if arguments.observers: + print(" Observers:") + for observer in replay.observers: + print(" {0}".format(observer.name)) + if arguments.messages: print(" Messages:") for message in replay.messages: @@ -43,9 +48,9 @@ def printReplay(filepath, arguments): print("\nVersion {0} replay:\n\t{1}".format(e.replay.release_string, e.replay.filepath)) print("\t{0}, Type={1:X}".format(e.msg, e.type)) print("\tPrevious Event: {0}".format(prev.name)) - print("\t\t"+prev.bytes.encode('hex')) + print("\t\t" + prev.bytes.encode('hex')) print("\tFollowing Bytes:") - print("\t\t"+e.buffer.read_range(e.location, e.location+30).encode('hex')) + print("\t\t" + e.buffer.read_range(e.location, e.location + 30).encode('hex')) print("Error with '{0}': ".format(filepath)) print(e) except Exception as e: @@ -92,13 +97,15 @@ def main(): shared_args = parser.add_argument_group('Shared Arguments') shared_args.add_argument('--date', action="store_true", default=True, - help="print(game date [default on]") + help="print game date [default on]") shared_args.add_argument('--length', action="store_true", default=False, - help="print(game duration mm:ss in game time (not real time) [default off]") + help="print game duration mm:ss in game time (not real time) [default off]") shared_args.add_argument('--map', action="store_true", default=True, - help="print(map name [default on]") + help="print map name [default on]") shared_args.add_argument('--teams', action="store_true", default=True, - help="print(teams, their players, and the race matchup [default on]") + help="print teams, their players, and the race matchup [default on]") + shared_args.add_argument('--observers', action="store_true", default=True, + help="print observers") replay_args = parser.add_argument_group('Replay Options') replay_args.add_argument('--messages', action="store_true", default=False, From 26c569e501bdac7af9a84fd342bdebceb4d62f55 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 29 Dec 2014 16:09:55 -0500 Subject: [PATCH 43/53] Harden up the code for processing s2ma files. --- sc2reader/resources.py | 60 ++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 44a26c7a..50b47623 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -612,17 +612,20 @@ def __getstate__(self): class Map(Resource): url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma' - #: The localized (only enUS supported right now) map name - name = str() + def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): + super(Map, self).__init__(map_file, filename, **options) - #: The map's author - author = str() + #: The localized (only enUS supported right now) map name. + self.name = str() - #: The map description as written by author - description = str() + #: The localized (only enUS supported right now) map author. + self.author = str() - def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): - super(Map, self).__init__(map_file, filename, **options) + #: The localized (only enUS supported right now) map description. + self.description = str() + + #: The localized (only enUS supported right now) map website. + self.website = str() #: The unique hash used to identify this map on bnet's depots. self.hash = map_hash @@ -643,9 +646,9 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option # Clearly this isn't a great solution but we can't be throwing exceptions # just because US English wasn't a concern of the map author. # TODO: Make this work regardless of the localizations available. - game_strings = self.archive.read_file('enUS.SC2Data\LocalizedData\GameStrings.txt').decode('utf8') - if game_strings: - for line in game_strings.split('\r\n'): + game_strings_file = self.archive.read_file('enUS.SC2Data\LocalizedData\GameStrings.txt') + if game_strings_file: + for line in game_strings_file.decode('utf8').split('\r\n'): if len(line) == 0: continue @@ -660,21 +663,26 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option self.website = value #: A reference to the map's :class:`~sc2reader.objects.MapInfo` object - self.map_info = MapInfo(self.archive.read_file('MapInfo')) - - doc_info = ElementTree.fromstring(self.archive.read_file('DocumentInfo').decode('utf8')) - - icon_path_node = doc_info.find('Icon/Value') - #: (Optional) The path to the icon for the map, relative to the archive root - self.icon_path = icon_path_node.text if icon_path_node is not None else None - - #: (Optional) The icon image for the map in tga format - self.icon = self.archive.read_file(self.icon_path) if self.icon_path is not None else None - - #: A list of module names this map depends on - self.dependencies = list() - for dependency_node in doc_info.findall('Dependencies/Value'): - self.dependencies.append(dependency_node.text) + self.map_info = None + map_info_file = self.archive.read_file('MapInfo') + if map_info_file: + self.map_info = MapInfo(map_info_file) + + doc_info_file = self.archive.read_file('DocumentInfo') + if doc_info_file: + doc_info = ElementTree.fromstring(doc_info_file.decode('utf8')) + + icon_path_node = doc_info.find('Icon/Value') + #: (Optional) The path to the icon for the map, relative to the archive root + self.icon_path = icon_path_node.text if icon_path_node is not None else None + + #: (Optional) The icon image for the map in tga format + self.icon = self.archive.read_file(self.icon_path) if self.icon_path is not None else None + + #: A list of module names this map depends on + self.dependencies = list() + for dependency_node in doc_info.findall('Dependencies/Value'): + self.dependencies.append(dependency_node.text) @classmethod def get_url(cls, region, map_hash): From 6c48e8a93216c9371b6b935029d1c8e92da850dd Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 29 Dec 2014 17:15:16 -0500 Subject: [PATCH 44/53] This is a polish translation. --- sc2reader/constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sc2reader/constants.py b/sc2reader/constants.py index 7b701dee..43c2d66e 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -23,7 +23,7 @@ '프로토스': 'Protoss', '저그': 'Zerg', - # ??eu + # plPL 'Terranie': 'Terran', 'Protosi': 'Protoss', 'Zergi': 'Zerg', @@ -126,6 +126,7 @@ 1: 'kr', 2: 'tw', }, + # Taiwan - appear to both map to same place 'tw': { 1: 'kr', From a194b677c5f702c6d122cb956a076a39b324478f Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Wed, 31 Dec 2014 14:06:55 -0500 Subject: [PATCH 45/53] Remove PersonDict and AttributeDict implementations. --- sc2reader/factories/plugins/replay.py | 2 +- sc2reader/factories/sc2factory.py | 4 +-- sc2reader/readers.py | 2 +- sc2reader/resources.py | 30 ++++++++-------- sc2reader/utils.py | 52 ++------------------------- 5 files changed, 22 insertions(+), 68 deletions(-) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index f6b19e79..ccf29401 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -123,7 +123,7 @@ def APMTracker(replay): @plugin def SelectionTracker(replay): - debug = replay.opt.debug + debug = replay.opt['debug'] logger = log_utils.get_logger(SelectionTracker) for person in replay.entities: diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index 7518767b..36020b2d 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -156,7 +156,7 @@ def _get_plugins(self, cls): return plugins def _get_options(self, cls, **new_options): - options = utils.AttributeDict() + options = dict() for opt_cls, cls_options in self.options.items(): if issubclass(cls, opt_cls): options.update(cls_options) @@ -175,7 +175,7 @@ def _load_resources(self, resources, options=None, **new_options): yield self._load_resource(resource, options=options) def load_remote_resource_contents(self, resource, **options): - self.logger.info("Fetching remote resource: "+resource) + self.logger.info("Fetching remote resource: " + resource) return urlopen(resource).read() def load_local_resource_contents(self, location, **options): diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 65cc3d5e..ad539a0c 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -297,7 +297,7 @@ def __call__(self, data, replay): # method short cuts, avoid dict lookups EVENT_DISPATCH = self.EVENT_DISPATCH - debug = replay.opt.debug + debug = replay.opt['debug'] tell = data.tell read_frames = data.read_frames read_bits = data.read_bits diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 50b47623..0aedcffd 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -23,7 +23,7 @@ class Resource(object): def __init__(self, file_object, filename=None, factory=None, **options): self.factory = factory - self.opt = utils.AttributeDict(options) + self.opt = options self.logger = log_utils.get_logger(self.__class__) self.filename = filename or getattr(file_object, 'name', 'Unavailable') @@ -137,7 +137,7 @@ class Replay(Resource): #: A dual key dict mapping player names and numbers to #: :class:`Player` objects - player = utils.PersonDict() + player = dict() #: A list of :class:`Observer` objects from the game observers = list() @@ -148,7 +148,7 @@ class Replay(Resource): #: A dual key dict mapping :class:`Person` object to their #: person id's and names - person = utils.PersonDict() + person = dict() #: A list of :class:`Person` objects from the game representing #: only the human players from the :attr:`people` list @@ -214,11 +214,11 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.events = 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.player = dict() + self.observer = dict() + self.human = dict() + self.computer = dict() + self.entity = dict() self.players = list() self.observers = list() # Unordered list of Observer @@ -599,7 +599,7 @@ 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']: + 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 __getstate__(self): @@ -862,7 +862,7 @@ def load_translations(self): self.lang_sheets = dict() self.translations = dict() for lang, files in self.localization_urls.items(): - if lang != self.opt.lang: + if lang != self.opt['lang']: continue sheets = list() @@ -873,9 +873,9 @@ def load_translations(self): for uid, (sheet, item) in self.id_map.items(): if sheet < len(sheets) and item in sheets[sheet]: translation[uid] = sheets[sheet][item] - elif self.opt.debug: + elif self.opt['debug']: msg = "No {0} translation for sheet {1}, item {2}" - raise SC2ReaderLocalizationError(msg.format(self.opt.lang, sheet, item)) + raise SC2ReaderLocalizationError(msg.format(self.opt['lang'], sheet, item)) else: translation[uid] = "Unknown" @@ -883,7 +883,7 @@ def load_translations(self): self.translations[lang] = translation def load_map_info(self): - map_strings = self.lang_sheets[self.opt.lang][-1] + map_strings = self.lang_sheets[self.opt['lang']][-1] self.map_name = map_strings[1] self.map_description = map_strings[2] self.map_tileset = map_strings[3] @@ -942,7 +942,7 @@ def use_property(prop, player=None): activated[(prop.id, player)] = use return use - translation = self.translations[self.opt.lang] + translation = self.translations[self.opt['lang']] for uid, prop in properties.items(): name = translation.get(uid, "Unknown") if prop.is_lobby: @@ -956,7 +956,7 @@ def use_property(prop, player=None): self.player_settings[index][name] = translation[(uid, value)] def load_player_stats(self): - translation = self.translations[self.opt.lang] + translation = self.translations[self.opt['lang']] stat_items = sum([p[0] for p in self.parts[3:]], []) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index e728c983..44634de7 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -48,57 +48,11 @@ def __str__(self): return self.url -class PersonDict(dict): - """ - Deprecated! - - Supports lookup on both the player name and player id - - :: - - person = PersonDict() - person[1] = Player(1,"ShadesofGray") - me = person.name("ShadesofGray") - del person[me.pid] - - Delete is supported on the player id only - """ - def __init__(self): - super(PersonDict, self).__init__() - self._key_map = dict() - - def name(self, player_name): - """ deprecated because it is possible for multiple players to have the same name. """ - return self[self._key_map[player_name]] - - def __setitem__(self, key, value): - self._key_map[value.name] = key - super(PersonDict, self).__setitem__(key, value) - - def windows_to_unix(windows_time): # This windows timestamp measures the number of 100 nanosecond periods since # January 1st, 1601. First we subtract the number of nanosecond periods from # 1601-1970, then we divide by 10^7 to bring it back to seconds. - return int((windows_time-116444735995904000)/10**7) - - -class AttributeDict(dict): - """ - Support access to dictionary items via the dot syntax as though they - were class attributes. Also support setting new keys via dot syntax. - """ - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError('No such attribute {0}'.format(name)) - - def __setattr__(self, name, value): - self[name] = value - - def copy(self): - return AttributeDict(self.items()) + return int((windows_time - 116444735995904000) / 10 ** 7) @loggable @@ -252,12 +206,12 @@ class Length(timedelta): @property def hours(self): """ The number of hours in represented. """ - return self.seconds//3600 + return self.seconds // 3600 @property def mins(self): """ The number of minutes in excess of the hours. """ - return self.seconds//60 % 60 + return self.seconds // 60 % 60 @property def secs(self): From 3e1cea3759a912775f45d45ab6f5eb12ee307f1f Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 5 Jan 2015 10:53:50 -0500 Subject: [PATCH 46/53] Remove dead code. --- sc2reader/utils.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 44634de7..2ee3d382 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -151,12 +151,6 @@ def recovery_attempt(): raise MPQError("Unable to extract file: {0}".format(data_file), e) -def merged_dict(a, b): - c = a.copy() - c.update(b) - return c - - def get_files(path, exclude=list(), depth=-1, followlinks=False, extension=None, **extras): """ Retrieves files from the given path with configurable behavior. From 722ae4f16430c023bafa32fa69a1773909b1f867 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Mon, 5 Jan 2015 10:54:47 -0500 Subject: [PATCH 47/53] Bump version to a pre-release (for clarity). --- sc2reader/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 040febcb..8ae84731 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -20,7 +20,7 @@ """ from __future__ import absolute_import, print_function, unicode_literals, division -__version__ = "0.6.4" +__version__ = "0.7.0-pre" import os import sys diff --git a/setup.py b/setup.py index c9dee97d..9e4b3f82 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version='0.6.4', + version='0.7.0-pre', keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read()+"\n\n"+open("CHANGELOG.rst").read(), From c5d22d68ba802f36068ed7199cc157ee0521819b Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Wed, 11 Feb 2015 10:51:15 -0800 Subject: [PATCH 48/53] decoding this so that it reads as a string not bytes in python 3 --- sc2reader/events/tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 5466d4d5..41b5c2f2 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -442,7 +442,7 @@ def __init__(self, frames, data, build): self.player = None #: The name of the upgrade - self.upgrade_type_name = data[1] + self.upgrade_type_name = data[1].decode('utf8') #: The number of times this upgrade as been researched self.count = data[2] From 20156ecd41d03cb581071dc74e23b63a3fffe2f0 Mon Sep 17 00:00:00 2001 From: Graylin Kim Date: Sun, 15 Mar 2015 13:46:32 -0400 Subject: [PATCH 49/53] Fix ancient typo in struct decoder. refs #184 --- sc2reader/decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index 63324649..b2ff35f9 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -368,7 +368,7 @@ def read_struct(self, datatype=None): data = self._buffer.read_bytes(4) # self.read_uint32() elif datatype == 0x08: # u64 - data = self._buffer.read_unit64() + data = self._buffer.read_uint64() elif datatype == 0x09: # vint data = self.read_vint() From 2139abc8b7c4fa27f74b26919b0c9a2d367a4163 Mon Sep 17 00:00:00 2001 From: dsjoerg Date: Wed, 7 Oct 2015 10:25:40 -0400 Subject: [PATCH 50/53] remove debugging print statement --- test_replays/test_all.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 2b3b28f1..32fd00e9 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -15,8 +15,6 @@ sc2reader.log_utils.log_to_console("INFO") -print("YO YO YO THIS IS FOR REAL") - class TestReplays(unittest.TestCase): def test_teams(self): From d69feb4e0be597581040588193579d29e8241431 Mon Sep 17 00:00:00 2001 From: dsjoerg Date: Wed, 7 Oct 2015 10:26:01 -0400 Subject: [PATCH 51/53] add failing test for 3.0 --- test_replays/3.0.0.38215/first.SC2Replay | Bin 0 -> 82735 bytes test_replays/test_all.py | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 test_replays/3.0.0.38215/first.SC2Replay diff --git a/test_replays/3.0.0.38215/first.SC2Replay b/test_replays/3.0.0.38215/first.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..373abc281c175d761b1cd76b0e9e0847fa6351bd GIT binary patch literal 82735 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%0Dq1U`;qn$u36EtnP`9ah;J{3wQ2J1>SbOQSs)?ku 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