Skip to content

Commit 3aa603c

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 3aa603c

File tree

4 files changed

+128
-50
lines changed

4 files changed

+128
-50
lines changed

sc2reader/events/game.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,12 @@ def __init__(self, frame, pid):
3737
self.name = self.__class__.__name__
3838

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

sc2reader/resources.py

Lines changed: 80 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,19 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en
268268
self.length = self.game_length = self.real_length = utils.Length(seconds=int(self.frames/fps))
269269

270270
# Load basic details if requested
271+
# .backup files are read in case the main files are missing or removed
271272
if load_level >= 1:
272273
self.load_level = 1
273-
for data_file in ['replay.initData', 'replay.details', 'replay.attributes.events']:
274+
files = [
275+
'replay.initData.backup',
276+
'replay.details.backup',
277+
'replay.attributes.events',
278+
'replay.initData',
279+
'replay.details'
280+
]
281+
for data_file in files:
274282
self._read_data(data_file, self._get_reader(data_file))
275-
self.load_details()
283+
self.load_all_details()
276284
self.datapack = self._get_datapack()
277285

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

312320
engine.run(self)
313321

314-
def load_details(self):
322+
def load_init_data(self):
315323
if 'replay.initData' in self.raw_data:
316324
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']
325+
elif 'replay.initData.backup' in self.raw_data:
326+
initData = self.raw_data['replay.initData.backup']
327+
else:
328+
return
325329

330+
options = initData['game_description']['game_options']
331+
self.amm = options['amm']
332+
self.ranked = options['ranked']
333+
self.competitive = options['competitive']
334+
self.practice = options['practice']
335+
self.cooperative = options['cooperative']
336+
self.battle_net = options['battle_net']
337+
self.hero_duplicates_allowed = options['hero_duplicates_allowed']
338+
339+
def load_attribute_events(self):
326340
if 'replay.attributes.events' in self.raw_data:
327341
# Organize the attribute data to be useful
328342
self.attributes = defaultdict(dict)
@@ -337,57 +351,75 @@ def load_details(self):
337351
self.is_ladder = (self.category == "Ladder")
338352
self.is_private = (self.category == "Private")
339353

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

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

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]
381+
self.windows_timestamp = details['file_time']
382+
self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp)
383+
self.end_time = datetime.utcfromtimestamp(self.unix_timestamp)
348384

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

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)
394+
self.game_length = self.length
395+
self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed]))
396+
self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds)
397+
self.date = self.end_time # backwards compatibility
375398

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
399+
def load_all_details(self):
400+
self.load_init_data()
401+
self.load_attribute_events()
402+
self.load_details()
380403

381404
def load_map(self):
382405
self.map = self.factory.load_map(self.map_file, **self.opt)
383406

384407
def load_players(self):
385408
# 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:
409+
# We can use the backup detail files if the main files have been removed
410+
if 'replay.details' in self.raw_data:
411+
details = self.raw_data['replay.details']
412+
elif 'replay.details.backup' in self.raw_data:
413+
details = self.raw_data['replay.details.backup']
414+
else:
387415
return
388416
if 'replay.attributes.events' not in self.raw_data:
389417
return
390-
if 'replay.initData' not in self.raw_data:
418+
if 'replay.initData' in self.raw_data:
419+
initData = self.raw_data['replay.initData']
420+
elif 'replay.initData.backup' in self.raw_data:
421+
initData = self.raw_data['replay.initData.backup']
422+
else:
391423
return
392424

393425
self.clients = list()
@@ -397,8 +429,6 @@ def load_players(self):
397429
# information. detail_id marks the current index into this data.
398430
detail_id = 0
399431
player_id = 1
400-
details = self.raw_data['replay.details']
401-
initData = self.raw_data['replay.initData']
402432

403433
# Assume that the first X map slots starting at 1 are player slots
404434
# so that we can assign player ids without the map
@@ -568,6 +598,8 @@ def register_default_readers(self):
568598
"""Registers factory default readers."""
569599
self.register_reader('replay.details', readers.DetailsReader(), lambda r: True)
570600
self.register_reader('replay.initData', readers.InitDataReader(), lambda r: True)
601+
self.register_reader('replay.details.backup', readers.DetailsReader(), lambda r: True)
602+
self.register_reader('replay.initData.backup', readers.InitDataReader(), lambda r: True)
571603
self.register_reader('replay.tracker.events', readers.TrackerEventsReader(), lambda r: True)
572604
self.register_reader('replay.message.events', readers.MessageEventsReader(), lambda r: True)
573605
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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import sc2reader
2121
from sc2reader.exceptions import CorruptTrackerFileError
22+
from sc2reader.events.game import GameEvent
23+
from sc2reader.objects import Player
2224

2325
sc2reader.log_utils.log_to_console("INFO")
2426

@@ -603,6 +605,41 @@ def test_70154(self):
603605
factory = sc2reader.factories.SC2Factory()
604606
replay = factory.load_replay(replayfilename)
605607

608+
def test_anonymous_replay(self):
609+
replayfilename = "test_replays/4.1.2.60604/1.SC2Replay"
610+
factory = sc2reader.factories.SC2Factory()
611+
replay = factory.load_replay(replayfilename)
612+
613+
def test_game_event_string(self):
614+
time = "00.01"
615+
# Global
616+
player = MockPlayer()
617+
player.name = "TestPlayer"
618+
player.play_race = "TestRace"
619+
event = GameEvent(16, 16)
620+
event.player = player
621+
self.assertEqual("{0}\t{1:<15} ".format(time, "Global"), event._str_prefix())
622+
623+
# Player with name
624+
player = MockPlayer()
625+
player.name = "TestPlayer"
626+
player.play_race = "TestRace"
627+
event = GameEvent(16, 1)
628+
event.player = player
629+
self.assertEqual("{0}\t{1:<15} ".format(time, player.name), event._str_prefix())
630+
631+
# No Player
632+
player = MockPlayer()
633+
event = GameEvent(16, 1)
634+
self.assertEqual("{0}\t{1:<15} ".format(time, "no name"), event._str_prefix())
635+
636+
# Player without name
637+
player = MockPlayer()
638+
player.play_race = "TestRace"
639+
player.pid = 1
640+
event = GameEvent(16, 1)
641+
event.player = player
642+
self.assertEqual("{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), event._str_prefix())
606643

607644
class TestGameEngine(unittest.TestCase):
608645
class TestEvent(object):
@@ -658,6 +695,11 @@ def test_plugin1(self):
658695
self.assertEqual(replay.plugin_result['TestPlugin1'], (1, dict(msg="Fail!")))
659696
self.assertEqual(replay.plugin_result['TestPlugin2'], (0, dict()))
660697

698+
class MockPlayer(object):
699+
def __init__(self):
700+
self.name = None
701+
self.play_race = None
702+
self.pid = None
661703

662704
if __name__ == '__main__':
663705
unittest.main()

0 commit comments

Comments
 (0)