Skip to content

Commit e58e8e5

Browse files
authored
Merge pull request #69 from Gusgus01/AnonymizedReplayWorkaround
Fallback to backup files for detail loading.
2 parents 3aaabe1 + 3aa603c commit e58e8e5

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)