Skip to content

Commit c28aa50

Browse files
committed
Make Replay self-construct with options provided.
Now you can directly call Replay(open(filename,'rb')) instead of going through the factory layer if you so choose. This is now the standard resource interface and will allow for many simplications in the factory classes.
1 parent 9d10152 commit c28aa50

File tree

1 file changed

+126
-34
lines changed

1 file changed

+126
-34
lines changed

sc2reader/resources.py

Lines changed: 126 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from StringIO import StringIO
99
from collections import defaultdict
1010

11+
import urllib2
1112
from mpyq import MPQArchive
1213

1314
from sc2reader import utils
1415
from sc2reader import log_utils
16+
from sc2reader import readers, data
1517
from sc2reader.objects import Player, Observer, Team, PlayerSummary, Graph
1618
from sc2reader.constants import REGIONS, LOCALIZED_RACES, GAME_SPEED_FACTOR, GAME_SPEED_CODES, RACE_CODES, PLAYER_TYPE_CODES, TEAM_COLOR_CODES, GAME_FORMAT_CODES, GAME_TYPE_CODES, DIFFICULTY_CODES
1719

@@ -107,14 +109,10 @@ class Replay(Resource):
107109
#: If there is a valid winning team this will contain a :class:`Team` otherwise it will be :class:`None`
108110
winner = None
109111

110-
def __init__(self, replay_file, filename=None, **options):
112+
def __init__(self, replay_file, filename=None, load_level=4, **options):
111113
super(Replay, self).__init__(replay_file, filename, **options)
112114
self.datapack = None
113115
self.raw_data = dict()
114-
self.listeners = defaultdict(list)
115-
116-
self.__dict__.update(utils.read_header(replay_file))
117-
self.archive = utils.open_archive(replay_file)
118116

119117
#default values, filled in during file read
120118
self.player_names = list()
@@ -144,26 +142,52 @@ def __init__(self, replay_file, filename=None, **options):
144142

145143
self.objects = {}
146144

147-
148-
def add_listener(self, event_class, listener):
149-
self.listeners[event_class].append(listener)
150-
151-
def read_data(self, data_file, reader):
152-
data = utils.extract_data_file(data_file,self.archive)
153-
if data:
154-
data_buffer = utils.ReplayBuffer(data)
155-
self.raw_data[data_file] = reader(data_buffer, self)
156-
elif self.opt.debug and data_file != 'replay.message.events':
157-
raise ValueError("{0} not found in archive".format(data_file))
158-
else:
159-
self.logger.error("{0} not found in archive".format(data_file))
145+
# Bootstrap the readers.
146+
self.registered_readers = defaultdict(list)
147+
self.register_default_readers()
148+
149+
# Bootstrap the datapacks.
150+
self.registered_datapacks= list()
151+
self.register_default_datapacks()
152+
153+
# Unpack the MPQ and read header data if requested
154+
if load_level >= 0:
155+
self.__dict__.update(utils.read_header(replay_file))
156+
self.archive = utils.open_archive(replay_file)
157+
158+
# Load basic details if requested
159+
if load_level >= 1:
160+
for data_file in ('replay.initData','replay.details','replay.attributes.events'):
161+
self._read_data(data_file, self._get_reader(data_file))
162+
self.load_details()
163+
self.datapack = self._get_datapack()
164+
165+
# Can only be effective if map data has been loaded
166+
if options.get('load_map', False):
167+
map_url = Map.get_url(self.gateway, self.map_hash)
168+
print map_url
169+
map_file = StringIO(urllib2.urlopen(map_url).read())
170+
replay.map = Map(map_file, filename=self.map, gateway=self.gateway, map_hash=self.map_hash)
171+
172+
173+
# Load players if requested
174+
if load_level >= 2:
175+
for data_file in ('replay.message.events'):
176+
self._read_data(data_file, self._get_reader(data_file))
177+
self.load_players()
178+
179+
# Load events if requested
180+
if load_level >= 3:
181+
for data_file in ('replay.game.events'):
182+
self._read_data(data_file, self._get_reader(data_file))
183+
self.load_events()
160184

161185
def load_details(self):
162186
if 'replay.initData' in self.raw_data:
163187
initData = self.raw_data['replay.initData']
164188
if initData.map_data:
165189
self.gateway = initData.map_data[0].gateway
166-
self.map_hash = initData.map_data[-1].map_hash
190+
self.map_hash = initData.map_data[-1].map_hash.encode('hex')
167191

168192
#Expand this special case mapping
169193
if self.gateway == 'sg':
@@ -331,9 +355,7 @@ def load_players(self):
331355
hash_input = self.gateway+":"+','.join(player_names)
332356
self.people_hash = hashlib.sha256(hash_input).hexdigest()
333357

334-
def load_events(self, datapack=None):
335-
self.datapack = datapack
336-
358+
def load_events(self):
337359
# Copy the events over
338360
# TODO: the events need to be fixed both on the reader and processor side
339361
if 'replay.game.events' in self.raw_data:
@@ -353,18 +375,88 @@ def load_events(self, datapack=None):
353375
if event.pid != 16:
354376
self.person[event.pid].events.append(event)
355377

356-
357-
def start(self):
358-
self.stopped = False
359-
360-
for listener in self.listeners:
361-
listener.setup(self)
362-
363-
for event in self.events:
364-
for listener in self.listeners:
365-
if listener.accepts(event):
366-
listener(event, self)
367-
378+
def register_reader(self, data_file, reader, filterfunc=lambda r: True):
379+
"""
380+
Allows you to specify your own reader for use when reading the data
381+
files packed into the .SC2Replay archives. Datapacks are checked for
382+
use with the supplied filterfunc in reverse registration order to give
383+
user registered datapacks preference over factory default datapacks.
384+
385+
Don't use this unless you know what you are doing.
386+
387+
:param data_file: The full file name that you would like this reader to
388+
parse.
389+
390+
:param reader: The :class:`Reader` object you wish to use to read the
391+
data file.
392+
393+
:param filterfunc: A function that accepts a partially loaded
394+
:class:`Replay` object as an argument and returns true if the
395+
reader should be used on this replay.
396+
"""
397+
self.registered_readers[data_file].insert(0,(filterfunc, reader))
398+
399+
def register_datapack(self, datapack, filterfunc=lambda r: True):
400+
"""
401+
Allows you to specify your own datapacks for use when loading replays.
402+
Datapacks are checked for use with the supplied filterfunc in reverse
403+
registration order to give user registered datapacks preference over
404+
factory default datapacks.
405+
406+
This is how you would add mappings for your favorite custom map.
407+
408+
:param datapack: A :class:`BaseData` object to use for mapping unit
409+
types and ability codes to their corresponding classes.
410+
411+
:param filterfunc: A function that accepts a partially loaded
412+
:class:`Replay` object as an argument and returns true if the
413+
datapack should be used on this replay.
414+
"""
415+
self.registered_datapacks.insert(0,(filterfunc, datapack))
416+
417+
418+
# Override points
419+
def register_default_readers(self):
420+
"""Registers factory default readers."""
421+
self.register_reader('replay.details', readers.DetailsReader_Base())
422+
self.register_reader('replay.initData', readers.InitDataReader_Base())
423+
self.register_reader('replay.message.events', readers.MessageEventsReader_Base())
424+
self.register_reader('replay.attributes.events', readers.AttributesEventsReader_Base(), lambda r: r.build < 17326)
425+
self.register_reader('replay.attributes.events', readers.AttributesEventsReader_17326(), lambda r: r.build >= 17326)
426+
self.register_reader('replay.game.events', readers.GameEventsReader_Base(), lambda r: r.build < 16561)
427+
self.register_reader('replay.game.events', readers.GameEventsReader_16561(), lambda r: 16561 <= r.build < 18574)
428+
self.register_reader('replay.game.events', readers.GameEventsReader_18574(), lambda r: 18574 <= r.build < 19595)
429+
self.register_reader('replay.game.events', readers.GameEventsReader_19595(), lambda r: 19595 <= r.build)
430+
431+
def register_default_datapacks(self):
432+
"""Registers factory default datapacks."""
433+
self.register_datapack(data.Data_16561, lambda r: 16561 <= r.build < 17326)
434+
self.register_datapack(data.Data_17326, lambda r: 17326 <= r.build < 18317)
435+
self.register_datapack(data.Data_18317, lambda r: 18317 <= r.build < 19595)
436+
self.register_datapack(data.Data_19595, lambda r: 19595 <= r.build)
437+
438+
439+
# Internal Methods
440+
def _get_reader(self, data_file):
441+
for callback, reader in self.registered_readers[data_file]:
442+
if callback(self):
443+
return reader
444+
445+
def _get_datapack(self):
446+
for callback, datapack in self.registered_datapacks:
447+
if callback(self):
448+
return datapack
449+
return None
450+
451+
def _read_data(self, data_file, reader):
452+
data = utils.extract_data_file(data_file,self.archive)
453+
if data:
454+
data_buffer = utils.ReplayBuffer(data)
455+
self.raw_data[data_file] = reader(data_buffer, self)
456+
elif self.opt.debug and data_file != 'replay.message.events':
457+
raise ValueError("{0} not found in archive".format(data_file))
458+
else:
459+
self.logger.error("{0} not found in archive".format(data_file))
368460

369461
class Map(Resource):
370462
url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma'

0 commit comments

Comments
 (0)