Skip to content

Commit aa26614

Browse files
committed
Add built-in summary, match info, and match history support.
1 parent a5108a6 commit aa26614

File tree

4 files changed

+153
-31
lines changed

4 files changed

+153
-31
lines changed

sc2reader/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,13 @@
2626
load_replay = __defaultSC2Reader.load_replay
2727
load_maps = __defaultSC2Reader.load_maps
2828
load_map = __defaultSC2Reader.load_map
29-
load_summaries = __defaultSC2Reader.load_summaries
30-
load_summary = __defaultSC2Reader.load_summary
29+
load_game_summaries = __defaultSC2Reader.load_game_summaries
30+
load_game_summary = __defaultSC2Reader.load_game_summary
31+
load_match_infos = __defaultSC2Reader.load_match_infos
32+
load_match_info = __defaultSC2Reader.load_match_info
33+
load_match_histories = __defaultSC2Reader.load_match_histories
34+
load_match_history = __defaultSC2Reader.load_match_history
35+
3136

3237
configure = __defaultSC2Reader.configure
3338
reset = __defaultSC2Reader.reset

sc2reader/factories.py

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from sc2reader import exceptions
1515
from sc2reader import utils
1616
from sc2reader import log_utils
17-
from sc2reader.resources import Replay, Map, Summary
17+
from sc2reader.resources import Replay, Map, GameSummary, MatchInfo, MatchHistory
1818

1919
class SC2Factory(object):
2020
"""
@@ -133,8 +133,12 @@ def load_resources(self, resources, resource_loader, options=None, **new_options
133133
if re.match(r'https?://',resources):
134134
yield resource_loader(resources, options=options)
135135
else:
136-
for resource in utils.get_replay_files(resources, **options):
137-
yield resource_loader(resource, options=options)
136+
for resource in utils.get_files(resources, **options):
137+
try:
138+
yield resource_loader(resource, options=options)
139+
except Exception as e:
140+
print "\n\n\nFAILURE!!!\n\n\n"
141+
yield None
138142

139143
# File like object?
140144
elif hasattr(resources,'read'):
@@ -175,17 +179,17 @@ def load_resource(self, resource, options=None, **new_options):
175179

176180
return (resource, resource_name)
177181

178-
def load_summaries(self, gs, options=None, **new_options):
182+
def load_game_summaries(self, gs, options=None, **new_options):
179183
"""
180-
Loads a collection of replays. See load_resources for detailed parameter
184+
Loads a collection of game summaries. See load_resources for detailed parameter
181185
documentation.
182186
183-
:rtype: generator(:class:`Map`)
187+
:rtype: generator(:class:`GameSummary`)
184188
"""
185-
for s in self.load_resources(gs, self.load_summary, options=options, **new_options):
189+
for s in self.load_resources(gs, self.load_game_summary, options=options, extensions=['.s2gs'], **new_options):
186190
yield s
187191

188-
def load_summary(self, summary_file, options=None, **new_options):
192+
def load_game_summary(self, summary_file, options=None, **new_options):
189193
"""
190194
Loads the specified summary using the current factory settings with the
191195
specified overrides.
@@ -199,16 +203,87 @@ def load_summary(self, summary_file, options=None, **new_options):
199203
:param new_options: Options values to override current factory settings
200204
while loading this map.
201205
202-
:rtype: :class:`Replay`
206+
:rtype: :class:`GameSummary`
203207
"""
204208
options = options or utils.merged_dict(self.options, new_options)
205209
resource, name = self.load_resource(summary_file, options=options)
206-
s = Summary(resource, name, **options)
210+
s2gs = GameSummary(resource, name, **options)
211+
212+
# Load summary procedure here!
213+
#
214+
215+
return s2gs
216+
217+
def load_match_infos(self, infos, options=None, **new_options):
218+
"""
219+
Loads a collection of MatchInfos. See load_resources for detailed
220+
parameter documentation.
221+
222+
:rtype: generator(:class:`MatchInfo`)
223+
"""
224+
for s2mi in self.load_resources(infos, self.load_match_info, options=options, extensions=['.s2mi'], **new_options):
225+
yield s2mi
226+
227+
def load_match_info(self, info_file, options=None, **new_options):
228+
"""
229+
Loads the specified match info using the current factory settings with
230+
the specified overrides.
231+
232+
:param info_file: An open file object or path/url to a single file
233+
234+
:param None options: When options are passed directly into the options
235+
parameter the current factory settings are ignored and only the
236+
specified options are used during replay load.
237+
238+
:param new_options: Options values to override current factory settings
239+
while loading this map.
240+
241+
:rtype: :class:`MatchInfo`
242+
"""
243+
options = options or utils.merged_dict(self.options, new_options)
244+
resource, name = self.load_resource(info_file, options=options)
245+
s2mi = MatchInfo(resource, name, **options)
246+
247+
# Load summary procedure here!
248+
#
249+
250+
return s2mi
251+
252+
def load_match_histories(self, histories, options=None, **new_options):
253+
"""
254+
Loads a collection of match history files. See load_resources for
255+
detailed parameter documentation.
256+
257+
:rtype: generator(:class:`MatchHistory`)
258+
"""
259+
for s2mh in self.load_resources(histories, self.load_match_history, options=options, extensions=['.s2mh'], **new_options):
260+
yield s2mh
261+
262+
def load_match_history(self, history_file, options=None, **new_options):
263+
"""
264+
Loads the specified match info using the current factory settings with
265+
the specified overrides.
266+
267+
:param history_file: An open file object or path/url to a single file
268+
269+
:param None options: When options are passed directly into the options
270+
parameter the current factory settings are ignored and only the
271+
specified options are used during replay load.
272+
273+
:param new_options: Options values to override current factory settings
274+
while loading this map.
275+
276+
:rtype: :class:`MatchHistory`
277+
"""
278+
options = options or utils.merged_dict(self.options, new_options)
279+
resource, name = self.load_resource(history_file, options=options)
280+
print name
281+
s2mh = MatchHistory(resource, name, **options)
207282

208283
# Load summary procedure here!
209284
#
210285

211-
return s
286+
return s2mh
212287

213288
def load_maps(self, maps, options=None, **new_options):
214289
"""
@@ -217,7 +292,7 @@ def load_maps(self, maps, options=None, **new_options):
217292
218293
:rtype: generator(:class:`Map`)
219294
"""
220-
for m in self.load_resources(maps, self.load_map, options=options, **new_options):
295+
for m in self.load_resources(maps, self.load_map, options=options, extensions=['.s2ma'], **new_options):
221296
yield m
222297

223298
def load_map(self, map_file, options=None, **new_options):
@@ -253,7 +328,7 @@ def load_replays(self, replays, options=None, **new_options):
253328
254329
:rtype: generator(:class:`Replay`)
255330
"""
256-
for r in self.load_resources(replays, self.load_replay, options=options, **new_options):
331+
for r in self.load_resources(replays, self.load_replay, options=options, extensions=['.sc2replay'], **new_options):
257332
yield r
258333

259334
def load_replay(self, replay_file, options=None, **new_options):

sc2reader/resources.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -396,11 +396,10 @@ def read_game_strings(self):
396396
elif parts[0] == 'DocInfo/DescLong':
397397
self.description = parts[1]
398398

399-
class Summary(Resource):
399+
class GameSummary(Resource):
400400
url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma'
401-
402401
def __init__(self, summary_file, filename=None, **options):
403-
super(Summary, self).__init__(summary_file, filename,**options)
402+
super(GameSummary, self).__init__(summary_file, filename,**options)
404403
self.data = zlib.decompress(summary_file.read()[16:])
405404
self.parts = list()
406405
buffer = utils.ReplayBuffer(self.data)
@@ -409,3 +408,16 @@ def __init__(self, summary_file, filename=None, **options):
409408
print str(part)+"\n\n\n"
410409
self.parts.append(buffer.read_data_struct())
411410
print len(self.parts)
411+
412+
class MatchInfo(Resource):
413+
url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma'
414+
def __init__(self, info_file, filename=None, **options):
415+
super(MatchInfo, self).__init__(info_file, filename,**options)
416+
self.data = utils.ReplayBuffer(info_file).read_data_struct()
417+
418+
419+
class MatchHistory(Resource):
420+
url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma'
421+
def __init__(self, history_file, filename=None, **options):
422+
super(MatchHistory, self).__init__(history_file, filename,**options)
423+
self.data = utils.ReplayBuffer(history_file).read_data_struct()

sc2reader/utils.py

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -261,19 +261,36 @@ def read_timestamp(self):
261261
else:
262262
raise ValueError()
263263

264-
def read_data_struct(self):
264+
def read_data_struct(self, indent=0, key=None):
265265
"""
266266
Read a Blizzard data-structure. Structure can contain strings, lists,
267267
dictionaries and custom integer types.
268268
"""
269269
#The first byte serves as a flag for the type of data to follow
270270
datatype = self.read_byte()
271+
prefix = hex(self.tell())+"\t"*indent
272+
if key != None:
273+
prefix+="{0}: {1}".format(key, datatype)
274+
275+
#print prefix
271276

272277
if datatype == 0x00:
273278
#0x00 is an array where the first X bytes mark the number of entries in
274279
#the array. See variable int documentation for details.
275280
entries = self.read_variable_int()
276-
return [self.read_data_struct() for i in range(entries)]
281+
#print "Entries, ",entries
282+
#print self.peek(10)
283+
return [self.read_data_struct(indent+1,i) for i in range(entries)]
284+
285+
if datatype == 0x01:# or datatype == 0x0A:
286+
#0x01 is an array where the first X bytes mark the number of entries in
287+
#the array. See variable int documentation for details.
288+
#print self.peek(10)
289+
self.read_chars(2).encode("hex")
290+
#print self.peek(10)
291+
entries = self.read_variable_int()
292+
#print "Entries, ",entries
293+
return [self.read_data_struct(indent+1,i) for i in range(entries)]
277294

278295
if datatype == 0x02:
279296
#0x02 is a byte string with the first byte indicating
@@ -284,14 +301,17 @@ def read_data_struct(self):
284301
#0x03 is an unknown data type where the first byte appears
285302
#to have no effect and kicks back the next instruction
286303
flag = self.read_byte()
287-
return self.read_data_struct()
304+
return self.read_data_struct(indent,key)
288305

289306
elif datatype == 0x04:
290307
#0x04 is an unknown data type where the first byte of information
291308
#is a switch (1 or 0) that can trigger another structure to be
292309
#read.
293-
if self.read_byte():
294-
return self.read_data_struct()
310+
flag = self.read_byte()
311+
#if flag == 0x04:
312+
# flag = self.read_byte()
313+
if flag:
314+
return self.read_data_struct(indent,key)
295315
else:
296316
return 0
297317

@@ -301,18 +321,25 @@ def read_data_struct(self):
301321
#When looping through the pairs, the first byte is the key,
302322
#followed by the serialized data object value
303323
data = dict()
304-
for i in range(self.read_count()):
324+
#print self.peek(10)
325+
entries = self.read_count()
326+
#print "Key Entries, ",entries
327+
for i in range(entries):
305328
key = self.read_count()
306-
data[key] = self.read_data_struct() #Done like this to keep correct parse order
329+
data[key] = self.read_data_struct(indent+1,key) #Done like this to keep correct parse order
307330
return data
308331

309332
elif datatype == 0x06:
310333
return self.read_byte()
311334
elif datatype == 0x07:
312335
return self.read_chars(4)
313336
elif datatype == 0x09:
337+
#print self.peek(10)
314338
return self.read_variable_int()
315-
339+
"""
340+
elif datatype == 0x0A:
341+
return self.read_byte()
342+
"""
316343
raise TypeError("Unknown Data Structure: '%s'" % datatype)
317344

318345
def read_object_type(self, read_modifier=False):
@@ -652,18 +679,21 @@ def merged_dict(a, b):
652679
c.update(b)
653680
return c
654681

655-
def sc2replay_ext(filename):
682+
def extension_filter(filename, extensions):
656683
name, ext = os.path.splitext(filename)
657-
return ext.lower() == ".sc2replay"
684+
return ext.lower() in extensions
658685

659-
def get_replay_files(path, exclude=[], depth=-1, followlinks=False, **extras):
686+
import functools
687+
def get_files(path, extensions=['.sc2replay'], exclude=[], depth=-1, followlinks=False, **extras):
660688
#os.walk and os.path.isfile fail silently. We want to be loud!
661689
if not os.path.exists(path):
662690
raise ValueError("Location `{0}` does not exist".format(path))
663691

692+
filtr_func = functools.partial(extension_filter, extensions=extensions)
693+
664694
# os.walk can't handle file paths, only directories
665695
if os.path.isfile(path):
666-
return [path] if sc2replay_ext(path) else []
696+
return [path] if filtr_func(path,extensions) else []
667697

668698
files = list()
669699
for root, directories, filenames in os.walk(path, followlinks=followlinks):
@@ -673,7 +703,7 @@ def get_replay_files(path, exclude=[], depth=-1, followlinks=False, **extras):
673703
directories.remove(directory)
674704

675705
# Extend our return value only with the allowed file type and regex
676-
allowed_files = filter(sc2replay_ext, filenames)
706+
allowed_files = filter(filtr_func, filenames)
677707
files.extend(os.path.join(root, filename) for filename in allowed_files)
678708
depth -= 1
679709

0 commit comments

Comments
 (0)