Skip to content

Commit 48df941

Browse files
committed
Fallback to backup files for detail loading.
Anonymized replays are missing the main replay.initData and replay.details files, this will fallback to the backup versions. Add a new print statement case to the GameEvent base to cover when just the player.name is missing.
1 parent 3aaabe1 commit 48df941

File tree

4 files changed

+87
-49
lines changed

4 files changed

+87
-49
lines changed

sc2reader/events/game.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ def __init__(self, frame, pid):
3838

3939
def _str_prefix(self):
4040
if self.player:
41-
player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global"
41+
if not self.player.name:
42+
player_name = "Player {0} - ({1})".format(self.player.pid, self.player.play_race)
43+
else:
44+
player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global"
4245
else:
4346
player_name = "no name"
4447
return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name)

sc2reader/resources.py

Lines changed: 78 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,16 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en
270270
# Load basic details if requested
271271
if load_level >= 1:
272272
self.load_level = 1
273-
for data_file in ['replay.initData', 'replay.details', 'replay.attributes.events']:
273+
files = [
274+
'replay.initData.backup',
275+
'replay.details.backup',
276+
'replay.attributes.events',
277+
'replay.initData',
278+
'replay.details'
279+
]
280+
for data_file in files:
274281
self._read_data(data_file, self._get_reader(data_file))
275-
self.load_details()
282+
self.load_all_details()
276283
self.datapack = self._get_datapack()
277284

278285
# Can only be effective if map data has been loaded
@@ -311,18 +318,24 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en
311318

312319
engine.run(self)
313320

314-
def load_details(self):
321+
def load_init_data(self):
315322
if 'replay.initData' in self.raw_data:
316323
initData = self.raw_data['replay.initData']
317-
options = initData['game_description']['game_options']
318-
self.amm = options['amm']
319-
self.ranked = options['ranked']
320-
self.competitive = options['competitive']
321-
self.practice = options['practice']
322-
self.cooperative = options['cooperative']
323-
self.battle_net = options['battle_net']
324-
self.hero_duplicates_allowed = options['hero_duplicates_allowed']
324+
elif 'replay.initData.backup' in self.raw_data:
325+
initData = self.raw_data['replay.initData.backup']
326+
else:
327+
return
325328

329+
options = initData['game_description']['game_options']
330+
self.amm = options['amm']
331+
self.ranked = options['ranked']
332+
self.competitive = options['competitive']
333+
self.practice = options['practice']
334+
self.cooperative = options['cooperative']
335+
self.battle_net = options['battle_net']
336+
self.hero_duplicates_allowed = options['hero_duplicates_allowed']
337+
338+
def load_attribute_events(self):
326339
if 'replay.attributes.events' in self.raw_data:
327340
# Organize the attribute data to be useful
328341
self.attributes = defaultdict(dict)
@@ -337,57 +350,74 @@ def load_details(self):
337350
self.is_ladder = (self.category == "Ladder")
338351
self.is_private = (self.category == "Private")
339352

353+
def load_details(self):
340354
if 'replay.details' in self.raw_data:
341355
details = self.raw_data['replay.details']
356+
elif 'replay.details.backup' in self.raw_data:
357+
details = self.raw_data['replay.details.backup']
358+
else:
359+
return
360+
361+
self.map_name = details['map_name']
362+
self.region = details['cache_handles'][0].server.lower()
363+
self.map_hash = details['cache_handles'][-1].hash
364+
self.map_file = details['cache_handles'][-1]
365+
366+
# Expand this special case mapping
367+
if self.region == 'sg':
368+
self.region = 'sea'
342369

343-
self.map_name = details['map_name']
370+
dependency_hashes = [d.hash for d in details['cache_handles']]
371+
if hashlib.sha256('Standard Data: Void.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
372+
self.expansion = 'LotV'
373+
elif hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
374+
self.expansion = 'HotS'
375+
elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
376+
self.expansion = 'WoL'
377+
else:
378+
self.expansion = ''
344379

345-
self.region = details['cache_handles'][0].server.lower()
346-
self.map_hash = details['cache_handles'][-1].hash
347-
self.map_file = details['cache_handles'][-1]
380+
self.windows_timestamp = details['file_time']
381+
self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp)
382+
self.end_time = datetime.utcfromtimestamp(self.unix_timestamp)
348383

349-
# Expand this special case mapping
350-
if self.region == 'sg':
351-
self.region = 'sea'
384+
# The utc_adjustment is either the adjusted windows timestamp OR
385+
# the value required to get the adjusted timestamp. We know the upper
386+
# limit for any adjustment number so use that to distinguish between
387+
# the two cases.
388+
if details['utc_adjustment'] < 10**7*60*60*24:
389+
self.time_zone = details['utc_adjustment']/(10**7*60*60)
390+
else:
391+
self.time_zone = (details['utc_adjustment']-details['file_time'])/(10**7*60*60)
352392

353-
dependency_hashes = [d.hash for d in details['cache_handles']]
354-
if hashlib.sha256('Standard Data: Void.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
355-
self.expansion = 'LotV'
356-
elif hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
357-
self.expansion = 'HotS'
358-
elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes:
359-
self.expansion = 'WoL'
360-
else:
361-
self.expansion = ''
362-
363-
self.windows_timestamp = details['file_time']
364-
self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp)
365-
self.end_time = datetime.utcfromtimestamp(self.unix_timestamp)
366-
367-
# The utc_adjustment is either the adjusted windows timestamp OR
368-
# the value required to get the adjusted timestamp. We know the upper
369-
# limit for any adjustment number so use that to distinguish between
370-
# the two cases.
371-
if details['utc_adjustment'] < 10**7*60*60*24:
372-
self.time_zone = details['utc_adjustment']/(10**7*60*60)
373-
else:
374-
self.time_zone = (details['utc_adjustment']-details['file_time'])/(10**7*60*60)
393+
self.game_length = self.length
394+
self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed]))
395+
self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds)
396+
self.date = self.end_time # backwards compatibility
375397

376-
self.game_length = self.length
377-
self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed]))
378-
self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds)
379-
self.date = self.end_time # backwards compatibility
398+
def load_all_details(self):
399+
self.load_init_data()
400+
self.load_attribute_events()
401+
self.load_details()
380402

381403
def load_map(self):
382404
self.map = self.factory.load_map(self.map_file, **self.opt)
383405

384406
def load_players(self):
385407
# If we don't at least have details and attributes_events we can go no further
386-
if 'replay.details' not in self.raw_data:
408+
if 'replay.details' in self.raw_data:
409+
details = self.raw_data['replay.details']
410+
elif 'replay.details.backup' in self.raw_data:
411+
details = self.raw_data['replay.details.backup']
412+
else:
387413
return
388414
if 'replay.attributes.events' not in self.raw_data:
389415
return
390-
if 'replay.initData' not in self.raw_data:
416+
if 'replay.initData' in self.raw_data:
417+
initData = self.raw_data['replay.initData']
418+
elif 'replay.initData.backup' in self.raw_data:
419+
initData = self.raw_data['replay.initData.backup']
420+
else:
391421
return
392422

393423
self.clients = list()
@@ -397,8 +427,6 @@ def load_players(self):
397427
# information. detail_id marks the current index into this data.
398428
detail_id = 0
399429
player_id = 1
400-
details = self.raw_data['replay.details']
401-
initData = self.raw_data['replay.initData']
402430

403431
# Assume that the first X map slots starting at 1 are player slots
404432
# so that we can assign player ids without the map
@@ -568,6 +596,8 @@ def register_default_readers(self):
568596
"""Registers factory default readers."""
569597
self.register_reader('replay.details', readers.DetailsReader(), lambda r: True)
570598
self.register_reader('replay.initData', readers.InitDataReader(), lambda r: True)
599+
self.register_reader('replay.details.backup', readers.DetailsReader(), lambda r: True)
600+
self.register_reader('replay.initData.backup', readers.InitDataReader(), lambda r: True)
571601
self.register_reader('replay.tracker.events', readers.TrackerEventsReader(), lambda r: True)
572602
self.register_reader('replay.message.events', readers.MessageEventsReader(), lambda r: True)
573603
self.register_reader('replay.attributes.events', readers.AttributesEventsReader(), lambda r: True)

test_replays/4.1.2.60604/1.SC2Replay

104 KB
Binary file not shown.

test_replays/test_all.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,11 @@ def test_70154(self):
603603
factory = sc2reader.factories.SC2Factory()
604604
replay = factory.load_replay(replayfilename)
605605

606+
def test_anonymous_replay(self):
607+
replayfilename = "test_replays/4.1.2.60604/1.SC2Replay"
608+
factory = sc2reader.factories.SC2Factory()
609+
replay = factory.load_replay(replayfilename)
610+
606611

607612
class TestGameEngine(unittest.TestCase):
608613
class TestEvent(object):

0 commit comments

Comments
 (0)