From 3aa603c16d5288dae18d0611818de411a411a79d Mon Sep 17 00:00:00 2001 From: Gus Foley Date: Sat, 24 Nov 2018 03:34:32 -0800 Subject: [PATCH 001/136] 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. --- sc2reader/events/game.py | 8 +- sc2reader/resources.py | 128 +++++++++++++++++---------- test_replays/4.1.2.60604/1.SC2Replay | Bin 0 -> 106109 bytes test_replays/test_all.py | 42 +++++++++ 4 files changed, 128 insertions(+), 50 deletions(-) create mode 100644 test_replays/4.1.2.60604/1.SC2Replay diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index bac26d94..7eadebfb 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -37,8 +37,12 @@ def __init__(self, frame, pid): self.name = self.__class__.__name__ def _str_prefix(self): - if self.player: - player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" + if getattr(self, 'pid', 16) == 16: + player_name = "Global" + elif self.player and not self.player.name: + player_name = "Player {0} - ({1})".format(self.player.pid, self.player.play_race) + elif self.player: + player_name = self.player.name else: player_name = "no name" return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 8d165670..32ac05bc 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -268,11 +268,19 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.length = self.game_length = self.real_length = utils.Length(seconds=int(self.frames/fps)) # Load basic details if requested + # .backup files are read in case the main files are missing or removed if load_level >= 1: self.load_level = 1 - for data_file in ['replay.initData', 'replay.details', 'replay.attributes.events']: + files = [ + 'replay.initData.backup', + 'replay.details.backup', + 'replay.attributes.events', + 'replay.initData', + 'replay.details' + ] + for data_file in files: self._read_data(data_file, self._get_reader(data_file)) - self.load_details() + self.load_all_details() self.datapack = self._get_datapack() # 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 engine.run(self) - def load_details(self): + def load_init_data(self): if 'replay.initData' in self.raw_data: initData = self.raw_data['replay.initData'] - options = initData['game_description']['game_options'] - self.amm = options['amm'] - self.ranked = options['ranked'] - self.competitive = options['competitive'] - self.practice = options['practice'] - self.cooperative = options['cooperative'] - self.battle_net = options['battle_net'] - self.hero_duplicates_allowed = options['hero_duplicates_allowed'] + elif 'replay.initData.backup' in self.raw_data: + initData = self.raw_data['replay.initData.backup'] + else: + return + options = initData['game_description']['game_options'] + self.amm = options['amm'] + self.ranked = options['ranked'] + self.competitive = options['competitive'] + self.practice = options['practice'] + self.cooperative = options['cooperative'] + self.battle_net = options['battle_net'] + self.hero_duplicates_allowed = options['hero_duplicates_allowed'] + + def load_attribute_events(self): if 'replay.attributes.events' in self.raw_data: # Organize the attribute data to be useful self.attributes = defaultdict(dict) @@ -337,57 +351,75 @@ def load_details(self): self.is_ladder = (self.category == "Ladder") self.is_private = (self.category == "Private") + def load_details(self): if 'replay.details' in self.raw_data: details = self.raw_data['replay.details'] + elif 'replay.details.backup' in self.raw_data: + details = self.raw_data['replay.details.backup'] + else: + return + + self.map_name = details['map_name'] + self.region = details['cache_handles'][0].server.lower() + self.map_hash = details['cache_handles'][-1].hash + self.map_file = details['cache_handles'][-1] + + # Expand this special case mapping + if self.region == 'sg': + self.region = 'sea' - self.map_name = details['map_name'] + dependency_hashes = [d.hash for d in details['cache_handles']] + if hashlib.sha256('Standard Data: Void.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: + self.expansion = 'LotV' + elif hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: + self.expansion = 'HotS' + elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: + self.expansion = 'WoL' + else: + self.expansion = '' - self.region = details['cache_handles'][0].server.lower() - self.map_hash = details['cache_handles'][-1].hash - self.map_file = details['cache_handles'][-1] + self.windows_timestamp = details['file_time'] + self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp) + self.end_time = datetime.utcfromtimestamp(self.unix_timestamp) - # Expand this special case mapping - if self.region == 'sg': - self.region = 'sea' + # The utc_adjustment is either the adjusted windows timestamp OR + # the value required to get the adjusted timestamp. We know the upper + # limit for any adjustment number so use that to distinguish between + # the two cases. + if details['utc_adjustment'] < 10**7*60*60*24: + self.time_zone = details['utc_adjustment']/(10**7*60*60) + else: + self.time_zone = (details['utc_adjustment']-details['file_time'])/(10**7*60*60) - dependency_hashes = [d.hash for d in details['cache_handles']] - if hashlib.sha256('Standard Data: Void.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'LotV' - elif hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'HotS' - elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'WoL' - else: - self.expansion = '' - - self.windows_timestamp = details['file_time'] - self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp) - self.end_time = datetime.utcfromtimestamp(self.unix_timestamp) - - # The utc_adjustment is either the adjusted windows timestamp OR - # the value required to get the adjusted timestamp. We know the upper - # limit for any adjustment number so use that to distinguish between - # the two cases. - if details['utc_adjustment'] < 10**7*60*60*24: - self.time_zone = details['utc_adjustment']/(10**7*60*60) - else: - self.time_zone = (details['utc_adjustment']-details['file_time'])/(10**7*60*60) + self.game_length = self.length + self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed])) + self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds) + self.date = self.end_time # backwards compatibility - self.game_length = self.length - self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed])) - self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds) - self.date = self.end_time # backwards compatibility + def load_all_details(self): + self.load_init_data() + self.load_attribute_events() + self.load_details() def load_map(self): self.map = self.factory.load_map(self.map_file, **self.opt) def load_players(self): # If we don't at least have details and attributes_events we can go no further - if 'replay.details' not in self.raw_data: + # We can use the backup detail files if the main files have been removed + if 'replay.details' in self.raw_data: + details = self.raw_data['replay.details'] + elif 'replay.details.backup' in self.raw_data: + details = self.raw_data['replay.details.backup'] + else: return if 'replay.attributes.events' not in self.raw_data: return - if 'replay.initData' not in self.raw_data: + if 'replay.initData' in self.raw_data: + initData = self.raw_data['replay.initData'] + elif 'replay.initData.backup' in self.raw_data: + initData = self.raw_data['replay.initData.backup'] + else: return self.clients = list() @@ -397,8 +429,6 @@ def load_players(self): # information. detail_id marks the current index into this data. detail_id = 0 player_id = 1 - details = self.raw_data['replay.details'] - initData = self.raw_data['replay.initData'] # Assume that the first X map slots starting at 1 are player slots # so that we can assign player ids without the map @@ -568,6 +598,8 @@ def register_default_readers(self): """Registers factory default readers.""" self.register_reader('replay.details', readers.DetailsReader(), lambda r: True) self.register_reader('replay.initData', readers.InitDataReader(), lambda r: True) + self.register_reader('replay.details.backup', readers.DetailsReader(), lambda r: True) + self.register_reader('replay.initData.backup', readers.InitDataReader(), lambda r: True) self.register_reader('replay.tracker.events', readers.TrackerEventsReader(), lambda r: True) self.register_reader('replay.message.events', readers.MessageEventsReader(), lambda r: True) self.register_reader('replay.attributes.events', readers.AttributesEventsReader(), lambda r: True) diff --git a/test_replays/4.1.2.60604/1.SC2Replay b/test_replays/4.1.2.60604/1.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..54693ca74f84179460a5f2442af52297d81dd42f GIT binary patch literal 106109 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 Op%9F14{Dd?#s2|XQcYO^ literal 0 HcmV?d00001 diff --git a/test_replays/test_all.py b/test_replays/test_all.py index b779e7cf..4161a0db 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -19,6 +19,8 @@ import sc2reader from sc2reader.exceptions import CorruptTrackerFileError +from sc2reader.events.game import GameEvent +from sc2reader.objects import Player sc2reader.log_utils.log_to_console("INFO") @@ -603,6 +605,41 @@ def test_70154(self): factory = sc2reader.factories.SC2Factory() replay = factory.load_replay(replayfilename) + def test_anonymous_replay(self): + replayfilename = "test_replays/4.1.2.60604/1.SC2Replay" + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) + + def test_game_event_string(self): + time = "00.01" + # Global + player = MockPlayer() + player.name = "TestPlayer" + player.play_race = "TestRace" + event = GameEvent(16, 16) + event.player = player + self.assertEqual("{0}\t{1:<15} ".format(time, "Global"), event._str_prefix()) + + # Player with name + player = MockPlayer() + player.name = "TestPlayer" + player.play_race = "TestRace" + event = GameEvent(16, 1) + event.player = player + self.assertEqual("{0}\t{1:<15} ".format(time, player.name), event._str_prefix()) + + # No Player + player = MockPlayer() + event = GameEvent(16, 1) + self.assertEqual("{0}\t{1:<15} ".format(time, "no name"), event._str_prefix()) + + # Player without name + player = MockPlayer() + player.play_race = "TestRace" + player.pid = 1 + event = GameEvent(16, 1) + event.player = player + self.assertEqual("{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), event._str_prefix()) class TestGameEngine(unittest.TestCase): class TestEvent(object): @@ -658,6 +695,11 @@ def test_plugin1(self): self.assertEqual(replay.plugin_result['TestPlugin1'], (1, dict(msg="Fail!"))) self.assertEqual(replay.plugin_result['TestPlugin2'], (0, dict())) +class MockPlayer(object): + def __init__(self): + self.name = None + self.play_race = None + self.pid = None if __name__ == '__main__': unittest.main() From 2d2f281b206c816ea215e567f461a775a5b6b223 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Thu, 29 Nov 2018 17:55:29 -0800 Subject: [PATCH 002/136] mark version 1.3.1 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 52337169..a23a4c3f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ CHANGELOG ============ +1.3.1 - November 29, 2018 +------------------------- +* Parse backup if data is missing #69 + 1.3.0 - November 16, 2018 ------------------------- * Added support for protocol 70154 (StarCraft 4.7.0) diff --git a/setup.py b/setup.py index c06d6fce..c5d1d694 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version='1.3.0', + version='1.3.1', keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read()+"\n\n"+open("CHANGELOG.rst").read(), From ea5946e2665606f3b2357fa9050b926346500956 Mon Sep 17 00:00:00 2001 From: cclauss Date: Thu, 13 Jun 2019 11:18:54 +0200 Subject: [PATCH 003/136] Travis CI: Stop testing on EOL versions of Python Python 2.6 and 3.4 are now End of Life. --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1fc93c94..9ae59763 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: python +dist: xenial python: - - 2.6 - 2.7 - - 3.4 - - 3.6 + - 3.5 + - 3.7 - pypy install: - python setup.py develop From c7ff9cb9d38d5b4a1cf22ff3e3134ea805edbba2 Mon Sep 17 00:00:00 2001 From: cclauss Date: Thu, 13 Jun 2019 11:23:42 +0200 Subject: [PATCH 004/136] Delete .travis.yml --- .travis.yml | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9ae59763..00000000 --- a/.travis.yml +++ /dev/null @@ -1,26 +0,0 @@ -language: python -dist: xenial -python: - - 2.7 - - 3.5 - - 3.7 - - pypy -install: - - python setup.py develop - - pip install coveralls hacking - - mkdir local_cache -before_script: - # stop the build if there are Python syntax errors or undefined names - - time flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - time flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics -script: - - SC2READER_CACHE_DIR=local_cache coverage run --source=sc2reader test_replays/test_all.py - - SC2READER_CACHE_DIR=local_cache coverage run --source=sc2reader --append test_s2gs/test_all.py -after_success: - - coveralls -branches: - only: - - master -notifications: - irc: "chat.freenode.net#sc2reader" From e07b8e23bd5611c31180fb9e33b68587b6ce2c6e Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 14:10:19 +0200 Subject: [PATCH 005/136] auto refactor using: "black -l 127" --- test_replays/test_all.py | 326 +++++++++++++++++++-------------------- 1 file changed, 163 insertions(+), 163 deletions(-) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 4161a0db..c9b7f968 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -7,6 +7,7 @@ # Newer unittest features aren't built in for python 2.6 import sys + if sys.version_info[:2] < (2, 7): import unittest2 as unittest else: @@ -24,8 +25,8 @@ sc2reader.log_utils.log_to_console("INFO") -class TestReplays(unittest.TestCase): +class TestReplays(unittest.TestCase): def test_teams(self): replay = sc2reader.load_replay("test_replays/1.2.2.17811/13.SC2Replay") self.assertNotEqual(replay.player[1].team.number, replay.player[2].team.number) @@ -200,7 +201,7 @@ def test_hots_pids(self): "test_replays/2.0.3.24764/Antiga Shipyard (3).SC2Replay", "test_replays/2.0.0.24247/molten.SC2Replay", "test_replays/2.0.0.23925/Akilon Wastes.SC2Replay", - ]: + ]: replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") @@ -217,8 +218,14 @@ def test_wol_pids(self): def test_hots_hatchfun(self): replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") - player_pids = set([ player.pid for player in replay.players]) - spawner_pids = set([ event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva"]) + player_pids = set([player.pid for player in replay.players]) + spawner_pids = set( + [ + event.player.pid + for event in replay.events + if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" + ] + ) self.assertTrue(spawner_pids.issubset(player_pids)) def test_hots_vs_ai(self): @@ -258,20 +265,20 @@ def test_cn_replays(self): def test_unit_types(self): """ sc2reader#136 regression test """ - replay = sc2reader.load_replay('test_replays/2.0.8.25604/issue136.SC2Replay') - hellion_times = [u.started_at for u in replay.players[0].units if u.name == 'Hellion'] - hellbat_times = [u.started_at for u in replay.players[0].units if u.name == 'BattleHellion'] + replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue136.SC2Replay") + hellion_times = [u.started_at for u in replay.players[0].units if u.name == "Hellion"] + hellbat_times = [u.started_at for u in replay.players[0].units if u.name == "BattleHellion"] self.assertEqual(hellion_times, [5180, 5183]) self.assertEqual(hellbat_times, [6736, 6741, 7215, 7220, 12004, 12038]) @unittest.expectedFailure def test_outmatched_pids(self): - replay = sc2reader.load_replay('test_replays/2.0.8.25604/issue131_arid_wastes.SC2Replay') + replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue131_arid_wastes.SC2Replay") self.assertEqual(replay.players[0].pid, 1) self.assertEqual(replay.players[1].pid, 3) self.assertEqual(replay.players[2].pid, 4) - replay = sc2reader.load_replay('test_replays/2.0.8.25604/issue135.SC2Replay') + replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue135.SC2Replay") self.assertEqual(replay.players[0].pid, 1) self.assertEqual(replay.players[1].pid, 2) self.assertEqual(replay.players[2].pid, 4) @@ -285,8 +292,8 @@ def test_outmatched_pids(self): @unittest.expectedFailure def test_map_info(self): replay = sc2reader.load_replay("test_replays/1.5.3.23260/ggtracker_109233.SC2Replay", load_map=True) - self.assertEqual(replay.map.map_info.tile_set, 'Avernus') - self.assertEqual(replay.map.map_info.fog_type, 'Dark') + self.assertEqual(replay.map.map_info.tile_set, "Avernus") + self.assertEqual(replay.map.map_info.fog_type, "Dark") self.assertEqual(replay.map.map_info.width, 176) self.assertEqual(replay.map.map_info.height, 160) self.assertEqual(replay.map.map_info.camera_top, 134) @@ -301,14 +308,10 @@ def test_engine_plugins(self): replay = sc2reader.load_replay( "test_replays/2.0.5.25092/cn1.SC2Replay", - engine=sc2reader.engine.GameEngine(plugins=[ - ContextLoader(), - APMTracker(), - SelectionTracker(), - ]) + engine=sc2reader.engine.GameEngine(plugins=[ContextLoader(), APMTracker(), SelectionTracker()]), ) - code, details = replay.plugins['ContextLoader'] + code, details = replay.plugins["ContextLoader"] self.assertEqual(code, 0) self.assertEqual(details, dict()) @@ -336,71 +339,77 @@ def test_factory_plugins(self): def test_gameheartnormalizer_plugin(self): from sc2reader.engine.plugins import GameHeartNormalizer + sc2reader.engine.register_plugin(GameHeartNormalizer()) # Not a GameHeart game! replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") - player_pids = set([ player.pid for player in replay.players]) - spawner_pids = set([ event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva"]) + player_pids = set([player.pid for player in replay.players]) + spawner_pids = set( + [ + event.player.pid + for event in replay.events + if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" + ] + ) self.assertTrue(spawner_pids.issubset(player_pids)) replay = sc2reader.load_replay("test_replays/gameheart/gameheart.SC2Replay") self.assertEqual(replay.events[0].frame, 0) self.assertEqual(replay.game_length.seconds, 636) self.assertEqual(len(replay.observers), 5) - self.assertEqual(replay.players[0].name, 'SjoWBBII') - self.assertEqual(replay.players[0].play_race, 'Terran') - self.assertEqual(replay.players[1].name, 'Stardust') - self.assertEqual(replay.players[1].play_race, 'Protoss') + self.assertEqual(replay.players[0].name, "SjoWBBII") + self.assertEqual(replay.players[0].play_race, "Terran") + self.assertEqual(replay.players[1].name, "Stardust") + self.assertEqual(replay.players[1].play_race, "Protoss") self.assertEqual(len(replay.teams), 2) - self.assertEqual(replay.teams[0].players[0].name, 'SjoWBBII') - self.assertEqual(replay.teams[1].players[0].name, 'Stardust') + self.assertEqual(replay.teams[0].players[0].name, "SjoWBBII") + self.assertEqual(replay.teams[1].players[0].name, "Stardust") self.assertEqual(replay.winner, replay.teams[1]) replay = sc2reader.load_replay("test_replays/gameheart/gh_sameteam.SC2Replay") self.assertEqual(replay.events[0].frame, 0) self.assertEqual(replay.game_length.seconds, 424) self.assertEqual(len(replay.observers), 5) - self.assertEqual(replay.players[0].name, 'EGJDRC') - self.assertEqual(replay.players[0].play_race, 'Zerg') - self.assertEqual(replay.players[1].name, 'LiquidTaeJa') - self.assertEqual(replay.players[1].play_race, 'Terran') + self.assertEqual(replay.players[0].name, "EGJDRC") + self.assertEqual(replay.players[0].play_race, "Zerg") + self.assertEqual(replay.players[1].name, "LiquidTaeJa") + self.assertEqual(replay.players[1].play_race, "Terran") self.assertEqual(len(replay.teams), 2) - self.assertEqual(replay.teams[0].players[0].name, 'EGJDRC') - self.assertEqual(replay.teams[1].players[0].name, 'LiquidTaeJa') + self.assertEqual(replay.teams[0].players[0].name, "EGJDRC") + self.assertEqual(replay.teams[1].players[0].name, "LiquidTaeJa") self.assertEqual(replay.winner, replay.teams[0]) def test_replay_event_order(self): replay = sc2reader.load_replay("test_replays/event_order.SC2Replay") def test_creepTracker(self): - from sc2reader.engine.plugins import CreepTracker - - for replayfilename in [ - "test_replays/2.0.8.25605/ggtracker_3621322.SC2Replay", - "test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay", - "test_replays/2.0.8.25605/ggtracker_3663861.SC2Replay", - "test_replays/2.0.8.25605/ggtracker_3695400.SC2Replay", - "test_replays/3.1.2/6494799.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - pluginEngine=sc2reader.engine.GameEngine(plugins=[ - CreepTracker() - ]) - replay =factory.load_replay(replayfilename,engine=pluginEngine,load_map= True,load_level=4) - - for player_id in replay.player: - if replay.player[player_id].play_race == "Zerg": - assert replay.player[player_id].max_creep_spread[1] >0 - assert replay.player[player_id].creep_spread_by_minute[0] >0 -# print("MCS", replay.player[player_id].max_creep_spread) -# print("CSBM", replay.player[player_id].creep_spread_by_minute) + from sc2reader.engine.plugins import CreepTracker - - replay =factory.load_replay("test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay",load_map= True,engine=pluginEngine,load_level=4) - assert replay.player[2].max_creep_spread == (840,24.83) - assert replay.player[2].creep_spread_by_minute[420] == 9.4 - assert replay.player[2].creep_spread_by_minute[780] == 22.42 + for replayfilename in [ + "test_replays/2.0.8.25605/ggtracker_3621322.SC2Replay", + "test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay", + "test_replays/2.0.8.25605/ggtracker_3663861.SC2Replay", + "test_replays/2.0.8.25605/ggtracker_3695400.SC2Replay", + "test_replays/3.1.2/6494799.SC2Replay", + ]: + factory = sc2reader.factories.SC2Factory() + pluginEngine = sc2reader.engine.GameEngine(plugins=[CreepTracker()]) + replay = factory.load_replay(replayfilename, engine=pluginEngine, load_map=True, load_level=4) + + for player_id in replay.player: + if replay.player[player_id].play_race == "Zerg": + assert replay.player[player_id].max_creep_spread[1] > 0 + assert replay.player[player_id].creep_spread_by_minute[0] > 0 + # print("MCS", replay.player[player_id].max_creep_spread) + # print("CSBM", replay.player[player_id].creep_spread_by_minute) + + replay = factory.load_replay( + "test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay", load_map=True, engine=pluginEngine, load_level=4 + ) + assert replay.player[2].max_creep_spread == (840, 24.83) + assert replay.player[2].creep_spread_by_minute[420] == 9.4 + assert replay.player[2].creep_spread_by_minute[780] == 22.42 def test_bad_unit_ids(self): with self.assertRaises(CorruptTrackerFileError): @@ -415,56 +424,47 @@ def test_reloaded(self): replay = sc2reader.load_replay("test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay") def test_214(self): - replay = sc2reader.load_replay("test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4) + replay = sc2reader.load_replay("test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4) def test_lotv1(self): - replay = sc2reader.load_replay("test_replays/lotv/lotv1.SC2Replay") - self.assertEqual(replay.expansion, "LotV") - replay = sc2reader.load_replay("test_replays/lotv/lotv2.SC2Replay") - self.assertEqual(replay.expansion, "LotV") - + replay = sc2reader.load_replay("test_replays/lotv/lotv1.SC2Replay") + self.assertEqual(replay.expansion, "LotV") + replay = sc2reader.load_replay("test_replays/lotv/lotv2.SC2Replay") + self.assertEqual(replay.expansion, "LotV") def test_lotv_creepTracker(self): - from sc2reader.engine.plugins import CreepTracker + from sc2reader.engine.plugins import CreepTracker - for replayfilename in [ - "test_replays/lotv/lotv1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - pluginEngine=sc2reader.engine.GameEngine(plugins=[ - CreepTracker() - ]) - replay =factory.load_replay(replayfilename,engine=pluginEngine,load_map= True) + for replayfilename in ["test_replays/lotv/lotv1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + pluginEngine = sc2reader.engine.GameEngine(plugins=[CreepTracker()]) + replay = factory.load_replay(replayfilename, engine=pluginEngine, load_map=True) - for player_id in replay.player: - if replay.player[player_id].play_race == "Zerg": - assert replay.player[player_id].max_creep_spread != 0 - assert replay.player[player_id].creep_spread_by_minute + for player_id in replay.player: + if replay.player[player_id].play_race == "Zerg": + assert replay.player[player_id].max_creep_spread != 0 + assert replay.player[player_id].creep_spread_by_minute def test_lotv_map(self): - # This test currently fails in decoders.py with 'TypeError: ord() expected a character, but string of length 0 found' - for replayfilename in [ - "test_replays/lotv/lotv1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay =factory.load_replay(replayfilename,load_level=1,load_map= True) + # This test currently fails in decoders.py with 'TypeError: ord() expected a character, but string of length 0 found' + for replayfilename in ["test_replays/lotv/lotv1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename, load_level=1, load_map=True) def test_30(self): - replay = sc2reader.load_replay("test_replays/3.0.0.38215/first.SC2Replay") - replay = sc2reader.load_replay("test_replays/3.0.0.38215/second.SC2Replay") - replay = sc2reader.load_replay("test_replays/3.0.0.38215/third.SC2Replay") + replay = sc2reader.load_replay("test_replays/3.0.0.38215/first.SC2Replay") + replay = sc2reader.load_replay("test_replays/3.0.0.38215/second.SC2Replay") + replay = sc2reader.load_replay("test_replays/3.0.0.38215/third.SC2Replay") def test_31(self): - for i in range(1,5): - print("DOING {}".format(i)) - replay = sc2reader.load_replay("test_replays/3.1.0/{}.SC2Replay".format(i)) + for i in range(1, 5): + print("DOING {}".format(i)) + replay = sc2reader.load_replay("test_replays/3.1.0/{}.SC2Replay".format(i)) def test_30_map(self): - for replayfilename in [ - "test_replays/3.0.0.38215/third.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay =factory.load_replay(replayfilename,load_level=1,load_map= True) + for replayfilename in ["test_replays/3.0.0.38215/third.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename, load_level=1, load_map=True) def test_30_apms(self): from sc2reader.factories.plugins.replay import APMTracker, SelectionTracker, toJSON @@ -473,60 +473,68 @@ def test_30_apms(self): factory.register_plugin("Replay", APMTracker()) replay = factory.load_replay("test_replays/3.0.0.38215/fourth.SC2Replay") for player in replay.players: - if player.name == 'Owl': + if player.name == "Owl": print(player.name, player.avg_apm) self.assertTrue(player.avg_apm > 110) def test_38749(self): replay = sc2reader.load_replay("test_replays/3.0.0.38749/1.SC2Replay") - self.assertEqual(replay.expansion, 'HotS') + self.assertEqual(replay.expansion, "HotS") replay = sc2reader.load_replay("test_replays/3.0.0.38749/2.SC2Replay") - self.assertEqual(replay.expansion, 'HotS') + self.assertEqual(replay.expansion, "HotS") def test_38996(self): replay = sc2reader.load_replay("test_replays/3.0.0.38996/1.SC2Replay") - self.assertEqual(replay.expansion, 'LotV') + self.assertEqual(replay.expansion, "LotV") replay = sc2reader.load_replay("test_replays/3.0.0.38996/2.SC2Replay") - self.assertEqual(replay.expansion, 'LotV') + self.assertEqual(replay.expansion, "LotV") def test_funny_minerals(self): replay = sc2reader.load_replay("test_replays/3.1.0/centralprotocol.SC2Replay") replay.load_map() - xmldoc = minidom.parseString(replay.map.archive.read_file('Objects')) - itemlist = xmldoc.getElementsByTagName('ObjectUnit') - mineralPosStrs = [ou.attributes['Position'].value for ou in itemlist if 'MineralField' in ou.attributes['UnitType'].value] - mineralFieldNames = list(set([ou.attributes['UnitType'].value for ou in itemlist if 'MineralField' in ou.attributes['UnitType'].value])) + xmldoc = minidom.parseString(replay.map.archive.read_file("Objects")) + itemlist = xmldoc.getElementsByTagName("ObjectUnit") + mineralPosStrs = [ + ou.attributes["Position"].value for ou in itemlist if "MineralField" in ou.attributes["UnitType"].value + ] + mineralFieldNames = list( + set([ou.attributes["UnitType"].value for ou in itemlist if "MineralField" in ou.attributes["UnitType"].value]) + ) # print(mineralFieldNames) self.assertTrue(len(mineralPosStrs) > 0) def test_dusk(self): replay = sc2reader.load_replay("test_replays/3.1.0/dusktowers.SC2Replay") - self.assertEqual(replay.expansion, 'LotV') + self.assertEqual(replay.expansion, "LotV") def test_32(self): replay = sc2reader.load_replay("test_replays/3.2.0/1.SC2Replay") self.assertTrue(replay is not None) def test_33(self): - for replaynum in range(1,4): + for replaynum in range(1, 4): replay = sc2reader.load_replay("test_replays/3.3.0/{}.SC2Replay".format(replaynum)) self.assertTrue(replay is not None) def test_33_shift_click_calldown_mule(self): replay = sc2reader.load_replay("test_replays/3.3.0/ggissue48.SC2Replay") + def efilter(e): return hasattr(e, "ability") and e.ability_name == "CalldownMULE" + self.assertEqual(len(list(filter(efilter, replay.events))), 29) def test_33_shift_click_spawn_larva(self): replay = sc2reader.load_replay("test_replays/3.3.0/ggissue49.SC2Replay") + def efilter(e): return hasattr(e, "ability") and e.ability_name == "SpawnLarva" + self.assertEqual(len(list(filter(efilter, replay.events))), 23) def test_34(self): replay = sc2reader.load_replay("test_replays/3.4.0/issueYY.SC2Replay") - self.assertEqual(replay.expansion, 'LotV') + self.assertEqual(replay.expansion, "LotV") def test_lotv_time(self): replay = sc2reader.load_replay("test_replays/lotv/lotv1.SC2Replay") @@ -538,56 +546,44 @@ def test_37(self): replay = sc2reader.load_replay("test_replays/3.7.0/2.SC2Replay") def test_312(self): - for replayfilename in [ - "test_replays/3.12/Honorgrounds.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay =factory.load_replay(replayfilename,load_level=0) - replay =factory.load_replay(replayfilename,load_level=1) + for replayfilename in ["test_replays/3.12/Honorgrounds.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename, load_level=0) + replay = factory.load_replay(replayfilename, load_level=1) def test_316(self): - for replayfilename in [ - "test_replays/3.16/AbyssalReef.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay =factory.load_replay(replayfilename) + for replayfilename in ["test_replays/3.16/AbyssalReef.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_54518(self): - for replayfilename in [ - "test_replays/3.14.0.54518/1.SC2Replay", - "test_replays/3.14.0.54518/2.SC2Replay", - "test_replays/3.14.0.54518/3.SC2Replay", + for replayfilename in [ + "test_replays/3.14.0.54518/1.SC2Replay", + "test_replays/3.14.0.54518/2.SC2Replay", + "test_replays/3.14.0.54518/3.SC2Replay", ]: - factory = sc2reader.factories.SC2Factory() - replay =factory.load_replay(replayfilename) + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_59587(self): - for replayfilename in [ - "test_replays/4.0.0.59587/1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay = factory.load_replay(replayfilename) + for replayfilename in ["test_replays/4.0.0.59587/1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_64469(self): - for replayfilename in [ - "test_replays/4.3.0.64469/1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay = factory.load_replay(replayfilename) + for replayfilename in ["test_replays/4.3.0.64469/1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_coop(self): - for replayfilename in [ - "test_replays/coop/CoA.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay = factory.load_replay(replayfilename) + for replayfilename in ["test_replays/coop/CoA.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_65895(self): - for replayfilename in [ - "test_replays/4.4.0.65895/1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay = factory.load_replay(replayfilename) + for replayfilename in ["test_replays/4.4.0.65895/1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_event_print(self): replay = sc2reader.load_replay("test_replays/lotv/lotv1.SC2Replay") @@ -599,11 +595,9 @@ def test_event_print(self): capturedOutput.close() def test_70154(self): - for replayfilename in [ - "test_replays/4.7.0.70154/1.SC2Replay", - ]: - factory = sc2reader.factories.SC2Factory() - replay = factory.load_replay(replayfilename) + for replayfilename in ["test_replays/4.7.0.70154/1.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) def test_anonymous_replay(self): replayfilename = "test_replays/4.1.2.60604/1.SC2Replay" @@ -641,33 +635,37 @@ def test_game_event_string(self): event.player = player self.assertEqual("{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), event._str_prefix()) + class TestGameEngine(unittest.TestCase): class TestEvent(object): - name='TestEvent' + name = "TestEvent" + def __init__(self, value): self.value = value + def __str__(self): return self.value class TestPlugin1(object): - name = 'TestPlugin1' + name = "TestPlugin1" def handleInitGame(self, event, replay): - yield TestGameEngine.TestEvent('b') - yield TestGameEngine.TestEvent('c') + yield TestGameEngine.TestEvent("b") + yield TestGameEngine.TestEvent("c") def handleTestEvent(self, event, replay): print("morestuff") - if event.value == 'd': + if event.value == "d": yield sc2reader.engine.PluginExit(self, code=1, details=dict(msg="Fail!")) else: - yield TestGameEngine.TestEvent('d') + yield TestGameEngine.TestEvent("d") def handleEndGame(self, event, replay): - yield TestGameEngine.TestEvent('g') + yield TestGameEngine.TestEvent("g") class TestPlugin2(object): - name = 'TestPlugin2' + name = "TestPlugin2" + def handleInitGame(self, event, replay): replay.engine_events = list() @@ -675,10 +673,10 @@ def handleTestEvent(self, event, replay): replay.engine_events.append(event) def handlePluginExit(self, event, replay): - yield TestGameEngine.TestEvent('e') + yield TestGameEngine.TestEvent("e") def handleEndGame(self, event, replay): - yield TestGameEngine.TestEvent('f') + yield TestGameEngine.TestEvent("f") class MockReplay(object): def __init__(self, events): @@ -688,12 +686,13 @@ def test_plugin1(self): engine = sc2reader.engine.GameEngine() engine.register_plugin(self.TestPlugin1()) engine.register_plugin(self.TestPlugin2()) - replay = self.MockReplay([self.TestEvent('a')]) + replay = self.MockReplay([self.TestEvent("a")]) engine.run(replay) - self.assertEqual(''.join(str(e) for e in replay.engine_events), 'bdecaf') - self.assertEqual(replay.plugin_failures, ['TestPlugin1']) - self.assertEqual(replay.plugin_result['TestPlugin1'], (1, dict(msg="Fail!"))) - self.assertEqual(replay.plugin_result['TestPlugin2'], (0, dict())) + self.assertEqual("".join(str(e) for e in replay.engine_events), "bdecaf") + self.assertEqual(replay.plugin_failures, ["TestPlugin1"]) + self.assertEqual(replay.plugin_result["TestPlugin1"], (1, dict(msg="Fail!"))) + self.assertEqual(replay.plugin_result["TestPlugin2"], (0, dict())) + class MockPlayer(object): def __init__(self): @@ -701,5 +700,6 @@ def __init__(self): self.play_race = None self.pid = None -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() From 0f3726f537ecedf952ef8fc5d7f846c76abc8115 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 14:14:09 +0200 Subject: [PATCH 006/136] this print does not help anybody, but the original author ... --- test_replays/test_all.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index c9b7f968..452b4543 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -654,7 +654,6 @@ def handleInitGame(self, event, replay): yield TestGameEngine.TestEvent("c") def handleTestEvent(self, event, replay): - print("morestuff") if event.value == "d": yield sc2reader.engine.PluginExit(self, code=1, details=dict(msg="Fail!")) else: From 5e93e1b8ddf430812a12c007143755c8e47c09ce Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 26 Jun 2019 14:16:49 +0200 Subject: [PATCH 007/136] CircleCI: Add more flake8 tests --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1370bc61..a7453d9e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ my-steps: &steps - run: sudo pip install flake8 pytest - run: python --version ; pip --version ; pwd ; ls -l # stop the build if there are Python syntax errors or undefined names - - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + - run: flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics #- run: pytest # test_s2gs test_replays From a403034cb30fd97b5e7b5b02a106adcf2b29e467 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 15:30:00 +0200 Subject: [PATCH 008/136] use a different replay for some lotv tests: - make sure there is really a zerg in that replay, otherwise the test would test nothing at all - remove outdated comment, that test does not fail. --- test_replays/test_all.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 452b4543..f974aaf2 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -435,19 +435,21 @@ def test_lotv1(self): def test_lotv_creepTracker(self): from sc2reader.engine.plugins import CreepTracker - for replayfilename in ["test_replays/lotv/lotv1.SC2Replay"]: + for replayfilename in ["test_replays/4.0.0.59587/1.SC2Replay"]: factory = sc2reader.factories.SC2Factory() pluginEngine = sc2reader.engine.GameEngine(plugins=[CreepTracker()]) replay = factory.load_replay(replayfilename, engine=pluginEngine, load_map=True) + is_at_least_one_zerg_in_game = False for player_id in replay.player: if replay.player[player_id].play_race == "Zerg": + is_at_least_one_zerg_in_game = True assert replay.player[player_id].max_creep_spread != 0 assert replay.player[player_id].creep_spread_by_minute + assert is_at_least_one_zerg_in_game def test_lotv_map(self): - # This test currently fails in decoders.py with 'TypeError: ord() expected a character, but string of length 0 found' - for replayfilename in ["test_replays/lotv/lotv1.SC2Replay"]: + for replayfilename in ["test_replays/4.0.0.59587/1.SC2Replay"]: factory = sc2reader.factories.SC2Factory() replay = factory.load_replay(replayfilename, load_level=1, load_map=True) From 2b1fc8430f8b1be7842687581ceb4d9f22905600 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 15:53:46 +0200 Subject: [PATCH 009/136] let's remove the s2gs tests for a moment in order to see if the replay tests work --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1370bc61..7e51e473 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ my-steps: &steps # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics #- run: pytest # test_s2gs test_replays - - run: python -m unittest discover test_s2gs + # - run: python -m unittest discover test_s2gs - run: python -m unittest discover test_replays jobs: From 0c875260d11e4c07419448d48635c2507822eac9 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 15:59:32 +0200 Subject: [PATCH 010/136] run s2gs test again --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7e51e473..1370bc61 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ my-steps: &steps # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics #- run: pytest # test_s2gs test_replays - # - run: python -m unittest discover test_s2gs + - run: python -m unittest discover test_s2gs - run: python -m unittest discover test_replays jobs: From 57c7064446c03a2c576bd3cfe73f0dd0595de2b4 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 16:35:46 +0200 Subject: [PATCH 011/136] replace a HotS summary with a LotV summary ... --- test_s2gs/lotv.s2gs | Bin 0 -> 30667 bytes test_s2gs/test_all.py | 10 ++++++---- 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 test_s2gs/lotv.s2gs diff --git a/test_s2gs/lotv.s2gs b/test_s2gs/lotv.s2gs new file mode 100644 index 0000000000000000000000000000000000000000..1324259498a75792f3cef8511a9a6ae1458c4020 GIT binary patch literal 30667 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�Yn~7 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 Date: Wed, 26 Jun 2019 17:01:13 +0200 Subject: [PATCH 012/136] just use pytest --- .circleci/config.yml | 4 +--- test_replays/{test_all.py => test_replays.py} | 0 2 files changed, 1 insertion(+), 3 deletions(-) rename test_replays/{test_all.py => test_replays.py} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1370bc61..c654bffd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,9 +9,7 @@ my-steps: &steps - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - #- run: pytest # test_s2gs test_replays - - run: python -m unittest discover test_s2gs - - run: python -m unittest discover test_replays + - run: pytest jobs: Python2: diff --git a/test_replays/test_all.py b/test_replays/test_replays.py similarity index 100% rename from test_replays/test_all.py rename to test_replays/test_replays.py From 4b899d35c59e9eb6e040c54b8c6ddf89b7176e62 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 17:05:51 +0200 Subject: [PATCH 013/136] pytest needs the package under test to be installed --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index c654bffd..8f34650f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ my-steps: &steps - run: sudo pip install -r requirements.txt - run: sudo pip install flake8 pytest - run: python --version ; pip --version ; pwd ; ls -l + - run: pip install --user . # stop the build if there are Python syntax errors or undefined names - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide From eb1ba54a765dd969cdacb59e18c2c2ff72baec8a Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 17:01:13 +0200 Subject: [PATCH 014/136] just use pytest --- .circleci/config.yml | 4 +--- test_replays/{test_all.py => test_replays.py} | 0 2 files changed, 1 insertion(+), 3 deletions(-) rename test_replays/{test_all.py => test_replays.py} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1370bc61..c654bffd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,9 +9,7 @@ my-steps: &steps - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - #- run: pytest # test_s2gs test_replays - - run: python -m unittest discover test_s2gs - - run: python -m unittest discover test_replays + - run: pytest jobs: Python2: diff --git a/test_replays/test_all.py b/test_replays/test_replays.py similarity index 100% rename from test_replays/test_all.py rename to test_replays/test_replays.py From 74905d89352826837fe95ad100a305c57c6a2365 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 17:05:51 +0200 Subject: [PATCH 015/136] pytest needs the package under test to be installed --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index c654bffd..8f34650f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ my-steps: &steps - run: sudo pip install -r requirements.txt - run: sudo pip install flake8 pytest - run: python --version ; pip --version ; pwd ; ls -l + - run: pip install --user . # stop the build if there are Python syntax errors or undefined names - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide From 17530b113fa95556d1afd41ce705c32b53fc9cb3 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 20:24:45 +0200 Subject: [PATCH 016/136] add docs for testing --- README.rst | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index ca2dfb3e..fa78c025 100644 --- a/README.rst +++ b/README.rst @@ -259,21 +259,36 @@ Please review the `CONTRIBUTING.md`_ file and get in touch with us before doing Testing ------------------- -We use the built in ``unittest`` module for testing. If you are still on Python 2.6 you will need to install ``unittest2`` because our test suite requires newer features than are included in the main distribution. +We use ``pytest`` for testing. If you don't have it just ``pip install pytest``. -To run the tests just use:: +To run the tests, just do:: - python test_replays/test_all.py - python test_s2gs/test_all.py + pytest -When repeatedly running tests it can be very helpful to make sure you've set a local cache directory to prevent long fetch times from battle.net:: - SC2READER_CACHE_DIR=local_cache PYTHONPATH=. python -m unittest test_replays.test_all +When repeatedly running tests it can be very helpful to make sure you've set a local cache directory to prevent long fetch times from battle.net. +So make some local cache folder:: + + mkdir cache + +And then run the tests like this:: + + SC2READER_CACHE_DIR=./cache pytest To run just one test: - SC2READER_CACHE_DIR=local_cache PYTHONPATH=. python -m unittest test_replays.test_all.TestReplays.test_38749 + SC2READER_CACHE_DIR=./cache pytest test_replays/test_replays.py::TestReplays::test_38749 + +If you'd like to see which are the 10 slowest tests (to find performance issues maybe):: + + pytest --durations=10 + +If you want ``pytest`` to stop after the first failing test:: + + pytest -x + +Have a look at the very fine ``pytest`` docs for more information. Good luck, have fun! From 5cd2b8472212e9a6640f2b2d45301fcbb7197bf5 Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Wed, 26 Jun 2019 20:44:15 +0200 Subject: [PATCH 017/136] add pytest as test requirement to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c5d1d694..d067af22 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ }, install_requires=['mpyq>=0.2.3', 'argparse', 'ordereddict', 'unittest2', 'pil'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.4'], + tests_require=["pytest"], packages=setuptools.find_packages(), include_package_data=True, zip_safe=True From 6fcd7f6a1ccc4f067024a9762014f05d3ba5395c Mon Sep 17 00:00:00 2001 From: Dominik Neise Date: Sat, 29 Jun 2019 20:27:39 +0200 Subject: [PATCH 018/136] reformat codebase using python/black (#87) * reformat codebase using python/black * black -l 80 * run black --check * mention coding style * just use black without options * run black only on py3 * use yaml merge key to define a few special steps for py3 * lists cannot be merged ... my YAML foo is weak :-( * maybe conditions do the trick * bad old copy & paste * define a new job, only for style checks * fix typo; add link to black editor integration * blacken setup.py * remove accidentally added test_script --- .circleci/config.yml | 31 +- CONTRIBUTING.md | 15 + docs/source/conf.py | 104 +- examples/sc2autosave.py | 351 +++-- examples/sc2store.py | 118 +- generate_build_data.py | 216 ++- new_units.py | 40 +- sc2reader/__init__.py | 4 +- sc2reader/constants.py | 181 +-- sc2reader/data/__init__.py | 136 +- sc2reader/data/create_lookup.py | 16 +- sc2reader/decoders.py | 62 +- sc2reader/engine/__init__.py | 1 + sc2reader/engine/engine.py | 49 +- sc2reader/engine/events.py | 6 +- sc2reader/engine/plugins/__init__.py | 1 - sc2reader/engine/plugins/apm.py | 13 +- sc2reader/engine/plugins/context.py | 160 +- sc2reader/engine/plugins/creeptracker.py | 313 ++-- sc2reader/engine/plugins/gameheart.py | 29 +- sc2reader/engine/plugins/selection.py | 27 +- sc2reader/engine/plugins/supply.py | 191 ++- sc2reader/engine/utils.py | 2 +- sc2reader/events/base.py | 2 +- sc2reader/events/game.py | 233 ++- sc2reader/events/message.py | 18 +- sc2reader/events/tracker.py | 121 +- sc2reader/exceptions.py | 2 +- sc2reader/factories/plugins/replay.py | 189 ++- sc2reader/factories/plugins/utils.py | 29 +- sc2reader/factories/sc2factory.py | 64 +- sc2reader/log_utils.py | 16 +- sc2reader/objects.py | 151 +- sc2reader/readers.py | 1705 ++++++++++++---------- sc2reader/resources.py | 736 +++++++--- sc2reader/scripts/sc2attributes.py | 59 +- sc2reader/scripts/sc2json.py | 32 +- sc2reader/scripts/sc2parse.py | 114 +- sc2reader/scripts/sc2printer.py | 138 +- sc2reader/scripts/sc2replayer.py | 53 +- sc2reader/scripts/utils.py | 16 +- sc2reader/utils.py | 142 +- setup.py | 65 +- test_replays/test_replays.py | 181 ++- test_s2gs/test_all.py | 14 +- 45 files changed, 3811 insertions(+), 2335 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 07526b6a..1344edfc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,31 +1,44 @@ version: 2.0 -my-steps: &steps +build_and_test: &build_and_test_steps - checkout - run: sudo pip install -r requirements.txt - - run: sudo pip install flake8 pytest - - run: python --version ; pip --version ; pwd ; ls -l - run: pip install --user . - # stop the build if there are Python syntax errors or undefined names - - run: flake8 . --count --select=E9,F63,F72,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - run: sudo pip install pytest + - run: python --version ; pip --version ; pwd ; ls -l - run: pytest + jobs: + StyleCheck: + docker: + - image: circleci/python:3.7 + steps: + - checkout + - run: sudo pip install flake8 black + - run: python --version ; pip --version ; pwd ; ls -l + # stop the build if there are Python syntax errors or undefined names + - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - run: black . --check + + Python2: docker: - image: circleci/python:2.7.15 - steps: *steps + steps: *build_and_test_steps Python3: docker: - image: circleci/python:3.7 - steps: *steps + steps: *build_and_test_steps + workflows: version: 2 build: jobs: + - StyleCheck - Python2 - Python3 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ce923408..53017ede 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,3 +18,18 @@ Please submit patches by pull request where possible. Patches should add a test If you are having trouble running/add/fixing tests for your patch let me know and I'll see if I can help. + +Coding Style +============== + +We'd like our code to follow PEP8 coding style in this project. +We use [python/black](https://github.com/python/black) in order to make our lives easier. +We propose you do the same within this project, otherwise you might be asked to +reformat your pull requests. + +It's really simple just: + + pip install black + black . + +And [there are plugins for many editors](https://black.readthedocs.io/en/stable/editor_integration.html). diff --git a/docs/source/conf.py b/docs/source/conf.py index 91b8a826..15623ce9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,32 +19,32 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.pngmath', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.pngmath", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'sc2reader' -copyright = u'2011-2013' +project = u"sc2reader" +copyright = u"2011-2013" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -58,163 +58,159 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'nature' +html_theme = "nature" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'sc2readerdoc' +htmlhelp_basename = "sc2readerdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'sc2reader.tex', u'sc2reader Documentation', - u'Graylin Kim', 'manual'), + ("index", "sc2reader.tex", u"sc2reader Documentation", u"Graylin Kim", "manual") ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'sc2reader', u'sc2reader Documentation', - [u'Graylin Kim'], 1) -] +man_pages = [("index", "sc2reader", u"sc2reader Documentation", [u"Graylin Kim"], 1)] diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index eb6b48e4..74bb1567 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -'''sc2autosave is a utility for reorganizing and renaming Starcraft II files. +"""sc2autosave is a utility for reorganizing and renaming Starcraft II files. Overview ============== @@ -158,7 +158,7 @@ POST-Parse filtering vs preparse filtering? POST-Parse, how to do it?!?!?!?! -''' +""" import argparse import cPickle import os @@ -169,91 +169,91 @@ import sc2reader try: - raw_input # Python 2 + raw_input # Python 2 except NameError: raw_input = input # Python 3 def run(args): - #Reset wipes the destination clean so we can start over. + # Reset wipes the destination clean so we can start over. if args.reset: reset(args) - #Set up validates the destination and source directories. - #It also loads the previous state or creates one as necessary. + # Set up validates the destination and source directories. + # It also loads the previous state or creates one as necessary. state = setup(args) - #We break out of this loop in batch mode and on KeyboardInterrupt + # We break out of this loop in batch mode and on KeyboardInterrupt while True: - #The file scan uses the arguments and the state to filter down to - #only new (since the last sync time) files. + # The file scan uses the arguments and the state to filter down to + # only new (since the last sync time) files. for path in scan(args, state): try: - #Read the file and expose useful aspects for renaming/filtering + # Read the file and expose useful aspects for renaming/filtering replay = sc2reader.load_replay(path, load_level=2) except KeyboardInterrupt: raise except: - #Failure to parse + # Failure to parse file_name = os.path.basename(path) - directory = make_directory(args, ('parse_error',)) + directory = make_directory(args, ("parse_error",)) new_path = os.path.join(directory, file_name) - source_path = path[len(args.source):] + source_path = path[len(args.source) :] args.log.write("Error parsing replay: {0}".format(source_path)) if not args.dryrun: args.action.run(path, new_path) - #Skip to the next replay + # Skip to the next replay continue aspects = generate_aspects(args, replay) - #Use the filter args to select files based on replay attributes + # Use the filter args to select files based on replay attributes if filter_out_replay(args, replay): continue - #Apply the aspects to the rename formatting. + # Apply the aspects to the rename formatting. #'/' is a special character for creation of subdirectories. - #TODO: Handle duplicate replay names, its possible.. - path_parts = args.rename.format(**aspects).split('/') - filename = path_parts.pop()+'.SC2Replay' + # TODO: Handle duplicate replay names, its possible.. + path_parts = args.rename.format(**aspects).split("/") + filename = path_parts.pop() + ".SC2Replay" - #Construct the directory and file paths; create needed directories + # Construct the directory and file paths; create needed directories directory = make_directory(args, path_parts) new_path = os.path.join(directory, filename) - #Find the source relative to the source directory for reporting - dest_path = new_path[len(args.dest):] - source_path = path[len(args.source):] + # Find the source relative to the source directory for reporting + dest_path = new_path[len(args.dest) :] + source_path = path[len(args.source) :] - #Log the action and run it if we are live + # Log the action and run it if we are live msg = "{0}:\n\tSource: {1}\n\tDest: {2}\n" args.log.write(msg.format(args.action.type, source_path, dest_path)) if not args.dryrun: args.action.run(path, new_path) - #After every batch completes, save the state and flush the log - #TODO: modify the state to include a list of remaining files + # After every batch completes, save the state and flush the log + # TODO: modify the state to include a list of remaining files args.log.flush() save_state(state, args) - #We only run once in batch mode! - if args.mode == 'BATCH': + # We only run once in batch mode! + if args.mode == "BATCH": break - #Since new replays come in fairly infrequently, reduce system load - #by sleeping for an acceptable response time before the next scan. + # Since new replays come in fairly infrequently, reduce system load + # by sleeping for an acceptable response time before the next scan. time.sleep(args.period) - args.log.write('Batch Completed') + args.log.write("Batch Completed") def filter_out_replay(args, replay): player_names = set([player.name for player in replay.players]) filter_out_player = not set(args.filter_player) & player_names - if args.filter_rule == 'ALLOW': + if args.filter_rule == "ALLOW": return filter_out_player else: return not filter_out_player @@ -268,8 +268,8 @@ def player_compare(player1, player2): # Normalize the player names and generate our key metrics player1_name = player1.name.lower() player2_name = player2.name.lower() - player1_favored = (player1_name in favored_set) - player2_favored = (player2_name in favored_set) + player1_favored = player1_name in favored_set + player2_favored = player2_name in favored_set # The favored player always comes first in the ordering if player1_favored and not player2_favored: @@ -286,7 +286,7 @@ def player_compare(player1, player2): # If neither is favored, we'll order by number for now # TODO: Allow command line specification of other orderings (maybe?) else: - return player1.pid-player2.pid + return player1.pid - player2.pid def team_compare(team1, team2): # Normalize the team name lists and generate our key metrics @@ -310,7 +310,7 @@ def team_compare(team1, team2): # If neither is favored, we'll order by number for now # TODO: Allow command line specification of other orderings (maybe?) else: - return team1.number-team2.number + return team1.number - team2.number return team_compare, player_compare @@ -321,8 +321,8 @@ def generate_aspects(args, replay): for team in teams: team.players = sorted(team.players, args.player_compare) composition = sorted(p.play_race[0].upper() for p in team.players) - matchups.append(''.join(composition)) - string = ', '.join(p.format(args.player_format) for p in team.players) + matchups.append("".join(composition)) + string = ", ".join(p.format(args.player_format) for p in team.players) team_strings.append(string) return sc2reader.utils.AttributeDict( @@ -331,8 +331,8 @@ def generate_aspects(args, replay): map=replay.map, type=replay.type, date=replay.date.strftime(args.date_format), - matchup='v'.join(matchups), - teams=' vs '.join(team_strings) + matchup="v".join(matchups), + teams=" vs ".join(team_strings), ) @@ -341,11 +341,11 @@ def make_directory(args, path_parts): for part in path_parts: directory = os.path.join(directory, part) if not os.path.exists(directory): - args.log.write('Creating subfolder: {0}\n'.format(directory)) + args.log.write("Creating subfolder: {0}\n".format(directory)) if not args.dryrun: os.mkdir(directory) elif not os.path.isdir(directory): - exit('Cannot create subfolder. Path is occupied: {0}', directory) + exit("Cannot create subfolder. Path is occupied: {0}", directory) return directory @@ -358,12 +358,13 @@ def scan(args, state): allow=False, exclude=args.exclude_dirs, depth=args.depth, - followlinks=args.follow_links) + followlinks=args.follow_links, + ) return filter(lambda f: os.path.getctime(f) > state.last_sync, files) def exit(msg, *args, **kwargs): - sys.exit(msg.format(*args, **kwargs)+"\n\nScript Aborted.") + sys.exit(msg.format(*args, **kwargs) + "\n\nScript Aborted.") def reset(args): @@ -372,10 +373,14 @@ def reset(args): elif not os.path.isdir(args.dest): exit("Cannot reset, destination must be directory: {0}", args.dest) - print('About to reset directory: {0}\nAll files and subdirectories will be removed.'.format(args.dest)) - choice = raw_input('Proceed anyway? (y/n) ') - if choice.lower() == 'y': - args.log.write('Removing old directory: {0}\n'.format(args.dest)) + print( + "About to reset directory: {0}\nAll files and subdirectories will be removed.".format( + args.dest + ) + ) + choice = raw_input("Proceed anyway? (y/n) ") + if choice.lower() == "y": + args.log.write("Removing old directory: {0}\n".format(args.dest)) if not args.dryrun: print(args.dest) shutil.rmtree(args.dest) @@ -385,25 +390,27 @@ def reset(args): def setup(args): args.team_compare, args.player_compare = create_compare_funcs(args) - args.action = sc2reader.utils.AttributeDict(type=args.action, run=shutil.copy if args.action == 'COPY' else shutil.move) + args.action = sc2reader.utils.AttributeDict( + type=args.action, run=shutil.copy if args.action == "COPY" else shutil.move + ) if not os.path.exists(args.source): - msg = 'Source does not exist: {0}.\n\nScript Aborted.' + msg = "Source does not exist: {0}.\n\nScript Aborted." sys.exit(msg.format(args.source)) elif not os.path.isdir(args.source): - msg = 'Source is not a directory: {0}.\n\nScript Aborted.' + msg = "Source is not a directory: {0}.\n\nScript Aborted." sys.exit(msg.format(args.source)) if not os.path.exists(args.dest): if not args.dryrun: os.mkdir(args.dest) else: - args.log.write('Creating destination: {0}\n'.format(args.dest)) + args.log.write("Creating destination: {0}\n".format(args.dest)) elif not os.path.isdir(args.dest): - sys.exit('Destination must be a directory.\n\nScript Aborted') + sys.exit("Destination must be a directory.\n\nScript Aborted") - data_file = os.path.join(args.dest, 'sc2autosave.dat') + data_file = os.path.join(args.dest, "sc2autosave.dat") - args.log.write('Loading state from file: {0}\n'.format(data_file)) + args.log.write("Loading state from file: {0}\n".format(data_file)) if os.path.isfile(data_file) and not args.reset: with open(data_file) as file: return cPickle.load(file) @@ -413,67 +420,115 @@ def setup(args): def save_state(state, args): state.last_sync = time.time() - data_file = os.path.join(args.dest, 'sc2autosave.dat') + data_file = os.path.join(args.dest, "sc2autosave.dat") if not args.dryrun: - with open(data_file, 'w') as file: + with open(data_file, "w") as file: cPickle.dump(state, file) else: - args.log.write('Writing state to file: {0}\n'.format(data_file)) + args.log.write("Writing state to file: {0}\n".format(data_file)) def main(): parser = argparse.ArgumentParser( - description='Automatically copy new replays to directory', - fromfile_prefix_chars='@', + description="Automatically copy new replays to directory", + fromfile_prefix_chars="@", formatter_class=sc2reader.scripts.utils.Formatter.new(max_help_position=35), - epilog="And that's all folks") - - required = parser.add_argument_group('Required Arguments') - required.add_argument('source', type=str, - help='The source directory to poll') - required.add_argument('dest', type=str, - help='The destination directory to copy to') - - general = parser.add_argument_group('General Options') - general.add_argument('--mode', dest='mode', - type=str, choices=['BATCH', 'CYCLE'], default='BATCH', - help='The operating mode for the organizer') - - general.add_argument('--action', dest='action', - choices=['COPY', 'MOVE'], default="COPY", type=str, - help='Have the organizer move your files instead of copying') - general.add_argument('--period', - dest='period', type=int, default=0, - help='The period of time to wait between scans.') - general.add_argument('--log', dest='log', metavar='LOGFILE', - type=argparse.FileType('w'), default=sys.stdout, - help='Destination file for log information') - general.add_argument('--dryrun', - dest='dryrun', action="store_true", - help="Don't do anything. Only simulate the output") - general.add_argument('--reset', - dest='reset', action='store_true', default=False, - help='Wipe the destination directory clean and start over.') - - fileargs = parser.add_argument_group('File Options') - fileargs.add_argument('--depth', - dest='depth', type=int, default=-1, - help='Maximum recussion depth. -1 (default) is unlimited.') - fileargs.add_argument('--exclude-dirs', dest='exclude_dirs', - type=str, metavar='NAME', nargs='+', default=[], - help='A list of directory names to exclude during recursion') - fileargs.add_argument('--exclude-files', dest='exclude_files', - type=str, metavar='REGEX', default="", - help='An expression to match excluded files') - fileargs.add_argument('--follow-links', - dest='follow_links', action="store_true", default=False, - help="Enable following of symbolic links while scanning") - - renaming = parser.add_argument_group('Renaming Options') - renaming.add_argument('--rename', - dest='rename', type=str, metavar='FORMAT', nargs='?', + epilog="And that's all folks", + ) + + required = parser.add_argument_group("Required Arguments") + required.add_argument("source", type=str, help="The source directory to poll") + required.add_argument("dest", type=str, help="The destination directory to copy to") + + general = parser.add_argument_group("General Options") + general.add_argument( + "--mode", + dest="mode", + type=str, + choices=["BATCH", "CYCLE"], + default="BATCH", + help="The operating mode for the organizer", + ) + + general.add_argument( + "--action", + dest="action", + choices=["COPY", "MOVE"], + default="COPY", + type=str, + help="Have the organizer move your files instead of copying", + ) + general.add_argument( + "--period", + dest="period", + type=int, + default=0, + help="The period of time to wait between scans.", + ) + general.add_argument( + "--log", + dest="log", + metavar="LOGFILE", + type=argparse.FileType("w"), + default=sys.stdout, + help="Destination file for log information", + ) + general.add_argument( + "--dryrun", + dest="dryrun", + action="store_true", + help="Don't do anything. Only simulate the output", + ) + general.add_argument( + "--reset", + dest="reset", + action="store_true", + default=False, + help="Wipe the destination directory clean and start over.", + ) + + fileargs = parser.add_argument_group("File Options") + fileargs.add_argument( + "--depth", + dest="depth", + type=int, + default=-1, + help="Maximum recussion depth. -1 (default) is unlimited.", + ) + fileargs.add_argument( + "--exclude-dirs", + dest="exclude_dirs", + type=str, + metavar="NAME", + nargs="+", + default=[], + help="A list of directory names to exclude during recursion", + ) + fileargs.add_argument( + "--exclude-files", + dest="exclude_files", + type=str, + metavar="REGEX", + default="", + help="An expression to match excluded files", + ) + fileargs.add_argument( + "--follow-links", + dest="follow_links", + action="store_true", + default=False, + help="Enable following of symbolic links while scanning", + ) + + renaming = parser.add_argument_group("Renaming Options") + renaming.add_argument( + "--rename", + dest="rename", + type=str, + metavar="FORMAT", + nargs="?", default="{length} {type} on {map}", - help='''\ + help="""\ The renaming format string. can have the following values: * {length} - The length of the replay ([H:]MM:SS) @@ -482,41 +537,73 @@ def main(): * {match} - Race matchup in team order, alphabetically by race. * {date} - The date the replay was played on * {teams} - The player line up - ''') - - renaming.add_argument('--length-format', - dest='length_format', type=str, metavar='FORMAT', default='%M.%S', - help='The length format string. See the python time module for details') - renaming.add_argument('--player-format', - dest='player_format', type=str, metavar='FORMAT', default='{name} ({play_race})', - help='The player format string used to render the :teams content item.') - renaming.add_argument('--date-format', - dest='date_format', type=str, metavar='FORMAT', default='%m-%d-%Y', - help='The date format string used to render the :date content item.') - ''' + """, + ) + + renaming.add_argument( + "--length-format", + dest="length_format", + type=str, + metavar="FORMAT", + default="%M.%S", + help="The length format string. See the python time module for details", + ) + renaming.add_argument( + "--player-format", + dest="player_format", + type=str, + metavar="FORMAT", + default="{name} ({play_race})", + help="The player format string used to render the :teams content item.", + ) + renaming.add_argument( + "--date-format", + dest="date_format", + type=str, + metavar="FORMAT", + default="%m-%d-%Y", + help="The date format string used to render the :date content item.", + ) + """ renaming.add_argument('--team-order-by', dest='team_order', type=str, metavar='FIELD', default='NUMBER', help='The field by which teams are ordered.') renaming.add_argument('--player-order-by', dest='player_order', type=str, metavar='FIELD', default='NAME', help='The field by which players are ordered on teams.') - ''' - renaming.add_argument('--favored', dest='favored', - type=str, default=[], metavar='NAME', nargs='+', - help='A list of the players to favor in ordering teams and players') - - filterargs = parser.add_argument_group('Filtering Options') - filterargs.add_argument('--filter-rule', dest='filter_rule', - choices=["ALLOW","DENY"], - help="The filters can either be used as a white list or a black list") - filterargs.add_argument('--filter-player', metavar='NAME', - dest='filter_player', nargs='+', type=str, default=[], - help="A list of players to filter on") + """ + renaming.add_argument( + "--favored", + dest="favored", + type=str, + default=[], + metavar="NAME", + nargs="+", + help="A list of the players to favor in ordering teams and players", + ) + + filterargs = parser.add_argument_group("Filtering Options") + filterargs.add_argument( + "--filter-rule", + dest="filter_rule", + choices=["ALLOW", "DENY"], + help="The filters can either be used as a white list or a black list", + ) + filterargs.add_argument( + "--filter-player", + metavar="NAME", + dest="filter_player", + nargs="+", + type=str, + default=[], + help="A list of players to filter on", + ) try: run(parser.parse_args()) except KeyboardInterrupt: print("\n\nScript Interupted. Process Aborting") -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/examples/sc2store.py b/examples/sc2store.py index 55ff22b0..0a072f43 100755 --- a/examples/sc2store.py +++ b/examples/sc2store.py @@ -11,6 +11,7 @@ import sc2reader from pprint import PrettyPrinter + pprint = PrettyPrinter(indent=2).pprint from sqlalchemy import create_engine @@ -22,50 +23,54 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy + Base = declarative_base() -party_member = Table('party_member', Base.metadata, - Column('person_id', Integer, ForeignKey('person.id')), - Column('party_id', Integer, ForeignKey('party.id')), +party_member = Table( + "party_member", + Base.metadata, + Column("person_id", Integer, ForeignKey("person.id")), + Column("party_id", Integer, ForeignKey("party.id")), ) + class Person(Base): - __tablename__ = 'person' - id = Column(Integer, Sequence('person_id_seq'), primary_key=True) + __tablename__ = "person" + id = Column(Integer, Sequence("person_id_seq"), primary_key=True) name = Column(String(50)) url = Column(String(50)) - parties = relationship('Party', secondary=party_member) - players = relationship('Player') + parties = relationship("Party", secondary=party_member) + players = relationship("Player") class Party(Base): - __tablename__ = 'party' - id = Column(Integer, Sequence('party_id_seq'), primary_key=True) + __tablename__ = "party" + id = Column(Integer, Sequence("party_id_seq"), primary_key=True) player_names = Column(String(255)) - members = relationship('Person', secondary=party_member) - teams = relationship('Team') + members = relationship("Person", secondary=party_member) + teams = relationship("Team") def __init__(self, *players): - self.player_names = '' + self.player_names = "" self.members = list() self.add_players(*players) def add_players(self, *players): for player in players: - self.player_names += '['+player.name+']' + self.player_names += "[" + player.name + "]" self.members.append(player.person) @classmethod def make_player_names(self, players): - return ''.join(sorted('['+player.name+']' for player in players)) + return "".join(sorted("[" + player.name + "]" for player in players)) class Game(Base): - __tablename__ = 'game' - id = Column(Integer, Sequence('game_id_seq'), primary_key=True) + __tablename__ = "game" + id = Column(Integer, Sequence("game_id_seq"), primary_key=True) map = Column(String(255)) file_name = Column(String(255)) @@ -77,8 +82,8 @@ class Game(Base): build = Column(String(25)) release_string = Column(String(50)) - teams = relationship('Team') - players = relationship('Player') + teams = relationship("Team") + players = relationship("Player") def __init__(self, replay, db): self.map = replay.map @@ -90,54 +95,58 @@ def __init__(self, replay, db): self.winner_known = replay.winner_known self.build = replay.build self.release_string = replay.release_string - self.teams = [Team(team,db) for team in replay.teams] - self.matchup = 'v'.join(sorted(team.lineup for team in self.teams)) + self.teams = [Team(team, db) for team in replay.teams] + self.matchup = "v".join(sorted(team.lineup for team in self.teams)) self.players = sum((team.players for team in self.teams), []) class Team(Base): - __tablename__ = 'team' - id = Column(Integer, Sequence('team_id_seq'), primary_key=True) - game_id = Column(Integer, ForeignKey('game.id')) - party_id = Column(Integer, ForeignKey('party.id')) + __tablename__ = "team" + id = Column(Integer, Sequence("team_id_seq"), primary_key=True) + game_id = Column(Integer, ForeignKey("game.id")) + party_id = Column(Integer, ForeignKey("party.id")) result = Column(String(50)) number = Column(Integer) lineup = Column(String(10)) - players = relationship('Player') - party = relationship('Party') - game = relationship('Game') + players = relationship("Player") + party = relationship("Party") + game = relationship("Game") def __init__(self, team, db): self.number = team.number self.result = team.result - self.players = [Player(player,db) for player in team.players] - self.lineup = ''.join(sorted(player.play_race[0].upper() for player in self.players)) + self.players = [Player(player, db) for player in team.players] + self.lineup = "".join( + sorted(player.play_race[0].upper() for player in self.players) + ) try: player_names = Party.make_player_names(self.players) - self.party = db.query(Party).filter(Party.player_names == player_names).one() + self.party = ( + db.query(Party).filter(Party.player_names == player_names).one() + ) except NoResultFound as e: self.party = Party(*self.players) class Player(Base): - __tablename__ = 'player' - id = Column(Integer, Sequence('player_id_seq'), primary_key=True) - game_id = Column(Integer, ForeignKey('game.id')) - team_id = Column(Integer, ForeignKey('team.id')) - person_id = Column(Integer, ForeignKey('person.id')) + __tablename__ = "player" + id = Column(Integer, Sequence("player_id_seq"), primary_key=True) + game_id = Column(Integer, ForeignKey("game.id")) + team_id = Column(Integer, ForeignKey("team.id")) + person_id = Column(Integer, ForeignKey("person.id")) play_race = Column(String(20)) pick_race = Column(String(20)) color_str = Column(String(20)) color_hex = Column(String(20)) - name = association_proxy('person','name') - person = relationship('Person') - team = relationship('Team') - game = relationship('Game') + name = association_proxy("person", "name") + person = relationship("Person") + team = relationship("Team") + game = relationship("Game") def __init__(self, player, db): try: @@ -154,18 +163,33 @@ def __init__(self, player, db): class Message(Base): - __tablename__ = 'message' - id = Column(Integer, Sequence('message_id_seq'), primary_key=True) - player_id = Column(Integer, ForeignKey('player.id')) + __tablename__ = "message" + id = Column(Integer, Sequence("message_id_seq"), primary_key=True) + player_id = Column(Integer, ForeignKey("player.id")) def parse_args(): import argparse - parser = argparse.ArgumentParser(description='Stores replay meta data into an SQL database') - parser.add_argument('--storage', default='sqlite:///:memory:', type=str, help='Path to the sql storage file of choice') - parser.add_argument('paths', metavar='PATH', type=str, nargs='+', help='Path to a replay file or a folder of replays') + + parser = argparse.ArgumentParser( + description="Stores replay meta data into an SQL database" + ) + parser.add_argument( + "--storage", + default="sqlite:///:memory:", + type=str, + help="Path to the sql storage file of choice", + ) + parser.add_argument( + "paths", + metavar="PATH", + type=str, + nargs="+", + help="Path to a replay file or a folder of replays", + ) return parser.parse_args() + def main(): args = parse_args() db = load_session(args) @@ -179,7 +203,7 @@ def main(): print(list(db.query(distinct(Person.name)).all())) - #for row in db.query(distinct(Person.name)).all(): + # for row in db.query(distinct(Person.name)).all(): # print(row) @@ -190,5 +214,5 @@ def load_session(args): return Session() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/generate_build_data.py b/generate_build_data.py index b56a72eb..32bdbbd2 100644 --- a/generate_build_data.py +++ b/generate_build_data.py @@ -35,7 +35,9 @@ def generate_build_data(balance_data_path): while len(ability_lookup[ability_name]) <= command_index: ability_lookup[ability_name].append("") - command_name = command_id if command_id != "Execute" else ability_name + command_name = ( + command_id if command_id != "Execute" else ability_name + ) ability_lookup[ability_name][command_index] = command_name unit_id = root.get("id") @@ -76,7 +78,9 @@ def generate_build_data(balance_data_path): ability_lookup[build_ability_name].append("") build_command_name = "Build{}".format(built_unit_id) - ability_lookup[build_ability_name][command_index] = build_command_name + ability_lookup[build_ability_name][ + command_index + ] = build_command_name train_unit_elements = root.findall("./trains/unit") if train_unit_elements: @@ -90,43 +94,53 @@ def generate_build_data(balance_data_path): ability_lookup[train_ability_name] = [] for element in train_unit_elements: - element_ability_index = element.get("ability") - trained_unit_name = element.get("id") - - if trained_unit_name: - # Handle cases where a unit can train other units using multiple ability indices. - # The Nexus is currently the only known example. - if element_ability_index != train_ability_index: - train_ability_index = element_ability_index - - train_ability_name = "{}Train{}".format(unit_id, trained_unit_name) - abilities[train_ability_index] = train_ability_name - - if train_ability_name not in ability_lookup: - ability_lookup[train_ability_name] = [] - - command_index_str = element.get("index") - - if command_index_str: - command_index = int(command_index_str) - - # Pad potential gaps in command indices with empty strings - while len(ability_lookup[train_ability_name]) <= command_index: - ability_lookup[train_ability_name].append("") - - ability_lookup[train_ability_name][command_index] = train_ability_name - else: - command_index_str = element.get("index") - - if command_index_str: - command_index = int(command_index_str) - - # Pad potential gaps in command indices with empty strings - while len(ability_lookup[train_ability_name]) <= command_index: - ability_lookup[train_ability_name].append("") - - train_command_name = "Train{}".format(trained_unit_name) - ability_lookup[train_ability_name][command_index] = train_command_name + element_ability_index = element.get("ability") + trained_unit_name = element.get("id") + + if trained_unit_name: + # Handle cases where a unit can train other units using multiple ability indices. + # The Nexus is currently the only known example. + if element_ability_index != train_ability_index: + train_ability_index = element_ability_index + + train_ability_name = "{}Train{}".format( + unit_id, trained_unit_name + ) + abilities[train_ability_index] = train_ability_name + + if train_ability_name not in ability_lookup: + ability_lookup[train_ability_name] = [] + + command_index_str = element.get("index") + + if command_index_str: + command_index = int(command_index_str) + + # Pad potential gaps in command indices with empty strings + while ( + len(ability_lookup[train_ability_name]) <= command_index + ): + ability_lookup[train_ability_name].append("") + + ability_lookup[train_ability_name][ + command_index + ] = train_ability_name + else: + command_index_str = element.get("index") + + if command_index_str: + command_index = int(command_index_str) + + # Pad potential gaps in command indices with empty strings + while ( + len(ability_lookup[train_ability_name]) <= command_index + ): + ability_lookup[train_ability_name].append("") + + train_command_name = "Train{}".format(trained_unit_name) + ability_lookup[train_ability_name][ + command_index + ] = train_command_name research_upgrade_elements = root.findall("./researches/upgrade") if research_upgrade_elements: @@ -150,17 +164,25 @@ def generate_build_data(balance_data_path): ability_lookup[research_ability_name].append("") research_command_name = "Research{}".format(researched_upgrade_id) - ability_lookup[research_ability_name][command_index] = research_command_name + ability_lookup[research_ability_name][ + command_index + ] = research_command_name - sorted_units = collections.OrderedDict(sorted(units.items(), key=lambda x: int(x[0]))) - sorted_abilities = collections.OrderedDict(sorted(abilities.items(), key=lambda x: int(x[0]))) + sorted_units = collections.OrderedDict( + sorted(units.items(), key=lambda x: int(x[0])) + ) + sorted_abilities = collections.OrderedDict( + sorted(abilities.items(), key=lambda x: int(x[0])) + ) unit_lookup = dict((unit_name, unit_name) for _, unit_name in sorted_units.items()) return sorted_units, sorted_abilities, unit_lookup, ability_lookup -def combine_lookups(old_unit_lookup, old_ability_lookup, new_unit_lookup, new_ability_lookup): +def combine_lookups( + old_unit_lookup, old_ability_lookup, new_unit_lookup, new_ability_lookup +): unit_lookup = collections.OrderedDict(old_unit_lookup) ability_lookup = collections.OrderedDict(old_ability_lookup) @@ -190,21 +212,42 @@ def combine_lookups(old_unit_lookup, old_ability_lookup, new_unit_lookup, new_ab def main(): - parser = argparse.ArgumentParser(description='Generate and install new [BUILD_VERSION]_abilities.csv, ' - '[BUILD_VERSION]_units.csv, and update ability_lookup.csv and ' - 'unit_lookup.csv files with any new units and ability commands.') - parser.add_argument('expansion', metavar='EXPANSION', type=str, choices=['WoL', 'HotS', 'LotV'], - help='the expansion level of the balance data export, one of \'WoL\', \'HotS\', or \'LotV\'') - parser.add_argument('build_version', metavar='BUILD_VERSION', type=int, - help='the build version of the balance data export') - parser.add_argument('balance_data_path', metavar='BALANCE_DATA_PATH', type=str, - help='the path to the balance data export') - parser.add_argument('project_path', metavar='SC2READER_PROJECT_PATH', type=str, - help='the path to the root of the sc2reader project directory') + parser = argparse.ArgumentParser( + description="Generate and install new [BUILD_VERSION]_abilities.csv, " + "[BUILD_VERSION]_units.csv, and update ability_lookup.csv and " + "unit_lookup.csv files with any new units and ability commands." + ) + parser.add_argument( + "expansion", + metavar="EXPANSION", + type=str, + choices=["WoL", "HotS", "LotV"], + help="the expansion level of the balance data export, one of 'WoL', 'HotS', or 'LotV'", + ) + parser.add_argument( + "build_version", + metavar="BUILD_VERSION", + type=int, + help="the build version of the balance data export", + ) + parser.add_argument( + "balance_data_path", + metavar="BALANCE_DATA_PATH", + type=str, + help="the path to the balance data export", + ) + parser.add_argument( + "project_path", + metavar="SC2READER_PROJECT_PATH", + type=str, + help="the path to the root of the sc2reader project directory", + ) args = parser.parse_args() - units, abilities, new_unit_lookup, new_ability_lookup = generate_build_data(args.balance_data_path) + units, abilities, new_unit_lookup, new_ability_lookup = generate_build_data( + args.balance_data_path + ) if not units or not abilities: parser.print_help() @@ -212,46 +255,67 @@ def main(): raise ValueError("No balance data found at provided balance data path.") - unit_lookup_path = os.path.join(args.project_path, 'sc2reader', 'data', 'unit_lookup.csv') - with open(unit_lookup_path, 'r') as file: - csv_reader = csv.reader(file, delimiter=',', lineterminator=os.linesep) - old_unit_lookup = collections.OrderedDict([(row[0], row[1]) for row in csv_reader if len(row) > 1]) - - ability_lookup_path = os.path.join(args.project_path, 'sc2reader', 'data', 'ability_lookup.csv') - with open(ability_lookup_path, 'r') as file: - csv_reader = csv.reader(file, delimiter=',', lineterminator=os.linesep) - old_ability_lookup = collections.OrderedDict([(row[0], row[1:]) for row in csv_reader if len(row) > 0]) + unit_lookup_path = os.path.join( + args.project_path, "sc2reader", "data", "unit_lookup.csv" + ) + with open(unit_lookup_path, "r") as file: + csv_reader = csv.reader(file, delimiter=",", lineterminator=os.linesep) + old_unit_lookup = collections.OrderedDict( + [(row[0], row[1]) for row in csv_reader if len(row) > 1] + ) + + ability_lookup_path = os.path.join( + args.project_path, "sc2reader", "data", "ability_lookup.csv" + ) + with open(ability_lookup_path, "r") as file: + csv_reader = csv.reader(file, delimiter=",", lineterminator=os.linesep) + old_ability_lookup = collections.OrderedDict( + [(row[0], row[1:]) for row in csv_reader if len(row) > 0] + ) if not old_unit_lookup or not old_ability_lookup: parser.print_help() print("\n") - raise ValueError("Could not find existing unit or ability lookups. Is the sc2reader project path correct?") + raise ValueError( + "Could not find existing unit or ability lookups. Is the sc2reader project path correct?" + ) unit_lookup, ability_lookup = combine_lookups( - old_unit_lookup, old_ability_lookup, new_unit_lookup, new_ability_lookup) + old_unit_lookup, old_ability_lookup, new_unit_lookup, new_ability_lookup + ) units_file_path = os.path.join( - args.project_path, 'sc2reader', 'data', args.expansion, '{}_units.csv'.format(args.build_version)) - with open(units_file_path, 'w') as file: - csv_writer = csv.writer(file, delimiter=',', lineterminator=os.linesep) + args.project_path, + "sc2reader", + "data", + args.expansion, + "{}_units.csv".format(args.build_version), + ) + with open(units_file_path, "w") as file: + csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) for unit_index, unit_name in units.items(): csv_writer.writerow([unit_index, unit_name]) abilities_file_path = os.path.join( - args.project_path, 'sc2reader', 'data', args.expansion, '{}_abilities.csv'.format(args.build_version)) - with open(abilities_file_path, 'w') as file: - csv_writer = csv.writer(file, delimiter=',', lineterminator=os.linesep) + args.project_path, + "sc2reader", + "data", + args.expansion, + "{}_abilities.csv".format(args.build_version), + ) + with open(abilities_file_path, "w") as file: + csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) for ability_index, ability_name in abilities.items(): csv_writer.writerow([ability_index, ability_name]) - with open(unit_lookup_path, 'w') as file: - csv_writer = csv.writer(file, delimiter=',', lineterminator=os.linesep) + with open(unit_lookup_path, "w") as file: + csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) for entry in unit_lookup.items(): csv_writer.writerow(list(entry)) - with open(ability_lookup_path, 'w') as file: - csv_writer = csv.writer(file, delimiter=',', lineterminator=os.linesep) + with open(ability_lookup_path, "w") as file: + csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) for ability_name, commands in ability_lookup.items(): csv_writer.writerow([ability_name] + commands) diff --git a/new_units.py b/new_units.py index e0d80118..dcc609a0 100644 --- a/new_units.py +++ b/new_units.py @@ -9,28 +9,30 @@ import sys UNIT_LOOKUP = dict() -for entry in pkgutil.get_data('sc2reader.data', 'unit_lookup.csv').split('\n'): - if not entry: continue - str_id, title = entry.strip().split(',') +for entry in pkgutil.get_data("sc2reader.data", "unit_lookup.csv").split("\n"): + if not entry: + continue + str_id, title = entry.strip().split(",") UNIT_LOOKUP[str_id] = title -with open(sys.argv[1],'r') as new_units: - for line in new_units: - new_unit_name = line.strip().split(',')[1] - if new_unit_name not in UNIT_LOOKUP: - print("{0},{1}".format(new_unit_name,new_unit_name)) +with open(sys.argv[1], "r") as new_units: + for line in new_units: + new_unit_name = line.strip().split(",")[1] + if new_unit_name not in UNIT_LOOKUP: + print("{0},{1}".format(new_unit_name, new_unit_name)) -print('') -print('') +print("") +print("") ABIL_LOOKUP = dict() -for entry in pkgutil.get_data('sc2reader.data', 'ability_lookup.csv').split('\n'): - if not entry: continue - str_id, abilities = entry.split(',',1) - ABIL_LOOKUP[str_id] = abilities.split(',') +for entry in pkgutil.get_data("sc2reader.data", "ability_lookup.csv").split("\n"): + if not entry: + continue + str_id, abilities = entry.split(",", 1) + ABIL_LOOKUP[str_id] = abilities.split(",") -with open(sys.argv[2], 'r') as new_abilities: - for line in new_abilities: - new_ability_name = line.strip().split(',')[1] - if new_ability_name not in ABIL_LOOKUP: - print("{0},{1}".format(new_ability_name,new_ability_name)) +with open(sys.argv[2], "r") as new_abilities: + for line in new_abilities: + new_ability_name = line.strip().split(",")[1] + if new_ability_name not in ABIL_LOOKUP: + print("{0},{1}".format(new_ability_name, new_ability_name)) diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 0bcf512a..68c429f1 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -99,8 +99,8 @@ def useDoubleCache(cache_dir, cache_max_size=0, **options): # Allow environment variables to activate caching -cache_dir = os.getenv('SC2READER_CACHE_DIR') -cache_max_size = os.getenv('SC2READER_CACHE_MAX_SIZE') +cache_dir = os.getenv("SC2READER_CACHE_DIR") +cache_max_size = os.getenv("SC2READER_CACHE_MAX_SIZE") if cache_dir and cache_max_size: useDoubleCache(cache_dir, cache_max_size) elif cache_dir: diff --git a/sc2reader/constants.py b/sc2reader/constants.py index aa3e0ade..1edfa70e 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -7,43 +7,34 @@ # The ??? means that I don't know what language it is. # If multiple languages use the same set they should be comma separated LOCALIZED_RACES = { - # enUS - 'Terran': 'Terran', - 'Protoss': 'Protoss', - 'Zerg': 'Zerg', - + "Terran": "Terran", + "Protoss": "Protoss", + "Zerg": "Zerg", # ruRU - 'Терран': 'Terran', - 'Протосс': 'Protoss', - 'Зерг': 'Zerg', - + "Терран": "Terran", + "Протосс": "Protoss", + "Зерг": "Zerg", # koKR - '테란': 'Terran', - '프로토스': 'Protoss', - '저그': 'Zerg', - + "테란": "Terran", + "프로토스": "Protoss", + "저그": "Zerg", # plPL - 'Terranie': 'Terran', - 'Protosi': 'Protoss', - 'Zergi': 'Zerg', - + "Terranie": "Terran", + "Protosi": "Protoss", + "Zergi": "Zerg", # zhCH - '人类': 'Terran', - '星灵': 'Protoss', - '异虫': 'Zerg', - + "人类": "Terran", + "星灵": "Protoss", + "异虫": "Zerg", # zhTW - '人類': 'Terran', - '神族': 'Protoss', - '蟲族': 'Zerg', - + "人類": "Terran", + "神族": "Protoss", + "蟲族": "Zerg", # ??? - 'Terrano': 'Terran', - + "Terrano": "Terran", # deDE - 'Terraner': 'Terran', - + "Terraner": "Terran", # esES - Spanish # esMX - Latin American # frFR - French - France @@ -51,131 +42,73 @@ # ptBR - Brazilian Portuguese } -MESSAGE_CODES = { - '0': 'All', - '2': 'Allies', - '128': 'Header', - '125': 'Ping', -} +MESSAGE_CODES = {"0": "All", "2": "Allies", "128": "Header", "125": "Ping"} GAME_SPEED_FACTOR = { - 'WoL': { - 'Slower': 0.6, - 'Slow': 0.8, - 'Normal': 1.0, - 'Fast': 1.2, - 'Faster': 1.4 - }, - 'HotS': { - 'Slower': 0.6, - 'Slow': 0.8, - 'Normal': 1.0, - 'Fast': 1.2, - 'Faster': 1.4 - }, - 'LotV': { - 'Slower': 0.2, - 'Slow': 0.4, - 'Normal': 0.6, - 'Fast': 0.8, - 'Faster': 1.0 - }, + "WoL": {"Slower": 0.6, "Slow": 0.8, "Normal": 1.0, "Fast": 1.2, "Faster": 1.4}, + "HotS": {"Slower": 0.6, "Slow": 0.8, "Normal": 1.0, "Fast": 1.2, "Faster": 1.4}, + "LotV": {"Slower": 0.2, "Slow": 0.4, "Normal": 0.6, "Fast": 0.8, "Faster": 1.0}, } GATEWAY_CODES = { - 'US': 'Americas', - 'KR': 'Asia', - 'EU': 'Europe', - 'SG': 'South East Asia', - 'XX': 'Public Test', + "US": "Americas", + "KR": "Asia", + "EU": "Europe", + "SG": "South East Asia", + "XX": "Public Test", } -GATEWAY_LOOKUP = { - 0: '', - 1: 'us', - 2: 'eu', - 3: 'kr', - 5: 'cn', - 6: 'sea', - 98: 'xx', -} +GATEWAY_LOOKUP = {0: "", 1: "us", 2: "eu", 3: "kr", 5: "cn", 6: "sea", 98: "xx"} COLOR_CODES = { - 'B4141E': 'Red', - '0042FF': 'Blue', - '1CA7EA': 'Teal', - 'EBE129': 'Yellow', - '540081': 'Purple', - 'FE8A0E': 'Orange', - '168000': 'Green', - 'CCA6FC': 'Light Pink', - '1F01C9': 'Violet', - '525494': 'Light Grey', - '106246': 'Dark Green', - '4E2A04': 'Brown', - '96FF91': 'Light Green', - '232323': 'Dark Grey', - 'E55BB0': 'Pink', - 'FFFFFF': 'White', - '000000': 'Black', + "B4141E": "Red", + "0042FF": "Blue", + "1CA7EA": "Teal", + "EBE129": "Yellow", + "540081": "Purple", + "FE8A0E": "Orange", + "168000": "Green", + "CCA6FC": "Light Pink", + "1F01C9": "Violet", + "525494": "Light Grey", + "106246": "Dark Green", + "4E2A04": "Brown", + "96FF91": "Light Green", + "232323": "Dark Grey", + "E55BB0": "Pink", + "FFFFFF": "White", + "000000": "Black", } COLOR_CODES_INV = dict(zip(COLOR_CODES.values(), COLOR_CODES.keys())) SUBREGIONS = { # United States - 'us': { - 1: 'us', - 2: 'la', - }, - + "us": {1: "us", 2: "la"}, # Europe - 'eu': { - 1: 'eu', - 2: 'ru', - }, - + "eu": {1: "eu", 2: "ru"}, # Korea - appear to both map to same place - 'kr': { - 1: 'kr', - 2: 'tw', - }, - + "kr": {1: "kr", 2: "tw"}, # Taiwan - appear to both map to same place - 'tw': { - 1: 'kr', - 2: 'tw', - }, - + "tw": {1: "kr", 2: "tw"}, # China - different url scheme (www.battlenet.com.cn)? - 'cn': { - 1: 'cn', - }, - + "cn": {1: "cn"}, # South East Asia - 'sea': { - 1: 'sea', - }, - + "sea": {1: "sea"}, # Singapore - 'sg': { - 1: 'sg', - }, - + "sg": {1: "sg"}, # Public Test - 'xx': { - 1: 'xx', - }, + "xx": {1: "xx"}, } import json import pkgutil -attributes_json = pkgutil.get_data('sc2reader.data', 'attributes.json').decode('utf8') +attributes_json = pkgutil.get_data("sc2reader.data", "attributes.json").decode("utf8") attributes_dict = json.loads(attributes_json) LOBBY_PROPERTIES = dict() -for key, value in attributes_dict.get('attributes', dict()).items(): +for key, value in attributes_dict.get("attributes", dict()).items(): LOBBY_PROPERTIES[int(key)] = value diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 7a3d4ecb..dd5ff207 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -12,33 +12,38 @@ from sc2reader.log_utils import loggable try: - cmp # Python 2 + cmp # Python 2 except NameError: cmp = lambda a, b: (a > b) - (a < b) # noqa Python 3 ABIL_LOOKUP = dict() -for entry in pkgutil.get_data('sc2reader.data', 'ability_lookup.csv').decode('utf8').split('\n'): +for entry in ( + pkgutil.get_data("sc2reader.data", "ability_lookup.csv").decode("utf8").split("\n") +): if not entry: continue - str_id, abilities = entry.split(',', 1) - ABIL_LOOKUP[str_id] = abilities.split(',') + str_id, abilities = entry.split(",", 1) + ABIL_LOOKUP[str_id] = abilities.split(",") UNIT_LOOKUP = dict() -for entry in pkgutil.get_data('sc2reader.data', 'unit_lookup.csv').decode('utf8').split('\n'): +for entry in ( + pkgutil.get_data("sc2reader.data", "unit_lookup.csv").decode("utf8").split("\n") +): if not entry: continue - str_id, title = entry.strip().split(',') + str_id, title = entry.strip().split(",") UNIT_LOOKUP[str_id] = title -unit_data = pkgutil.get_data('sc2reader.data', 'unit_info.json').decode('utf8') +unit_data = pkgutil.get_data("sc2reader.data", "unit_info.json").decode("utf8") unit_lookup = json.loads(unit_data) -command_data = pkgutil.get_data('sc2reader.data', 'train_commands.json').decode('utf8') +command_data = pkgutil.get_data("sc2reader.data", "train_commands.json").decode("utf8") train_commands = json.loads(command_data) class Unit(object): """Represents an in-game unit.""" + def __init__(self, unit_id): #: A reference to the player that currently owns this unit. Only available for 2.0.8+ replays. self.owner = None @@ -120,14 +125,18 @@ def is_type(self, unit_type, strict=True): else: if isinstance(unit_type, int): if self._type_class: - return unit_type in [utype.id for utype in self.type_history.values()] + return unit_type in [ + utype.id for utype in self.type_history.values() + ] else: return unit_type == 0 elif isinstance(unit_type, Unit): return unit_type in self.type_history.values() else: if self._type_class: - return unit_type in [utype.str_id for utype in self.type_history.values()] + return unit_type in [ + utype.str_id for utype in self.type_history.values() + ] else: return unit_type is None @@ -213,8 +222,21 @@ def __repr__(self): class UnitType(object): """ Represents an in game unit type """ - def __init__(self, type_id, str_id=None, name=None, title=None, race=None, minerals=0, - vespene=0, supply=0, is_building=False, is_worker=False, is_army=False): + + def __init__( + self, + type_id, + str_id=None, + name=None, + title=None, + race=None, + minerals=0, + vespene=0, + supply=0, + is_building=False, + is_worker=False, + is_army=False, + ): #: The internal integer id representing this unit type self.id = type_id @@ -251,7 +273,10 @@ def __init__(self, type_id, str_id=None, name=None, title=None, race=None, miner class Ability(object): """ Represents an in-game ability """ - def __init__(self, id, name=None, title=None, is_build=False, build_time=0, build_unit=None): + + def __init__( + self, id, name=None, title=None, is_build=False, build_time=0, build_unit=None + ): #: The internal integer id representing this ability. self.id = id @@ -283,6 +308,7 @@ class Build(object): All build data is valid for standard games only. For arcade maps milage may vary. """ + def __init__(self, build_id): #: The integer id of the build self.id = build_id @@ -315,21 +341,46 @@ def change_type(self, unit, new_type, frame): unit_type = self.units[new_type] unit.set_type(unit_type, frame) else: - self.logger.error("Unable to change type of {0} to {1} [frame {2}]; unit type not found in build {3}".format(unit, new_type, frame, self.id)) + self.logger.error( + "Unable to change type of {0} to {1} [frame {2}]; unit type not found in build {3}".format( + unit, new_type, frame, self.id + ) + ) - def add_ability(self, ability_id, name, title=None, is_build=False, build_time=None, build_unit=None): + def add_ability( + self, + ability_id, + name, + title=None, + is_build=False, + build_time=None, + build_unit=None, + ): ability = Ability( ability_id, name=name, title=title or name, is_build=is_build, build_time=build_time, - build_unit=build_unit + build_unit=build_unit, ) setattr(self, name, ability) self.abilities[ability_id] = ability - def add_unit_type(self, type_id, str_id, name, title=None, race='Neutral', minerals=0, vespene=0, supply=0, is_building=False, is_worker=False, is_army=False): + def add_unit_type( + self, + type_id, + str_id, + name, + title=None, + race="Neutral", + minerals=0, + vespene=0, + supply=0, + is_building=False, + is_worker=False, + is_army=False, + ): unit = UnitType( type_id, str_id=str_id, @@ -351,40 +402,46 @@ def add_unit_type(self, type_id, str_id, name, title=None, race='Neutral', miner def load_build(expansion, version): build = Build(version) - unit_file = '{0}/{1}_units.csv'.format(expansion, version) - for entry in pkgutil.get_data('sc2reader.data', unit_file).decode('utf8').split('\n'): + unit_file = "{0}/{1}_units.csv".format(expansion, version) + for entry in ( + pkgutil.get_data("sc2reader.data", unit_file).decode("utf8").split("\n") + ): if not entry: continue - int_id, str_id = entry.strip().split(',') + int_id, str_id = entry.strip().split(",") unit_type = int(int_id, 10) title = UNIT_LOOKUP[str_id] values = dict(type_id=unit_type, str_id=str_id, name=title) - for race in ('Protoss', 'Terran', 'Zerg'): + for race in ("Protoss", "Terran", "Zerg"): if title.lower() in unit_lookup[race]: values.update(unit_lookup[race][title.lower()]) - values['race'] = race + values["race"] = race break build.add_unit_type(**values) - abil_file = '{0}/{1}_abilities.csv'.format(expansion, version) - build.add_ability(ability_id=0, name='RightClick', title='Right Click') - for entry in pkgutil.get_data('sc2reader.data', abil_file).decode('utf8').split('\n'): + abil_file = "{0}/{1}_abilities.csv".format(expansion, version) + build.add_ability(ability_id=0, name="RightClick", title="Right Click") + for entry in ( + pkgutil.get_data("sc2reader.data", abil_file).decode("utf8").split("\n") + ): if not entry: continue - int_id_base, str_id = entry.strip().split(',') + int_id_base, str_id = entry.strip().split(",") int_id_base = int(int_id_base, 10) << 5 abils = ABIL_LOOKUP[str_id] - real_abils = [(i, abil) for i, abil in enumerate(abils) if abil.strip() != ''] + real_abils = [(i, abil) for i, abil in enumerate(abils) if abil.strip() != ""] if len(real_abils) == 0: real_abils = [(0, str_id)] for index, ability_name in real_abils: - unit_name, build_time = train_commands.get(ability_name, ('', 0)) - if 'Hallucinated' in unit_name: # Not really sure how to handle hallucinations + unit_name, build_time = train_commands.get(ability_name, ("", 0)) + if ( + "Hallucinated" in unit_name + ): # Not really sure how to handle hallucinations unit_name = unit_name[12:] build.add_ability( @@ -392,26 +449,27 @@ def load_build(expansion, version): name=ability_name, is_build=bool(unit_name), build_unit=getattr(build, unit_name, None), - build_time=build_time + build_time=build_time, ) return build + # Load the WoL Data wol_builds = dict() -for version in ('16117', '17326', '18092', '19458', '22612', '24944'): - wol_builds[version] = load_build('WoL', version) +for version in ("16117", "17326", "18092", "19458", "22612", "24944"): + wol_builds[version] = load_build("WoL", version) # Load HotS Data hots_builds = dict() -for version in ('base', '23925', '24247', '24764'): - hots_builds[version] = load_build('HotS', version) -hots_builds['38215'] = load_build('LotV', 'base') -hots_builds['38215'].id = '38215' +for version in ("base", "23925", "24247", "24764"): + hots_builds[version] = load_build("HotS", version) +hots_builds["38215"] = load_build("LotV", "base") +hots_builds["38215"].id = "38215" # Load LotV Data lotv_builds = dict() -for version in ('base', '44401', '47185', '48258', '53644', '54724', '59587', '70154'): - lotv_builds[version] = load_build('LotV', version) +for version in ("base", "44401", "47185", "48258", "53644", "54724", "59587", "70154"): + lotv_builds[version] = load_build("LotV", version) -datapacks = builds = {'WoL': wol_builds, 'HotS': hots_builds, 'LotV': lotv_builds} +datapacks = builds = {"WoL": wol_builds, "HotS": hots_builds, "LotV": lotv_builds} diff --git a/sc2reader/data/create_lookup.py b/sc2reader/data/create_lookup.py index 73623836..2ffa2560 100755 --- a/sc2reader/data/create_lookup.py +++ b/sc2reader/data/create_lookup.py @@ -1,14 +1,14 @@ abilities = dict() -with open('hots_abilities.csv', 'r') as f: +with open("hots_abilities.csv", "r") as f: for line in f: - num, ability = line.strip('\r\n ').split(',') - abilities[ability] = [""]*32 + num, ability = line.strip("\r\n ").split(",") + abilities[ability] = [""] * 32 -with open('command_lookup.csv', 'r') as f: +with open("command_lookup.csv", "r") as f: for line in f: - ability, commands = line.strip('\r\n ').split('|', 1) - abilities[ability] = commands.split('|') + ability, commands = line.strip("\r\n ").split("|", 1) + abilities[ability] = commands.split("|") -with open('new_lookup.csv', 'w') as out: +with open("new_lookup.csv", "w") as out: for ability, commands in sorted(abilities.items()): - out.write(','.join([ability]+commands)+'\n') + out.write(",".join([ability] + commands) + "\n") diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index b2ff35f9..78372e8f 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -34,7 +34,7 @@ def __init__(self, contents, endian): """ Accepts both strings and files implementing ``read()`` and decodes them in the specified endian format. """ - if hasattr(contents, 'read'): + if hasattr(contents, "read"): self._contents = contents.read() else: self._contents = contents @@ -49,18 +49,21 @@ def __init__(self, contents, endian): # decode the endian value if necessary self.endian = endian.lower() - if self.endian.lower() == 'little': + if self.endian.lower() == "little": self.endian = "<" - elif self.endian.lower() == 'big': + elif self.endian.lower() == "big": self.endian = ">" - elif self.endian not in ('<', '>'): - raise ValueError("Endian must be one of 'little', '<', 'big', or '>' but was: "+self.endian) + elif self.endian not in ("<", ">"): + raise ValueError( + "Endian must be one of 'little', '<', 'big', or '>' but was: " + + self.endian + ) # Pre-compiling - self._unpack_int = struct.Struct(str(self.endian+'I')).unpack - self._unpack_short = struct.Struct(str(self.endian+'H')).unpack - self._unpack_longlong = struct.Struct(str(self.endian+'Q')).unpack - self._unpack_bytes = lambda bytes: bytes if self.endian == '>' else bytes[::-1] + self._unpack_int = struct.Struct(str(self.endian + "I")).unpack + self._unpack_short = struct.Struct(str(self.endian + "H")).unpack + self._unpack_longlong = struct.Struct(str(self.endian + "Q")).unpack + self._unpack_bytes = lambda bytes: bytes if self.endian == ">" else bytes[::-1] def done(self): """ Returns true when all bytes have been decoded """ @@ -73,7 +76,7 @@ def read_range(self, start, end): def peek(self, count): """ Returns the raw byte string for the next ``count`` bytes """ start = self.tell() - return self._contents[start:start+count] + return self._contents[start : start + count] def read_uint8(self): """ Returns the next byte as an unsigned integer """ @@ -97,17 +100,17 @@ def read_bytes(self, count): def read_uint(self, count): """ Returns the next ``count`` bytes as an unsigned integer """ - unpack = struct.Struct(str(self.endian+'B'*count)).unpack + unpack = struct.Struct(str(self.endian + "B" * count)).unpack uint = 0 for byte in unpack(self.read(count)): uint = uint << 8 | byte return uint - def read_string(self, count, encoding='utf8'): + def read_string(self, count, encoding="utf8"): """ Read a string in given encoding (default utf8) that is ``count`` bytes long """ return self.read_bytes(count).decode(encoding) - def read_cstring(self, encoding='utf8'): + def read_cstring(self, encoding="utf8"): """ Read a NULL byte terminated character string decoded with given encoding (default utf8). Ignores endian. """ cstring = BytesIO() while True: @@ -128,6 +131,7 @@ class BitPackedDecoder(object): bits and not in bytes. """ + #: The ByteDecoder used internally to read byte #: aligned values. _buffer = None @@ -153,7 +157,7 @@ class BitPackedDecoder(object): _bit_masks = list(zip(_lo_masks, _hi_masks)) def __init__(self, contents): - self._buffer = ByteDecoder(contents, endian='BIG') + self._buffer = ByteDecoder(contents, endian="BIG") # Partially expose the ByteBuffer interface self.length = self._buffer.length @@ -194,8 +198,8 @@ def read_uint16(self): if self._bit_shift != 0: lo_mask, hi_mask = self._bit_masks[self._bit_shift] hi_bits = (self._next_byte & hi_mask) << 8 - mi_bits = (data & 0xFF00) >> (8-self._bit_shift) - lo_bits = (data & lo_mask) + mi_bits = (data & 0xFF00) >> (8 - self._bit_shift) + lo_bits = data & lo_mask self._next_byte = data & 0xFF data = hi_bits | mi_bits | lo_bits @@ -208,8 +212,8 @@ def read_uint32(self): if self._bit_shift != 0: lo_mask, hi_mask = self._bit_masks[self._bit_shift] hi_bits = (self._next_byte & hi_mask) << 24 - mi_bits = (data & 0xFFFFFF00) >> (8-self._bit_shift) - lo_bits = (data & lo_mask) + mi_bits = (data & 0xFFFFFF00) >> (8 - self._bit_shift) + lo_bits = data & lo_mask self._next_byte = data & 0xFF data = hi_bits | mi_bits | lo_bits @@ -222,8 +226,8 @@ def read_uint64(self): if self._bit_shift != 0: lo_mask, hi_mask = self._bit_masks[self._bit_shift] hi_bits = (self._next_byte & hi_mask) << 56 - mi_bits = (data & 0xFFFFFFFFFFFFFF00) >> (8-self._bit_shift) - lo_bits = (data & lo_mask) + mi_bits = (data & 0xFFFFFFFFFFFFFF00) >> (8 - self._bit_shift) + lo_bits = data & lo_mask self._next_byte = data & 0xFF data = hi_bits | mi_bits | lo_bits @@ -246,7 +250,7 @@ def read_aligned_bytes(self, count): self.byte_align() return self._buffer.read_bytes(count) - def read_aligned_string(self, count, encoding='utf8'): + def read_aligned_string(self, count, encoding="utf8"): """ Skips to the beginning of the next byte and returns the next ``count`` bytes decoded with encoding (default utf8) """ self.byte_align() return self._buffer.read_string(count, encoding) @@ -259,8 +263,10 @@ def read_bytes(self, count): temp_buffer = BytesIO() prev_byte = self._next_byte lo_mask, hi_mask = self._bit_masks[self._bit_shift] - for next_byte in struct.unpack(str("B")*count, data): - temp_buffer.write(struct.pack(str("B"), prev_byte & hi_mask | next_byte & lo_mask)) + for next_byte in struct.unpack(str("B") * count, data): + temp_buffer.write( + struct.pack(str("B"), prev_byte & hi_mask | next_byte & lo_mask) + ) prev_byte = next_byte self._next_byte = prev_byte @@ -277,7 +283,7 @@ def read_bits(self, count): # If we've got a byte in progress use it first if bit_shift != 0: - bits_left = 8-bit_shift + bits_left = 8 - bit_shift if bits_left < bits: bits -= bits_left @@ -291,7 +297,7 @@ def read_bits(self, count): # Then grab any additional whole bytes as needed if bits >= 8: - bytes = int(bits/8) + bytes = int(bits / 8) if bytes == 1: bits -= 8 @@ -306,7 +312,7 @@ def read_bits(self, count): result |= self._buffer.read_uint32() << bits else: - for byte in struct.unpack(str("B")*bytes, self._read(bytes)): + for byte in struct.unpack(str("B") * bytes, self._read(bytes)): bits -= 8 result |= byte << bits @@ -359,7 +365,9 @@ def read_struct(self, datatype=None): elif datatype == 0x05: # Struct entries = self.read_vint() - data = dict([(self.read_vint(), self.read_struct()) for i in range(entries)]) + data = dict( + [(self.read_vint(), self.read_struct()) for i in range(entries)] + ) elif datatype == 0x06: # u8 data = ord(self._buffer.read(1)) diff --git a/sc2reader/engine/__init__.py b/sc2reader/engine/__init__.py index cd72973c..b3b8ad87 100644 --- a/sc2reader/engine/__init__.py +++ b/sc2reader/engine/__init__.py @@ -15,6 +15,7 @@ def setGameEngine(engine): module.register_plugin = engine.register_plugin module.register_plugins = engine.register_plugins + _default_engine = GameEngine() _default_engine.register_plugin(plugins.GameHeartNormalizer()) _default_engine.register_plugin(plugins.ContextLoader()) diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index 65caced1..e091fa3a 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -112,6 +112,7 @@ def handleCommandEvent(self, event, replay): message = "RequiredPlugin failed with code: {0}. Cannot continue.".format(code) yield PluginExit(self, code=1, details=dict(msg=message)) """ + def __init__(self, plugins=[]): self._plugins = list() self.register_plugins(*plugins) @@ -124,7 +125,7 @@ def register_plugins(self, *plugins): self.register_plugin(plugin) def plugins(self): - return self._plugins + return self._plugins def run(self, replay): # A map of [event.name] => event handlers in plugin registration order @@ -152,7 +153,7 @@ def run(self, replay): while len(event_queue) > 0: event = event_queue.popleft() - if event.name == 'PluginExit': + if event.name == "PluginExit": # Remove the plugin and reset the handlers. plugins.remove(event.plugin) handlers.clear() @@ -174,18 +175,20 @@ def run(self, replay): new_events = collections.deque() for event_handler in event_handlers: try: - for new_event in (event_handler(event, replay) or []): - if new_event.name == 'PluginExit': + for new_event in event_handler(event, replay) or []: + if new_event.name == "PluginExit": new_events.append(new_event) break else: new_events.appendleft(new_event) except Exception as e: - if event_handler.__self__.name in ['ContextLoader']: + if event_handler.__self__.name in ["ContextLoader"]: # Certain built in plugins should probably still cause total failure raise # Maybe?? else: - new_event = PluginExit(event_handler.__self__, code=1, details=dict(error=e)) + new_event = PluginExit( + event_handler.__self__, code=1, details=dict(error=e) + ) new_events.append(new_event) event_queue.extendleft(new_events) @@ -195,22 +198,26 @@ def run(self, replay): replay.plugin_result[plugin.name] = (0, dict()) def _get_event_handlers(self, event, plugins): - return sum([self._get_plugin_event_handlers(plugin, event) for plugin in plugins], []) + return sum( + [self._get_plugin_event_handlers(plugin, event) for plugin in plugins], [] + ) def _get_plugin_event_handlers(self, plugin, event): handlers = list() - if isinstance(event, Event) and hasattr(plugin, 'handleEvent'): - handlers.append(getattr(plugin, 'handleEvent', None)) - if isinstance(event, MessageEvent) and hasattr(plugin, 'handleMessageEvent'): - handlers.append(getattr(plugin, 'handleMessageEvent', None)) - if isinstance(event, GameEvent) and hasattr(plugin, 'handleGameEvent'): - handlers.append(getattr(plugin, 'handleGameEvent', None)) - if isinstance(event, TrackerEvent) and hasattr(plugin, 'handleTrackerEvent'): - handlers.append(getattr(plugin, 'handleTrackerEvent', None)) - if isinstance(event, CommandEvent) and hasattr(plugin, 'handleCommandEvent'): - handlers.append(getattr(plugin, 'handleCommandEvent', None)) - if isinstance(event, ControlGroupEvent) and hasattr(plugin, 'handleControlGroupEvent'): - handlers.append(getattr(plugin, 'handleControlGroupEvent', None)) - if hasattr(plugin, 'handle'+event.name): - handlers.append(getattr(plugin, 'handle'+event.name, None)) + if isinstance(event, Event) and hasattr(plugin, "handleEvent"): + handlers.append(getattr(plugin, "handleEvent", None)) + if isinstance(event, MessageEvent) and hasattr(plugin, "handleMessageEvent"): + handlers.append(getattr(plugin, "handleMessageEvent", None)) + if isinstance(event, GameEvent) and hasattr(plugin, "handleGameEvent"): + handlers.append(getattr(plugin, "handleGameEvent", None)) + if isinstance(event, TrackerEvent) and hasattr(plugin, "handleTrackerEvent"): + handlers.append(getattr(plugin, "handleTrackerEvent", None)) + if isinstance(event, CommandEvent) and hasattr(plugin, "handleCommandEvent"): + handlers.append(getattr(plugin, "handleCommandEvent", None)) + if isinstance(event, ControlGroupEvent) and hasattr( + plugin, "handleControlGroupEvent" + ): + handlers.append(getattr(plugin, "handleControlGroupEvent", None)) + if hasattr(plugin, "handle" + event.name): + handlers.append(getattr(plugin, "handle" + event.name, None)) return handlers diff --git a/sc2reader/engine/events.py b/sc2reader/engine/events.py index 44387e52..8857a4b1 100644 --- a/sc2reader/engine/events.py +++ b/sc2reader/engine/events.py @@ -3,15 +3,15 @@ class InitGameEvent(object): - name = 'InitGame' + name = "InitGame" class EndGameEvent(object): - name = 'EndGame' + name = "EndGame" class PluginExit(object): - name = 'PluginExit' + name = "PluginExit" def __init__(self, plugin, code=0, details=None): self.plugin = plugin diff --git a/sc2reader/engine/plugins/__init__.py b/sc2reader/engine/plugins/__init__.py index 15e77ff3..7ddff085 100644 --- a/sc2reader/engine/plugins/__init__.py +++ b/sc2reader/engine/plugins/__init__.py @@ -7,4 +7,3 @@ from sc2reader.engine.plugins.supply import SupplyTracker from sc2reader.engine.plugins.creeptracker import CreepTracker from sc2reader.engine.plugins.gameheart import GameHeartNormalizer - diff --git a/sc2reader/engine/plugins/apm.py b/sc2reader/engine/plugins/apm.py index 526bf028..ec6fee7c 100644 --- a/sc2reader/engine/plugins/apm.py +++ b/sc2reader/engine/plugins/apm.py @@ -15,7 +15,8 @@ class APMTracker(object): APM is 0 for games under 1 minute in length. """ - name = 'APMTracker' + + name = "APMTracker" def handleInitGame(self, event, replay): for human in replay.humans: @@ -25,15 +26,15 @@ def handleInitGame(self, event, replay): def handleControlGroupEvent(self, event, replay): event.player.aps[event.second] += 1.4 - event.player.apm[int(event.second/60)] += 1.4 + event.player.apm[int(event.second / 60)] += 1.4 def handleSelectionEvent(self, event, replay): event.player.aps[event.second] += 1.4 - event.player.apm[int(event.second/60)] += 1.4 + event.player.apm[int(event.second / 60)] += 1.4 def handleCommandEvent(self, event, replay): event.player.aps[event.second] += 1.4 - event.player.apm[int(event.second/60)] += 1.4 + event.player.apm[int(event.second / 60)] += 1.4 def handlePlayerLeaveEvent(self, event, replay): event.player.seconds_played = event.second @@ -41,6 +42,8 @@ def handlePlayerLeaveEvent(self, event, replay): def handleEndGame(self, event, replay): for human in replay.humans: if len(human.apm.keys()) > 0: - human.avg_apm = sum(human.aps.values())/float(human.seconds_played)*60 + human.avg_apm = ( + sum(human.aps.values()) / float(human.seconds_played) * 60 + ) else: human.avg_apm = 0 diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 6b5362bc..dd846634 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -8,7 +8,7 @@ @loggable class ContextLoader(object): - name = 'ContextLoader' + name = "ContextLoader" def handleInitGame(self, event, replay): replay.units = set() @@ -32,17 +32,26 @@ def handleCommandEvent(self, event, replay): if event.player.pid in self.last_target_ability_event: del self.last_target_ability_event[event.player.pid] - if not getattr(replay, 'marked_error', None): + if not getattr(replay, "marked_error", None): replay.marked_error = True event.logger.error(replay.filename) event.logger.error("Release String: " + replay.release_string) for player in replay.players: try: - event.logger.error("\t"+unicode(player).encode('ascii', 'ignore')) + event.logger.error( + "\t" + unicode(player).encode("ascii", "ignore") + ) except NameError: # unicode() is not defined in Python 3 - event.logger.error("\t"+player.__str__()) + event.logger.error("\t" + player.__str__()) - self.logger.error("{0}\t{1}\tMissing ability {2:X} from {3}".format(event.frame, event.player.name, event.ability_id, replay.datapack.__class__.__name__)) + self.logger.error( + "{0}\t{1}\tMissing ability {2:X} from {3}".format( + event.frame, + event.player.name, + event.ability_id, + replay.datapack.__class__.__name__, + ) + ) else: event.ability = replay.datapack.abilities[event.ability_id] @@ -61,13 +70,19 @@ def handleTargetUnitCommandEvent(self, event, replay): if event.target_unit_id in replay.objects: event.target = replay.objects[event.target_unit_id] - if not replay.tracker_events and not event.target.is_type(event.target_unit_type): - replay.datapack.change_type(event.target, event.target_unit_type, event.frame) + if not replay.tracker_events and not event.target.is_type( + event.target_unit_type + ): + replay.datapack.change_type( + event.target, event.target_unit_type, event.frame + ) else: # Often when the target_unit_id is not in replay.objects it is 0 because it # is a target building/destructable hidden by fog of war. Perhaps we can match # it through the fog using location? - unit = replay.datapack.create_unit(event.target_unit_id, event.target_unit_type, event.frame) + unit = replay.datapack.create_unit( + event.target_unit_id, event.target_unit_type, event.frame + ) event.target = unit replay.objects[event.target_unit_id] = unit @@ -78,7 +93,9 @@ def handleUpdateTargetUnitCommandEvent(self, event, replay): if event.player.pid in self.last_target_ability_event: # store corresponding TargetUnitCommandEvent data in this event # currently using for *MacroTracker only, so only need ability name - event.ability_name = self.last_target_ability_event[event.player.pid].ability_name + event.ability_name = self.last_target_ability_event[ + event.player.pid + ].ability_name self.handleTargetUnitCommandEvent(event, replay) @@ -88,14 +105,23 @@ def handleSelectionEvent(self, event, replay): units = list() # TODO: Blizzard calls these subgroup flags but that doesn't make sense right now - for (unit_id, unit_type, subgroup_flags, intra_subgroup_flags) in event.new_unit_info: + for ( + unit_id, + unit_type, + subgroup_flags, + intra_subgroup_flags, + ) in event.new_unit_info: # If we don't have access to tracker events, use selection events to create # new units and track unit type changes. It won't be perfect, but it is better # than nothing. if not replay.tracker_events: # Starting at 23925 the default viking mode is assault. Most people expect # the default viking mode to be figher so fudge it a bit here. - if replay.versions[1] == 2 and replay.build >= 23925 and unit_type == 71: + if ( + replay.versions[1] == 2 + and replay.build >= 23925 + and unit_type == 71 + ): unit_type = 72 if unit_id in replay.objects: @@ -109,13 +135,12 @@ def handleSelectionEvent(self, event, replay): # If we have tracker events, the unit must already exist and must already # have the correct unit type. elif unit_id in replay.objects: - unit = replay.objects[unit_id] + unit = replay.objects[unit_id] # Except when it doesn't. else: - unit = replay.datapack.create_unit(unit_id, unit_type, event.frame) - replay.objects[unit_id] = unit - + unit = replay.datapack.create_unit(unit_id, unit_type, event.frame) + replay.objects[unit_id] = unit # Selection events hold flags on units (like hallucination) unit.apply_flags(intra_subgroup_flags) @@ -148,7 +173,9 @@ def handleUnitBornEvent(self, event, replay): event.unit = replay.objects[event.unit_id] else: # TODO: How to tell if something is hallucination? - event.unit = replay.datapack.create_unit(event.unit_id, event.unit_type_name, event.frame) + event.unit = replay.datapack.create_unit( + event.unit_id, event.unit_type_name, event.frame + ) replay.objects[event.unit_id] = event.unit replay.active_units[event.unit_id_index] = event.unit @@ -171,9 +198,17 @@ def handleUnitDiedEvent(self, event, replay): if event.unit_id_index in replay.active_units: del replay.active_units[event.unit_id_index] else: - self.logger.error("Unable to delete unit index {0} at {1} [{2}], index not active.".format(event.killer_pid, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unable to delete unit index {0} at {1} [{2}], index not active.".format( + event.killer_pid, Length(seconds=event.second), event.frame + ) + ) else: - self.logger.error("Unit {0} died at {1} [{2}] before it was born!".format(event.unit_id, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unit {0} died at {1} [{2}] before it was born!".format( + event.unit_id, Length(seconds=event.second), event.frame + ) + ) if event.killing_player_id in replay.player: event.killing_player = event.killer = replay.player[event.killing_player_id] @@ -181,7 +216,11 @@ def handleUnitDiedEvent(self, event, replay): event.unit.killing_player = event.unit.killed_by = event.killing_player event.killing_player.killed_units.append(event.unit) elif event.killing_player_id: - self.logger.error("Unknown killing player id {0} at {1} [{2}]".format(event.killing_player_id, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unknown killing player id {0} at {1} [{2}]".format( + event.killing_player_id, Length(seconds=event.second), event.frame + ) + ) if event.killing_unit_id in replay.objects: event.killing_unit = replay.objects[event.killing_unit_id] @@ -189,7 +228,11 @@ def handleUnitDiedEvent(self, event, replay): event.unit.killing_unit = event.killing_unit event.killing_unit.killed_units.append(event.unit) elif event.killing_unit_id: - self.logger.error("Unknown killing unit id {0} at {1} [{2}]".format(event.killing_unit_id, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unknown killing unit id {0} at {1} [{2}]".format( + event.killing_unit_id, Length(seconds=event.second), event.frame + ) + ) def handleUnitOwnerChangeEvent(self, event, replay): self.load_tracker_controller(event, replay) @@ -201,7 +244,11 @@ def handleUnitOwnerChangeEvent(self, event, replay): if event.unit_id in replay.objects: event.unit = replay.objects[event.unit_id] else: - self.logger.error("Unit {0} owner changed at {1} [{2}] before it was born!".format(event.unit_id, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unit {0} owner changed at {1} [{2}] before it was born!".format( + event.unit_id, Length(seconds=event.second), event.frame + ) + ) if event.unit_upkeeper: if event.unit.owner: @@ -217,7 +264,11 @@ def handleUnitTypeChangeEvent(self, event, replay): event.unit = replay.objects[event.unit_id] replay.datapack.change_type(event.unit, event.unit_type_name, event.frame) else: - self.logger.error("Unit {0} type changed at {1} [{2}] before it was born!".format(event.unit_id, Length(seconds=event.second))) + self.logger.error( + "Unit {0} type changed at {1} [{2}] before it was born!".format( + event.unit_id, Length(seconds=event.second) + ) + ) def handleUpgradeCompleteEvent(self, event, replay): self.load_tracker_player(event, replay) @@ -235,7 +286,9 @@ def handleUnitInitEvent(self, event, replay): event.unit = replay.objects[event.unit_id] else: # TODO: How to tell if something is hallucination? - event.unit = replay.datapack.create_unit(event.unit_id, event.unit_type_name, event.frame) + event.unit = replay.datapack.create_unit( + event.unit_id, event.unit_type_name, event.frame + ) replay.objects[event.unit_id] = event.unit replay.active_units[event.unit_id_index] = event.unit @@ -254,7 +307,11 @@ def handleUnitDoneEvent(self, event, replay): event.unit = replay.objects[event.unit_id] event.unit.finished_at = event.frame else: - self.logger.error("Unit {0} done at {1} [{2}] before it was started!".format(event.killer_pid, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unit {0} done at {1} [{2}] before it was started!".format( + event.killer_pid, Length(seconds=event.second), event.frame + ) + ) def handleUnitPositionsEvent(self, event, replay): if not replay.datapack: @@ -266,15 +323,28 @@ def handleUnitPositionsEvent(self, event, replay): unit.location = (x, y) event.units[unit] = unit.location else: - self.logger.error("Unit at active_unit index {0} moved at {1} [{2}] but it doesn't exist!".format(event.killer_pid, Length(seconds=event.second), event.frame)) + self.logger.error( + "Unit at active_unit index {0} moved at {1} [{2}] but it doesn't exist!".format( + event.killer_pid, Length(seconds=event.second), event.frame + ) + ) def load_message_game_player(self, event, replay): - if replay.versions[1] == 1 or (replay.versions[1] == 2 and replay.build < 24247): + if replay.versions[1] == 1 or ( + replay.versions[1] == 2 and replay.build < 24247 + ): if event.pid in replay.entity: event.player = replay.entity[event.pid] event.player.events.append(event) elif event.pid != 16: - self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frame)) + self.logger.error( + "Bad pid ({0}) for event {1} at {2} [{3}].".format( + event.pid, + event.__class__, + Length(seconds=event.second), + event.frame, + ) + ) else: pass # This is a global event @@ -283,7 +353,14 @@ def load_message_game_player(self, event, replay): event.player = replay.human[event.pid] event.player.events.append(event) elif event.pid != 16: - self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frame)) + self.logger.error( + "Bad pid ({0}) for event {1} at {2} [{3}].".format( + event.pid, + event.__class__, + Length(seconds=event.second), + event.frame, + ) + ) else: pass # This is a global event @@ -291,16 +368,37 @@ def load_tracker_player(self, event, replay): if event.pid in replay.entity: event.player = replay.entity[event.pid] else: - self.logger.error("Bad pid ({0}) for event {1} at {2} [{3}].".format(event.pid, event.__class__, Length(seconds=event.second), event.frame)) + self.logger.error( + "Bad pid ({0}) for event {1} at {2} [{3}].".format( + event.pid, + event.__class__, + Length(seconds=event.second), + event.frame, + ) + ) def load_tracker_upkeeper(self, event, replay): if event.upkeep_pid in replay.entity: event.unit_upkeeper = replay.entity[event.upkeep_pid] elif event.upkeep_pid != 0: - self.logger.error("Bad upkeep_pid ({0}) for event {1} at {2} [{3}].".format(event.upkeep_pid, event.__class__, Length(seconds=event.second), event.frame)) + self.logger.error( + "Bad upkeep_pid ({0}) for event {1} at {2} [{3}].".format( + event.upkeep_pid, + event.__class__, + Length(seconds=event.second), + event.frame, + ) + ) def load_tracker_controller(self, event, replay): if event.control_pid in replay.entity: event.unit_controller = replay.entity[event.control_pid] elif event.control_pid != 0: - self.logger.error("Bad control_pid ({0}) for event {1} at {2} [{3}].".format(event.control_pid, event.__class__, Length(seconds=event.second), event.frame)) + self.logger.error( + "Bad control_pid ({0}) for event {1} at {2} [{3}].".format( + event.control_pid, + event.__class__, + Length(seconds=event.second), + event.frame, + ) + ) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 7f35f868..98f0148f 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -22,80 +22,94 @@ # The creep tracker plugin class CreepTracker(object): - ''' + """ The Creep tracker populates player.max_creep_spread and player.creep_spread by minute This uses the creep_tracker class to calculate the features - ''' - name = 'CreepTracker' + """ + + name = "CreepTracker" def handleInitGame(self, event, replay): - try: - if len( replay.tracker_events) ==0 : - return - if replay.map is None: - replay.load_map() - self.creepTracker = creep_tracker(replay) - for player in replay.players: - if player.play_race[0] == 'Z': - self.creepTracker.init_cgu_lists(player.pid) - except Exception as e: - print("Whoa! {}".format(e)) - pass + try: + if len(replay.tracker_events) == 0: + return + if replay.map is None: + replay.load_map() + self.creepTracker = creep_tracker(replay) + for player in replay.players: + if player.play_race[0] == "Z": + self.creepTracker.init_cgu_lists(player.pid) + except Exception as e: + print("Whoa! {}".format(e)) + pass def handleUnitDiedEvent(self, event, replay): - try: - self.creepTracker.remove_from_list(event.unit_id,event.second) - except Exception as e: - print("Whoa! {}".format(e)) - pass - + try: + self.creepTracker.remove_from_list(event.unit_id, event.second) + except Exception as e: + print("Whoa! {}".format(e)) + pass - def handleUnitInitEvent(self,event,replay): - try: - if event.unit_type_name in ["CreepTumor", "Hatchery","NydusCanal"] : - self.creepTracker.add_to_list(event.control_pid,event.unit_id,\ - (event.x, event.y), event.unit_type_name,event.second) - except Exception as e: - print("Whoa! {}".format(e)) - pass + def handleUnitInitEvent(self, event, replay): + try: + if event.unit_type_name in ["CreepTumor", "Hatchery", "NydusCanal"]: + self.creepTracker.add_to_list( + event.control_pid, + event.unit_id, + (event.x, event.y), + event.unit_type_name, + event.second, + ) + except Exception as e: + print("Whoa! {}".format(e)) + pass - def handleUnitBornEvent(self,event,replay): - try: - if event.unit_type_name== "Hatchery": - self.creepTracker.add_to_list(event.control_pid, event.unit_id,\ - (event.x,event.y),event.unit_type_name,event.second) - except Exception as e: - print("Whoa! {}".format(e)) - pass + def handleUnitBornEvent(self, event, replay): + try: + if event.unit_type_name == "Hatchery": + self.creepTracker.add_to_list( + event.control_pid, + event.unit_id, + (event.x, event.y), + event.unit_type_name, + event.second, + ) + except Exception as e: + print("Whoa! {}".format(e)) + pass def handleEndGame(self, event, replay): - try: - if len( replay.tracker_events) ==0 : - return - for player in replay.players: - if player.play_race[0] == 'Z': - self.creepTracker.reduce_cgu_per_minute(player.pid) - player.creep_spread_by_minute = self.creepTracker.get_creep_spread_area(player.pid) - # note that player.max_creep_spread may be a tuple or an int - if player.creep_spread_by_minute: - player.max_creep_spread = max(player.creep_spread_by_minute.items(),key=lambda x:x[1]) - else: - ## Else statement is for players with no creep spread(ie: not Zerg) - player.max_creep_spread = 0 - except Exception as e: - print("Whoa! {}".format(e)) - pass + try: + if len(replay.tracker_events) == 0: + return + for player in replay.players: + if player.play_race[0] == "Z": + self.creepTracker.reduce_cgu_per_minute(player.pid) + player.creep_spread_by_minute = self.creepTracker.get_creep_spread_area( + player.pid + ) + # note that player.max_creep_spread may be a tuple or an int + if player.creep_spread_by_minute: + player.max_creep_spread = max( + player.creep_spread_by_minute.items(), key=lambda x: x[1] + ) + else: + ## Else statement is for players with no creep spread(ie: not Zerg) + player.max_creep_spread = 0 + except Exception as e: + print("Whoa! {}".format(e)) + pass ## The class used to used to calculate the creep spread -class creep_tracker(): - def __init__(self,replay): - #if the debug option is selected, minimaps will be printed to a file +class creep_tracker: + def __init__(self, replay): + # if the debug option is selected, minimaps will be printed to a file ##and a stringIO containing the minimap image will be saved for ##every minite in the game and the minimap with creep highlighted ## will be printed out. - self.debug = replay.opt['debug'] + self.debug = replay.opt["debug"] ##This list contains creep spread area for each player self.creep_spread_by_minute = dict() ## this list contains a minimap highlighted with creep spread for each player @@ -104,62 +118,63 @@ def __init__(self,replay): ## This list contains all the active cgus in every time frame self.creep_gen_units = dict() ## Thist list corresponds to creep_gen_units storing the time of each CGU - self.creep_gen_units_times= dict() + self.creep_gen_units_times = dict() ## convert all possible cgu radii into a sets of coordinates centred around the origin, ## in order to use this with the CGUs, the centre point will be ## subtracted with coordinates of cgus created in game - self.unit_name_to_radius={'CreepTumor': 10,"Hatchery":8,"NydusCanal": 5} - self.radius_to_coordinates= dict() + self.unit_name_to_radius = {"CreepTumor": 10, "Hatchery": 8, "NydusCanal": 5} + self.radius_to_coordinates = dict() for x in self.unit_name_to_radius: - self.radius_to_coordinates[self.unit_name_to_radius[x]] =\ - self.radius_to_map_positions(self.unit_name_to_radius[x]) - #Get map information + self.radius_to_coordinates[ + self.unit_name_to_radius[x] + ] = self.radius_to_map_positions(self.unit_name_to_radius[x]) + # Get map information replayMap = replay.map # extract image from replay package mapsio = BytesIO(replayMap.minimap) im = PIL_open(mapsio) ##remove black box around minimap -# https://github.com/jonomon/sc2reader/commit/2a793475c0358989e7fda4a75642035a810e2274 -# cropped = im.crop(im.getbbox()) -# cropsize = cropped.size + # https://github.com/jonomon/sc2reader/commit/2a793475c0358989e7fda4a75642035a810e2274 + # cropped = im.crop(im.getbbox()) + # cropsize = cropped.size cropsizeX = replay.map.map_info.camera_right - replay.map.map_info.camera_left cropsizeY = replay.map.map_info.camera_top - replay.map.map_info.camera_bottom - cropsize = (cropsizeX,cropsizeY) + cropsize = (cropsizeX, cropsizeY) self.map_height = 100.0 # resize height to MAPHEIGHT, and compute new width that # would preserve aspect ratio self.map_width = int(cropsize[0] * (float(self.map_height) / cropsize[1])) - self.mapSize =self.map_height * self.map_width + self.mapSize = self.map_height * self.map_width ## the following parameters are only needed if minimaps have to be printed -# minimapSize = ( self.map_width,int(self.map_height) ) -# self.minimap_image = cropped.resize(minimapSize, ANTIALIAS) + # minimapSize = ( self.map_width,int(self.map_height) ) + # self.minimap_image = cropped.resize(minimapSize, ANTIALIAS) - mapOffsetX= replayMap.map_info.camera_left + mapOffsetX = replayMap.map_info.camera_left mapOffsetY = replayMap.map_info.camera_bottom - mapCenter = [mapOffsetX+ cropsize[0]/2.0, mapOffsetY + cropsize[1]/2.0] + mapCenter = [mapOffsetX + cropsize[0] / 2.0, mapOffsetY + cropsize[1] / 2.0] # this is the center of the minimap image, in pixel coordinates - imageCenter = [(self.map_width/2), self.map_height/2] + imageCenter = [(self.map_width / 2), self.map_height / 2] # this is the scaling factor to go from the SC2 coordinate # system to pixel coordinates self.image_scale = float(self.map_height) / cropsize[1] - self.transX =imageCenter[0] + self.image_scale * (mapCenter[0]) + self.transX = imageCenter[0] + self.image_scale * (mapCenter[0]) self.transY = imageCenter[1] + self.image_scale * (mapCenter[1]) - def radius_to_map_positions(self,radius): + def radius_to_map_positions(self, radius): ## this function converts all radius into map coordinates ## centred around the origin that the creep can exist ## the cgu_radius_to_map_position function will simply ## substract every coordinate with the centre point of the tumour output_coordinates = list() # Sample a square area using the radius - for x in range (-radius,radius): - for y in range (-radius, radius): - if (x**2 + y**2) <= (radius * radius): - output_coordinates.append((x,y)) + for x in range(-radius, radius): + for y in range(-radius, radius): + if (x ** 2 + y ** 2) <= (radius * radius): + output_coordinates.append((x, y)) return output_coordinates def init_cgu_lists(self, player_id): @@ -169,139 +184,155 @@ def init_cgu_lists(self, player_id): self.creep_gen_units[player_id] = list() self.creep_gen_units_times[player_id] = list() - def add_to_list(self,player_id,unit_id,unit_location,unit_type,event_time): - # This functions adds a new time frame to creep_generating_units_list - # Each time frame contains a list of all CGUs that are alive + def add_to_list(self, player_id, unit_id, unit_location, unit_type, event_time): + # This functions adds a new time frame to creep_generating_units_list + # Each time frame contains a list of all CGUs that are alive length_cgu_list = len(self.creep_gen_units[player_id]) - if length_cgu_list==0: - self.creep_gen_units[player_id].append([(unit_id, unit_location,unit_type)]) + if length_cgu_list == 0: + self.creep_gen_units[player_id].append( + [(unit_id, unit_location, unit_type)] + ) self.creep_gen_units_times[player_id].append(event_time) else: - #if the list is not empty, take the previous time frame, + # if the list is not empty, take the previous time frame, # add the new CGU to it and append it as a new time frame - previous_list = self.creep_gen_units[player_id][length_cgu_list-1][:] - previous_list.append((unit_id, unit_location,unit_type)) + previous_list = self.creep_gen_units[player_id][length_cgu_list - 1][:] + previous_list.append((unit_id, unit_location, unit_type)) self.creep_gen_units[player_id].append(previous_list) self.creep_gen_units_times[player_id].append(event_time) - def remove_from_list(self,unit_id,time_frame): + def remove_from_list(self, unit_id, time_frame): ## This function searches is given a unit ID for every unit who died ## the unit id will be searched in cgu_gen_units for matches ## if there are any, that unit will be removed from active CGUs ## and appended as a new time frame for player_id in self.creep_gen_units: length_cgu_list = len(self.creep_gen_units[player_id]) - if length_cgu_list ==0: - break - cgu_per_player = self.creep_gen_units[player_id] [length_cgu_list-1] - creep_generating_died =filter(lambda x:x[0]==unit_id,cgu_per_player) + if length_cgu_list == 0: + break + cgu_per_player = self.creep_gen_units[player_id][length_cgu_list - 1] + creep_generating_died = filter(lambda x: x[0] == unit_id, cgu_per_player) for creep_generating_died_unit in creep_generating_died: - new_cgu_per_player = list(filter(lambda x:x != creep_generating_died_unit, cgu_per_player )) + new_cgu_per_player = list( + filter(lambda x: x != creep_generating_died_unit, cgu_per_player) + ) self.creep_gen_units[player_id].append(new_cgu_per_player) self.creep_gen_units_times[player_id].append(time_frame) - def cgu_gen_times_to_chunks(self,cgu_time_list): + def cgu_gen_times_to_chunks(self, cgu_time_list): ## this function returns the index and value of every cgu time maximum_cgu_time = max(cgu_time_list) for i in range(0, maximum_cgu_time): - a = list(filter(lambda x_y: x_y[1]//60==i , enumerate(cgu_time_list))) - if len(a)>0: + a = list(filter(lambda x_y: x_y[1] // 60 == i, enumerate(cgu_time_list))) + if len(a) > 0: yield a - def cgu_in_min_to_cgu_units(self,player_id,cgu_in_minutes): - ## this function takes index and value of CGU times and returns - ## the cgu units with the maximum length + def cgu_in_min_to_cgu_units(self, player_id, cgu_in_minutes): + ## this function takes index and value of CGU times and returns + ## the cgu units with the maximum length for cgu_per_minute in cgu_in_minutes: - indexes = map(lambda x:x[0], cgu_per_minute) + indexes = map(lambda x: x[0], cgu_per_minute) cgu_units = list() for index in indexes: cgu_units.append(self.creep_gen_units[player_id][index]) - cgu_max_in_minute = max(cgu_units,key = len) + cgu_max_in_minute = max(cgu_units, key=len) yield cgu_max_in_minute - def reduce_cgu_per_minute(self,player_id): - #the creep_gen_units_lists contains every single time frame - #where a CGU is added, - #To reduce the calculations required, the time frame containing - #the most cgus every minute will be used to represent that minute - cgu_per_minute1, cgu_per_minute2 = tee (self.cgu_gen_times_to_chunks(self.creep_gen_units_times[player_id])) - cgu_unit_max_per_minute = self.cgu_in_min_to_cgu_units(player_id,cgu_per_minute1) - minutes = map(lambda x:int(x[0][1]//60)*60, cgu_per_minute2) - self.creep_gen_units[player_id] = list(cgu_unit_max_per_minute) - self.creep_gen_units_times[player_id] = list(minutes) + def reduce_cgu_per_minute(self, player_id): + # the creep_gen_units_lists contains every single time frame + # where a CGU is added, + # To reduce the calculations required, the time frame containing + # the most cgus every minute will be used to represent that minute + cgu_per_minute1, cgu_per_minute2 = tee( + self.cgu_gen_times_to_chunks(self.creep_gen_units_times[player_id]) + ) + cgu_unit_max_per_minute = self.cgu_in_min_to_cgu_units( + player_id, cgu_per_minute1 + ) + minutes = map(lambda x: int(x[0][1] // 60) * 60, cgu_per_minute2) + self.creep_gen_units[player_id] = list(cgu_unit_max_per_minute) + self.creep_gen_units_times[player_id] = list(minutes) - def get_creep_spread_area(self,player_id): + def get_creep_spread_area(self, player_id): ## iterates through all cgus and and calculate the area - for index,cgu_per_player in enumerate(self.creep_gen_units[player_id]): + for index, cgu_per_player in enumerate(self.creep_gen_units[player_id]): # convert cgu list into centre of circles and radius - cgu_radius = map(lambda x: (x[1], self.unit_name_to_radius[x[2]]),\ - cgu_per_player) + cgu_radius = map( + lambda x: (x[1], self.unit_name_to_radius[x[2]]), cgu_per_player + ) # convert event coords to minimap coords cgu_radius = self.convert_cgu_radius_event_to_map_coord(cgu_radius) - creep_area_positions = self.cgu_radius_to_map_positions(cgu_radius,self.radius_to_coordinates) + creep_area_positions = self.cgu_radius_to_map_positions( + cgu_radius, self.radius_to_coordinates + ) cgu_event_time = self.creep_gen_units_times[player_id][index] - cgu_event_time_str=str(int(cgu_event_time//60))+":"+str(cgu_event_time%60) + cgu_event_time_str = ( + str(int(cgu_event_time // 60)) + ":" + str(cgu_event_time % 60) + ) if self.debug: - self.print_image(creep_area_positions,player_id,cgu_event_time_str) + self.print_image(creep_area_positions, player_id, cgu_event_time_str) creep_area = len(creep_area_positions) - self.creep_spread_by_minute[player_id][cgu_event_time]=\ - float(creep_area)/self.mapSize*100 + self.creep_spread_by_minute[player_id][cgu_event_time] = ( + float(creep_area) / self.mapSize * 100 + ) return self.creep_spread_by_minute[player_id] - def cgu_radius_to_map_positions(self,cgu_radius,radius_to_coordinates): - ## This function uses the output of radius_to_map_positions + def cgu_radius_to_map_positions(self, cgu_radius, radius_to_coordinates): + ## This function uses the output of radius_to_map_positions total_points_on_map = Set() - if len(cgu_radius)==0: + if len(cgu_radius) == 0: return [] for cgu in cgu_radius: point = cgu[0] radius = cgu[1] ## subtract all radius_to_coordinates with centre of ## cgu radius to change centre of circle - cgu_map_position = map( lambda x:(x[0]+point[0],x[1]+point[1])\ - ,self.radius_to_coordinates[radius]) - total_points_on_map= total_points_on_map | Set(cgu_map_position) + cgu_map_position = map( + lambda x: (x[0] + point[0], x[1] + point[1]), + self.radius_to_coordinates[radius], + ) + total_points_on_map = total_points_on_map | Set(cgu_map_position) return total_points_on_map - def print_image(self,total_points_on_map,player_id,time_stamp): + def print_image(self, total_points_on_map, player_id, time_stamp): minimap_copy = self.minimap_image.copy() # Convert all creeped points to white for points in total_points_on_map: x = points[0] y = points[1] - x,y = self.check_image_pixel_within_boundary(x,y) - minimap_copy.putpixel((x,y) , (255, 255, 255)) + x, y = self.check_image_pixel_within_boundary(x, y) + minimap_copy.putpixel((x, y), (255, 255, 255)) creeped_image = minimap_copy # write creeped minimap image to a string as a png creeped_imageIO = StringIO() creeped_image.save(creeped_imageIO, "png") - self.creep_spread_image_by_minute[player_id][time_stamp]=creeped_imageIO + self.creep_spread_image_by_minute[player_id][time_stamp] = creeped_imageIO ##debug for print out the images - f = open(str(player_id)+'image'+time_stamp+'.png','w') + f = open(str(player_id) + "image" + time_stamp + ".png", "w") f.write(creeped_imageIO.getvalue()) creeped_imageIO.close() f.close() - def check_image_pixel_within_boundary(self,pointX, pointY): - pointX = 0 if pointX <0 else pointX - pointY=0 if pointY <0 else pointY + def check_image_pixel_within_boundary(self, pointX, pointY): + pointX = 0 if pointX < 0 else pointX + pointY = 0 if pointY < 0 else pointY # put a minus 1 to make sure the pixel is not directly on the edge - pointX = int(self.map_width-1 if pointX >= self.map_width else pointX) - pointY = int(self.map_height-1 if pointY >= self.map_height else pointY) - return pointX,pointY + pointX = int(self.map_width - 1 if pointX >= self.map_width else pointX) + pointY = int(self.map_height - 1 if pointY >= self.map_height else pointY) + return pointX, pointY - def convert_cgu_radius_event_to_map_coord(self,cgu_radius): + def convert_cgu_radius_event_to_map_coord(self, cgu_radius): cgu_radius_new = list() for cgu in cgu_radius: x = cgu[0][0] y = cgu[0][1] - (x,y) = self.convert_event_coord_to_map_coord(x,y) - cgu = ((x,y),cgu[1]) + (x, y) = self.convert_event_coord_to_map_coord(x, y) + cgu = ((x, y), cgu[1]) cgu_radius_new.append(cgu) return cgu_radius_new - def convert_event_coord_to_map_coord(self,x,y): + def convert_event_coord_to_map_coord(self, x, y): imageX = int(self.map_width - self.transX + self.image_scale * x) imageY = int(self.transY - self.image_scale * y) return imageX, imageY diff --git a/sc2reader/engine/plugins/gameheart.py b/sc2reader/engine/plugins/gameheart.py index 08bfc67e..1e412031 100644 --- a/sc2reader/engine/plugins/gameheart.py +++ b/sc2reader/engine/plugins/gameheart.py @@ -23,7 +23,8 @@ class GameHeartNormalizer(object): * They are all 1v1's. * You can't random in GameHeart """ - name = 'GameHeartNormalizer' + + name = "GameHeartNormalizer" PRIMARY_BUILDINGS = dict(Hatchery="Zerg", Nexus="Protoss", CommandCenter="Terran") @@ -38,14 +39,20 @@ def handleInitGame(self, event, replay): for event in replay.tracker_events: if start_frame != -1 and event.frame > start_frame + 5: # fuzz it a little break - if event.name == 'UnitBornEvent' and event.control_pid and event.unit_type_name in self.PRIMARY_BUILDINGS: + if ( + event.name == "UnitBornEvent" + and event.control_pid + and event.unit_type_name in self.PRIMARY_BUILDINGS + ): # In normal replays, starting units are born on frame zero. if event.frame == 0: yield PluginExit(self, code=0, details=dict()) return else: start_frame = event.frame - actual_players[event.control_pid] = self.PRIMARY_BUILDINGS[event.unit_type_name] + actual_players[event.control_pid] = self.PRIMARY_BUILDINGS[ + event.unit_type_name + ] self.fix_entities(replay, actual_players) self.fix_events(replay, start_frame) @@ -53,8 +60,12 @@ def handleInitGame(self, event, replay): replay.frames -= start_frame replay.game_length = Length(seconds=replay.frames / 16) replay.real_type = get_real_type(replay.teams) - replay.real_length = Length(seconds=int(replay.game_length.seconds/GAME_SPEED_FACTOR[replay.speed])) - replay.start_time = datetime.utcfromtimestamp(replay.unix_timestamp-replay.real_length.seconds) + replay.real_length = Length( + seconds=int(replay.game_length.seconds / GAME_SPEED_FACTOR[replay.speed]) + ) + replay.start_time = datetime.utcfromtimestamp( + replay.unix_timestamp - replay.real_length.seconds + ) def fix_events(self, replay, start_frame): # Set back the game clock for all events @@ -70,8 +81,8 @@ def fix_entities(self, replay, actual_players): # Change the players that aren't playing into observers for p in [p for p in replay.players if p.pid not in actual_players]: # Fix the slot data to be accurate - p.slot_data['observe'] = 1 - p.slot_data['team_id'] = None + p.slot_data["observe"] = 1 + p.slot_data["team_id"] = None obs = Observer(p.sid, p.slot_data, p.uid, p.init_data, p.pid) # Because these obs start the game as players the client @@ -103,7 +114,7 @@ def fix_entities(self, replay, actual_players): replay.team = dict() replay.teams = list() for index, player in enumerate(replay.players): - team_id = index+1 + team_id = index + 1 team = Team(team_id) replay.team[team_id] = team replay.teams.append(team) @@ -113,5 +124,5 @@ def fix_entities(self, replay, actual_players): player.play_race = player.pick_race team.players = [player] team.result = player.result - if team.result == 'Win': + if team.result == "Win": replay.winner = team diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 69aa12a7..9f741894 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -23,7 +23,8 @@ class SelectionTracker(object): # TODO: list a few error inducing sitations """ - name = 'SelectionTracker' + + name = "SelectionTracker" def handleInitGame(self, event, replay): for person in replay.entities: @@ -34,7 +35,9 @@ def handleInitGame(self, event, replay): def handleSelectionEvent(self, event, replay): selection = event.player.selection[event.control_group] - new_selection, error = self._deselect(selection, event.mask_type, event.mask_data) + new_selection, error = self._deselect( + selection, event.mask_type, event.mask_data + ) new_selection = self._select(new_selection, event.objects) event.player.selection[event.control_group] = new_selection if error: @@ -42,7 +45,9 @@ def handleSelectionEvent(self, event, replay): def handleGetControlGroupEvent(self, event, replay): selection = event.player.selection[event.control_group] - new_selection, error = self._deselect(selection, event.mask_type, event.mask_data) + new_selection, error = self._deselect( + selection, event.mask_type, event.mask_data + ) event.player.selection[10] = new_selection if error: event.player.selection_errors += 1 @@ -52,36 +57,38 @@ def handleSetControlGroupEvent(self, event, replay): def handleAddToControlGroupEvent(self, event, replay): selection = event.player.selection[event.control_group] - new_selection, error = self._deselect(selection, event.mask_type, event.mask_data) + new_selection, error = self._deselect( + selection, event.mask_type, event.mask_data + ) new_selection = self._select(new_selection, event.player.selection[10]) event.player.selection[event.control_group] = new_selection if error: event.player.selection_errors += 1 def _select(self, selection, units): - return sorted(set(selection+units)) + return sorted(set(selection + units)) def _deselect(self, selection, mode, data): """Returns false if there was a data error when deselecting""" - if mode == 'None': + if mode == "None": return selection, False selection_size, data_size = len(selection), len(data) - if mode == 'Mask': + if mode == "Mask": # Deselect objects according to deselect mask sfilter = lambda bit_u: not bit_u[0] - mask = data+[False]*(selection_size-data_size) + mask = data + [False] * (selection_size - data_size) new_selection = [u for (bit, u) in filter(sfilter, zip(mask, selection))] error = data_size > selection_size - elif mode == 'OneIndices': + elif mode == "OneIndices": # Deselect objects according to indexes clean_data = list(filter(lambda i: i < selection_size, data)) new_selection = [u for i, u in enumerate(selection) if i < selection_size] error = len(list(filter(lambda i: i >= selection_size, data))) != 0 - elif mode == 'ZeroIndices': + elif mode == "ZeroIndices": # Select objects according to indexes clean_data = list(filter(lambda i: i < selection_size, data)) new_selection = [selection[i] for i in clean_data] diff --git a/sc2reader/engine/plugins/supply.py b/sc2reader/engine/plugins/supply.py index 394a3a64..c106f757 100644 --- a/sc2reader/engine/plugins/supply.py +++ b/sc2reader/engine/plugins/supply.py @@ -3,85 +3,158 @@ from collections import defaultdict + class SupplyTracker(object): - def add_to_units_alive(self,event,replay): + def add_to_units_alive(self, event, replay): unit_name = event.unit_type_name - if unit_name in self.unit_name_to_supply: + if unit_name in self.unit_name_to_supply: supplyCount = self.unit_name_to_supply[event.unit_type_name][0] buildTime = self.unit_name_to_supply[event.unit_type_name][1] - time_built = event.second - buildTime - time_built= 0 if time_built < 0 else time_built + time_built = event.second - buildTime + time_built = 0 if time_built < 0 else time_built new_unit = (supplyCount, event.unit_id) self.units_alive[event.control_pid].append(new_unit) total_supply = sum([x[0] for x in self.units_alive[event.control_pid]]) - replay.players[event.control_pid-1].current_food_used[time_built]= total_supply - print("Second",time_built,replay.players[event.control_pid-1],"SUPPLY",replay.players[event.control_pid-1].current_food_used[time_built]) - + replay.players[event.control_pid - 1].current_food_used[ + time_built + ] = total_supply + print( + "Second", + time_built, + replay.players[event.control_pid - 1], + "SUPPLY", + replay.players[event.control_pid - 1].current_food_used[time_built], + ) + elif unit_name in self.supply_gen_unit: - ## see if the unit provides supply + ## see if the unit provides supply supply_gen_count = self.supply_gen_unit[event.unit_type_name][0] build_time = self.supply_gen_unit[event.unit_type_name][1] - time_complete = event.second+ build_time - supply_gen_unit = (supply_gen_count,event.unit_id) + time_complete = event.second + build_time + supply_gen_unit = (supply_gen_count, event.unit_id) self.supply_gen[event.control_pid].append(supply_gen_unit) total_supply_gen = sum([x[0] for x in self.supply_gen[event.control_pid]]) - replay.players[event.control_pid-1].current_food_made[time_complete]= total_supply_gen - print("Second",time_complete, replay.players[event.control_pid-1],"Built",replay.players[event.control_pid-1].current_food_made[time_complete]) + replay.players[event.control_pid - 1].current_food_made[ + time_complete + ] = total_supply_gen + print( + "Second", + time_complete, + replay.players[event.control_pid - 1], + "Built", + replay.players[event.control_pid - 1].current_food_made[time_complete], + ) else: print("Unit name {0} does not exist".format(event.unit_type_name)) return - def remove_from_units_alive(self,event,replay): + def remove_from_units_alive(self, event, replay): died_unit_id = event.unit_id for player in replay.player: - dead_unit = filter(lambda x:x[1]==died_unit_id,self.units_alive[player]) + dead_unit = filter(lambda x: x[1] == died_unit_id, self.units_alive[player]) if dead_unit: self.units_alive[player].remove(dead_unit[0]) total_supply = sum([x[0] for x in self.units_alive[player]]) - - replay.players[player-1].current_food_used[event.second] = total_supply - print("Second", event.second, "Killed", event.unit.name,"SUPPLY",replay.players[player-1].current_food_used[event.second]) - - dead_supply_gen=filter(lambda x:x[1]==died_unit_id, self.supply_gen[player]) + + replay.players[player - 1].current_food_used[ + event.second + ] = total_supply + print( + "Second", + event.second, + "Killed", + event.unit.name, + "SUPPLY", + replay.players[player - 1].current_food_used[event.second], + ) + + dead_supply_gen = filter( + lambda x: x[1] == died_unit_id, self.supply_gen[player] + ) if dead_supply_gen: self.supply_gen[player].remove(dead_supply_gen[0]) total_supply_gen = sum([x[0] for x in self.supply_gen[player]]) - replay.players[player-1].current_food_made[event.second] = total_supply_gen - print("Second", event.second, "Killed", event.unit.name,"SUPPLY",replay.players[player-1].current_food_made[event.second]) + replay.players[player - 1].current_food_made[ + event.second + ] = total_supply_gen + print( + "Second", + event.second, + "Killed", + event.unit.name, + "SUPPLY", + replay.players[player - 1].current_food_made[event.second], + ) def handleInitGame(self, event, replay): ## This dictionary contains te supply of every unit - self.unit_name_to_supply = { - #Zerg - "Drone":(1,17),"Zergling":(1,25),"Baneling":(0,20),"Queen":(2,50),\ - "Hydralisk":(2,33),"Roach":(2,27),"Infestor":(2,50),"Mutalisk":(2,33),\ - "Corruptor":(2,40),"Utralisk":(6,55),"Broodlord":(2,34),\ - "SwarmHost":(3,40), "Viper":(3,40),\ - #Terran - "SCV":(1,17),"Marine":(1,25),"Marauder":(2,30),"SiegeTank":(2,45),\ - "Reaper":(1,45),"Ghost":(2,40),"Hellion":(2,30),"Thor":(6,60),\ - "Viking":(2,42),"Medivac":(2,42),"Raven":(2,60), "Banshee":(3,60),\ - "Battlecruiser":(6,90), "Hellbat":(2,30),"WidowMine":(2,40),\ - #Protoss - "Probe":(1,17),"Zealot":(2,38),"Stalker":(2,42),"Sentry":(2,42),\ - "Observer":(1,30), "Immortal":(4,55),"WarpPrism":(2,50),\ - "Colossus":(6,75), "Phoenix":(2,35),"VoidRay":(4,60), \ - "HighTemplar":(2,55),"DarkTemplar":(2,55), "Archon":(4,12),\ - "Carrier":(6,120), "Mothership":(6,100),"MothershipCore":(2,30),\ - "Oracle":(3,50),"Tempest":(4,60)} - + self.unit_name_to_supply = { + # Zerg + "Drone": (1, 17), + "Zergling": (1, 25), + "Baneling": (0, 20), + "Queen": (2, 50), + "Hydralisk": (2, 33), + "Roach": (2, 27), + "Infestor": (2, 50), + "Mutalisk": (2, 33), + "Corruptor": (2, 40), + "Utralisk": (6, 55), + "Broodlord": (2, 34), + "SwarmHost": (3, 40), + "Viper": (3, 40), + # Terran + "SCV": (1, 17), + "Marine": (1, 25), + "Marauder": (2, 30), + "SiegeTank": (2, 45), + "Reaper": (1, 45), + "Ghost": (2, 40), + "Hellion": (2, 30), + "Thor": (6, 60), + "Viking": (2, 42), + "Medivac": (2, 42), + "Raven": (2, 60), + "Banshee": (3, 60), + "Battlecruiser": (6, 90), + "Hellbat": (2, 30), + "WidowMine": (2, 40), + # Protoss + "Probe": (1, 17), + "Zealot": (2, 38), + "Stalker": (2, 42), + "Sentry": (2, 42), + "Observer": (1, 30), + "Immortal": (4, 55), + "WarpPrism": (2, 50), + "Colossus": (6, 75), + "Phoenix": (2, 35), + "VoidRay": (4, 60), + "HighTemplar": (2, 55), + "DarkTemplar": (2, 55), + "Archon": (4, 12), + "Carrier": (6, 120), + "Mothership": (6, 100), + "MothershipCore": (2, 30), + "Oracle": (3, 50), + "Tempest": (4, 60), + } + self.supply_gen_unit = { - #overlord build time is zero because event for units are made when + # overlord build time is zero because event for units are made when # it is born not when it's created - "Overlord":(8,0),"Hatchery":(2,100), \ - "SupplyDepot":(8,30),"CommandCenter":(11,100),\ - "Pylon":(8,25),"Nexus":(10,100) + "Overlord": (8, 0), + "Hatchery": (2, 100), + "SupplyDepot": (8, 30), + "CommandCenter": (11, 100), + "Pylon": (8, 25), + "Nexus": (10, 100), } - ## This list contains a turple of the units supply and unit ID. + ## This list contains a turple of the units supply and unit ID. ## the purpose of the list is to know which user owns which unit - ## so that when a unit dies, that + ## so that when a unit dies, that self.units_alive = dict() - ## + ## self.supply_gen = dict() for player in replay.players: self.supply_gen[player.pid] = list() @@ -90,20 +163,24 @@ def handleInitGame(self, event, replay): player.current_food_made = defaultdict(int) player.time_supply_capped = int() - def handleUnitInitEvent(self,event,replay): - #print ("Init",event.unit_type_name, event.unit_id) - self.add_to_units_alive(event,replay) + def handleUnitInitEvent(self, event, replay): + # print ("Init",event.unit_type_name, event.unit_id) + self.add_to_units_alive(event, replay) - def handleUnitBornEvent(self,event,replay): - #print ("Born",event.unit_type_name,event.unit_id) - self.add_to_units_alive(event,replay) + def handleUnitBornEvent(self, event, replay): + # print ("Born",event.unit_type_name,event.unit_id) + self.add_to_units_alive(event, replay) - def handleUnitDiedEvent(self,event,replay): + def handleUnitDiedEvent(self, event, replay): if event.unit.name not in self.unit_name_to_supply: return - self.remove_from_units_alive(event,replay) + self.remove_from_units_alive(event, replay) def handleEndGame(self, event, replay): for player in replay.players: - player.current_food_used = sorted(player.current_food_used.iteritems(), key=lambda x: x[0]) - player.current_food_made = sorted(player.current_food_made.iteritems(), key=lambda x:x[0]) + player.current_food_used = sorted( + player.current_food_used.iteritems(), key=lambda x: x[0] + ) + player.current_food_made = sorted( + player.current_food_made.iteritems(), key=lambda x: x[0] + ) diff --git a/sc2reader/engine/utils.py b/sc2reader/engine/utils.py index e3a9a18c..c8ca4af5 100644 --- a/sc2reader/engine/utils.py +++ b/sc2reader/engine/utils.py @@ -31,7 +31,7 @@ def __getitem__(self, frame): else: # Copy the previous state and use it as our basis here state = self[prev_frame] - if hasattr(state, 'copy'): + if hasattr(state, "copy"): state = state.copy() self[frame] = state diff --git a/sc2reader/events/base.py b/sc2reader/events/base.py index dbea286c..89f16c5e 100644 --- a/sc2reader/events/base.py +++ b/sc2reader/events/base.py @@ -3,4 +3,4 @@ class Event(object): - name = 'Event' + name = "Event" diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 7eadebfb..9dda98b5 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -13,6 +13,7 @@ class GameEvent(Event): """ This is the base class for all game events. The attributes below are universally available. """ + def __init__(self, frame, pid): #: The id of the player generating the event. This is 16 for global non-player events. #: Prior to Heart of the Swarm this was the player id. Since HotS it is @@ -31,16 +32,18 @@ def __init__(self, frame, pid): self.second = frame >> 4 #: A flag indicating if it is a local or global event. - self.is_local = (pid != 16) + self.is_local = pid != 16 #: Short cut string for event class name self.name = self.__class__.__name__ def _str_prefix(self): - if getattr(self, 'pid', 16) == 16: + if getattr(self, "pid", 16) == 16: player_name = "Global" elif self.player and not self.player.name: - player_name = "Player {0} - ({1})".format(self.player.pid, self.player.play_race) + player_name = "Player {0} - ({1})".format( + self.player.pid, self.player.play_race + ) elif self.player: player_name = self.player.name else: @@ -56,6 +59,7 @@ class GameStartEvent(GameEvent): Recorded when the game starts and the frames start to roll. This is a global non-player event. """ + def __init__(self, frame, pid, data): super(GameStartEvent, self).__init__(frame, pid) @@ -67,6 +71,7 @@ class PlayerLeaveEvent(GameEvent): """ Recorded when a player leaves the game. """ + def __init__(self, frame, pid, data): super(PlayerLeaveEvent, self).__init__(frame, pid) @@ -79,48 +84,51 @@ class UserOptionsEvent(GameEvent): This event is recorded for each player at the very beginning of the game before the :class:`GameStartEvent`. """ + def __init__(self, frame, pid, data): super(UserOptionsEvent, self).__init__(frame, pid) #: - self.game_fully_downloaded = data['game_fully_downloaded'] + self.game_fully_downloaded = data["game_fully_downloaded"] #: - self.development_cheats_enabled = data['development_cheats_enabled'] + self.development_cheats_enabled = data["development_cheats_enabled"] #: - self.multiplayer_cheats_enabled = data['multiplayer_cheats_enabled'] + self.multiplayer_cheats_enabled = data["multiplayer_cheats_enabled"] #: - self.sync_checksumming_enabled = data['sync_checksumming_enabled'] + self.sync_checksumming_enabled = data["sync_checksumming_enabled"] #: - self.is_map_to_map_transition = data['is_map_to_map_transition'] + self.is_map_to_map_transition = data["is_map_to_map_transition"] #: - self.use_ai_beacons = data['use_ai_beacons'] + self.use_ai_beacons = data["use_ai_beacons"] #: Are workers sent to auto-mine on game start - self.starting_rally = data['starting_rally'] if 'starting_rally' in data else None + self.starting_rally = ( + data["starting_rally"] if "starting_rally" in data else None + ) #: - self.debug_pause_enabled = data['debug_pause_enabled'] + self.debug_pause_enabled = data["debug_pause_enabled"] #: - self.base_build_num = data['base_build_num'] + self.base_build_num = data["base_build_num"] def create_command_event(frame, pid, data): - ability_type = data['data'][0] - if ability_type == 'None': + ability_type = data["data"][0] + if ability_type == "None": return BasicCommandEvent(frame, pid, data) - elif ability_type == 'TargetUnit': + elif ability_type == "TargetUnit": return TargetUnitCommandEvent(frame, pid, data) - elif ability_type == 'TargetPoint': + elif ability_type == "TargetPoint": return TargetPointCommandEvent(frame, pid, data) - elif ability_type == 'Data': + elif ability_type == "Data": return DataCommandEvent(frame, pid, data) @@ -135,11 +143,12 @@ class CommandEvent(GameEvent): See :class:`TargetPointCommandEvent`, :class:`TargetUnitCommandEvent`, and :class:`DataCommandEvent` for individual details. """ + def __init__(self, frame, pid, data): super(CommandEvent, self).__init__(frame, pid) #: Flags on the command??? - self.flags = data['flags'] + self.flags = data["flags"] #: A dictionary of possible ability flags. Flags are: #: @@ -192,16 +201,20 @@ def __init__(self, frame, pid, data): ) #: Flag marking that the command had ability information - self.has_ability = data['ability'] is not None + self.has_ability = data["ability"] is not None #: Link the the ability group - self.ability_link = data['ability']['ability_link'] if self.has_ability else 0 + self.ability_link = data["ability"]["ability_link"] if self.has_ability else 0 #: The index of the ability in the ability group - self.command_index = data['ability']['ability_command_index'] if self.has_ability else 0 + self.command_index = ( + data["ability"]["ability_command_index"] if self.has_ability else 0 + ) #: Additional ability data. - self.ability_data = data['ability']['ability_command_data'] if self.has_ability else 0 + self.ability_data = ( + data["ability"]["ability_command_data"] if self.has_ability else 0 + ) #: Unique identifier for the ability self.ability_id = self.ability_link << 5 | self.command_index @@ -210,16 +223,16 @@ def __init__(self, frame, pid, data): self.ability = None #: A shortcut to the name of the ability being used - self.ability_name = '' + self.ability_name = "" #: The type of ability, one of: None (no target), TargetPoint, TargetUnit, or Data - self.ability_type = data['data'][0] + self.ability_type = data["data"][0] #: The raw data associated with this ability type - self.ability_type_data = data['data'][1] + self.ability_type_data = data["data"][1] #: Other unit id?? - self.other_unit_id = data['other_unit_tag'] + self.other_unit_id = data["other_unit_tag"] #: A reference to the other unit self.other_unit = None @@ -233,10 +246,12 @@ def __str__(self): else: string += "Right Click" - if self.ability_type == 'TargetUnit': - string += "; Target: {0} [{1:0>8X}]".format(self.target.name, self.target_unit_id) + if self.ability_type == "TargetUnit": + string += "; Target: {0} [{1:0>8X}]".format( + self.target.name, self.target_unit_id + ) - if self.ability_type in ('TargetPoint', 'TargetUnit'): + if self.ability_type in ("TargetPoint", "TargetUnit"): string += "; Location: {0}".format(str(self.location)) return string @@ -251,6 +266,7 @@ class BasicCommandEvent(CommandEvent): Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ + def __init__(self, frame, pid, data): super(BasicCommandEvent, self).__init__(frame, pid, data) @@ -266,17 +282,18 @@ class TargetPointCommandEvent(CommandEvent): Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ + def __init__(self, frame, pid, data): super(TargetPointCommandEvent, self).__init__(frame, pid, data) #: The x coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.x = self.ability_type_data['point'].get('x', 0) / 4096.0 + self.x = self.ability_type_data["point"].get("x", 0) / 4096.0 #: The y coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.y = self.ability_type_data['point'].get('y', 0) / 4096.0 + self.y = self.ability_type_data["point"].get("y", 0) / 4096.0 #: The z coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.z = self.ability_type_data['point'].get('z', 0) + self.z = self.ability_type_data["point"].get("z", 0) #: The location of the target. Available for TargetPoint and TargetUnit type events self.location = (self.x, self.y, self.z) @@ -293,18 +310,19 @@ class TargetUnitCommandEvent(CommandEvent): Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ + def __init__(self, frame, pid, data): super(TargetUnitCommandEvent, self).__init__(frame, pid, data) #: Flags set on the target unit. Available for TargetUnit type events - self.target_flags = self.ability_type_data.get('flags', None) + self.target_flags = self.ability_type_data.get("flags", None) #: Timer?? Available for TargetUnit type events. - self.target_timer = self.ability_type_data.get('timer', None) + self.target_timer = self.ability_type_data.get("timer", None) #: Unique id of the target unit. Available for TargetUnit type events. #: This id can be 0 when the target unit is shrouded by fog of war. - self.target_unit_id = self.ability_type_data.get('unit_tag', None) + self.target_unit_id = self.ability_type_data.get("unit_tag", None) #: A reference to the targetted unit. When the :attr:`target_unit_id` is #: 0 this target unit is a generic, reused fog of war unit of the :attr:`target_unit_type` @@ -312,27 +330,28 @@ def __init__(self, frame, pid, data): self.target_unit = None #: Current integer type id of the target unit. Available for TargetUnit type events. - self.target_unit_type = self.ability_type_data.get('unit_link', None) + self.target_unit_type = self.ability_type_data.get("unit_link", None) #: Integer player id of the controlling player. Available for TargetUnit type events starting in 19595. #: When the targetted unit is under fog of war this id is zero. - self.control_player_id = self.ability_type_data.get('control_player_id', None) + self.control_player_id = self.ability_type_data.get("control_player_id", None) #: Integer player id of the player paying upkeep. Available for TargetUnit type events. - self.upkeep_player_id = self.ability_type_data.get('upkeep_player_id', None) + self.upkeep_player_id = self.ability_type_data.get("upkeep_player_id", None) #: The x coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.x = self.ability_type_data['point'].get('x', 0) / 4096.0 + self.x = self.ability_type_data["point"].get("x", 0) / 4096.0 #: The y coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.y = self.ability_type_data['point'].get('y', 0) / 4096.0 + self.y = self.ability_type_data["point"].get("y", 0) / 4096.0 #: The z coordinate of the target. Available for TargetPoint and TargetUnit type events. - self.z = self.ability_type_data['point'].get('z', 0) + self.z = self.ability_type_data["point"].get("z", 0) #: The location of the target. Available for TargetPoint and TargetUnit type events self.location = (self.x, self.y, self.z) + class UpdateTargetPointCommandEvent(TargetPointCommandEvent): """ Extends :class: 'TargetPointCommandEvent' @@ -342,7 +361,9 @@ class UpdateTargetPointCommandEvent(TargetPointCommandEvent): instances of this occurring. """ - name = 'UpdateTargetPointCommandEvent' + + name = "UpdateTargetPointCommandEvent" + class UpdateTargetUnitCommandEvent(TargetUnitCommandEvent): """ @@ -357,7 +378,7 @@ class UpdateTargetUnitCommandEvent(TargetUnitCommandEvent): holding shift, and then shift clicking on a second hatchery. """ - name = 'UpdateTargetUnitCommandEvent' + name = "UpdateTargetUnitCommandEvent" class DataCommandEvent(CommandEvent): @@ -370,11 +391,12 @@ class DataCommandEvent(CommandEvent): Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ + def __init__(self, frame, pid, data): super(DataCommandEvent, self).__init__(frame, pid, data) #: Other target data. Available for Data type events. - self.target_data = self.ability_type_data.get('data', None) + self.target_data = self.ability_type_data.get("data", None) @loggable @@ -389,37 +411,83 @@ class SelectionEvent(GameEvent): by non-player actions. When a player action updates a control group a :class:`ControlGroupEvent` is generated. """ + def __init__(self, frame, pid, data): super(SelectionEvent, self).__init__(frame, pid) #: The control group being modified. 10 for active selection - self.control_group = data['control_group_index'] + self.control_group = data["control_group_index"] #: Deprecated, use control_group self.bank = self.control_group #: ??? - self.subgroup_index = data['subgroup_index'] + self.subgroup_index = data["subgroup_index"] #: The type of mask to apply. One of None, Mask, OneIndices, ZeroIndices - self.mask_type = data['remove_mask'][0] + self.mask_type = data["remove_mask"][0] #: The data for the mask - self.mask_data = data['remove_mask'][1] + self.mask_data = data["remove_mask"][1] #: The unit type data for the new units - self.new_unit_types = [(d['unit_link'], d['subgroup_priority'], d['intra_subgroup_priority'], d['count']) for d in data['add_subgroups']] + self.new_unit_types = [ + ( + d["unit_link"], + d["subgroup_priority"], + d["intra_subgroup_priority"], + d["count"], + ) + for d in data["add_subgroups"] + ] #: The unit id data for the new units - self.new_unit_ids = data['add_unit_tags'] + self.new_unit_ids = data["add_unit_tags"] # This stretches out the unit types and priorities to be zipped with ids. - unit_types = chain(*[[utype]*count for (utype, subgroup_priority, intra_subgroup_priority, count) in self.new_unit_types]) - unit_subgroup_priorities = chain(*[[subgroup_priority]*count for (utype, subgroup_priority, intra_subgroup_priority, count) in self.new_unit_types]) - unit_intra_subgroup_priorities = chain(*[[intra_subgroup_priority]*count for (utype, subgroup_priority, intra_subgroup_priority, count) in self.new_unit_types]) + unit_types = chain( + *[ + [utype] * count + for ( + utype, + subgroup_priority, + intra_subgroup_priority, + count, + ) in self.new_unit_types + ] + ) + unit_subgroup_priorities = chain( + *[ + [subgroup_priority] * count + for ( + utype, + subgroup_priority, + intra_subgroup_priority, + count, + ) in self.new_unit_types + ] + ) + unit_intra_subgroup_priorities = chain( + *[ + [intra_subgroup_priority] * count + for ( + utype, + subgroup_priority, + intra_subgroup_priority, + count, + ) in self.new_unit_types + ] + ) #: The combined type and id information for new units - self.new_unit_info = list(zip(self.new_unit_ids, unit_types, unit_subgroup_priorities, unit_intra_subgroup_priorities)) + self.new_unit_info = list( + zip( + self.new_unit_ids, + unit_types, + unit_subgroup_priorities, + unit_intra_subgroup_priorities, + ) + ) #: A list of references to units added by this selection self.new_units = None @@ -429,13 +497,13 @@ def __init__(self, frame, pid, data): def __str__(self): if self.new_units: - return GameEvent.__str__(self)+str([str(u) for u in self.new_units]) + return GameEvent.__str__(self) + str([str(u) for u in self.new_units]) else: - return GameEvent.__str__(self)+str([str(u) for u in self.new_unit_info]) + return GameEvent.__str__(self) + str([str(u) for u in self.new_unit_info]) def create_control_group_event(frame, pid, data): - update_type = data['control_group_update'] + update_type = data["control_group_update"] if update_type == 0: return SetControlGroupEvent(frame, pid, data) elif update_type == 1: @@ -464,11 +532,12 @@ class ControlGroupEvent(GameEvent): All three events have the same set of data (shown below) but are interpretted differently. See the class entry for details. """ + def __init__(self, frame, pid, data): super(ControlGroupEvent, self).__init__(frame, pid) #: Index to the control group being modified - self.control_group = data['control_group_index'] + self.control_group = data["control_group_index"] #: Deprecated, use control_group self.bank = self.control_group @@ -477,13 +546,13 @@ def __init__(self, frame, pid, data): self.hotkey = self.control_group #: The type of update being performed, 0 (set),1 (add),2 (get) - self.update_type = data['control_group_update'] + self.update_type = data["control_group_update"] #: The type of mask to apply. One of None, Mask, OneIndices, ZeroIndices - self.mask_type = data['remove_mask'][0] + self.mask_type = data["remove_mask"][0] #: The data for the mask - self.mask_data = data['remove_mask'][1] + self.mask_data = data["remove_mask"][1] class SetControlGroupEvent(ControlGroupEvent): @@ -521,29 +590,32 @@ class CameraEvent(GameEvent): It does not matter why the camera changed, this event simply records the current state of the camera after changing. """ + def __init__(self, frame, pid, data): super(CameraEvent, self).__init__(frame, pid) #: The x coordinate of the center of the camera - self.x = (data['target']['x'] if data['target'] is not None else 0)/256.0 + self.x = (data["target"]["x"] if data["target"] is not None else 0) / 256.0 #: The y coordinate of the center of the camera - self.y = (data['target']['y'] if data['target'] is not None else 0)/256.0 + self.y = (data["target"]["y"] if data["target"] is not None else 0) / 256.0 #: The location of the center of the camera self.location = (self.x, self.y) #: The distance to the camera target ?? - self.distance = data['distance'] + self.distance = data["distance"] #: The current pitch of the camera - self.pitch = data['pitch'] + self.pitch = data["pitch"] #: The current yaw of the camera - self.yaw = data['yaw'] + self.yaw = data["yaw"] def __str__(self): - return self._str_prefix() + "{0} at ({1}, {2})".format(self.name, self.x, self.y) + return self._str_prefix() + "{0} at ({1}, {2})".format( + self.name, self.x, self.y + ) @loggable @@ -552,6 +624,7 @@ class ResourceTradeEvent(GameEvent): Generated when a player trades resources with another player. But not when fullfulling resource requests. """ + def __init__(self, frame, pid, data): super(ResourceTradeEvent, self).__init__(frame, pid) @@ -562,13 +635,13 @@ def __init__(self, frame, pid, data): self.sender = None #: The id of the player receiving the resources - self.recipient_id = data['recipient_id'] + self.recipient_id = data["recipient_id"] #: A reference to the player receiving the resources self.recipient = None #: An array of resources sent - self.resources = data['resources'] + self.resources = data["resources"] #: Amount minerals sent self.minerals = self.resources[0] if len(self.resources) >= 1 else None @@ -583,18 +656,21 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format(self.minerals, self.vespene, self.terrazine, self.custom, self.recipient) + return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format( + self.minerals, self.vespene, self.terrazine, self.custom, self.recipient + ) class ResourceRequestEvent(GameEvent): """ Generated when a player creates a resource request. """ + def __init__(self, frame, pid, data): super(ResourceRequestEvent, self).__init__(frame, pid) #: An array of resources sent - self.resources = data['resources'] + self.resources = data["resources"] #: Amount minerals sent self.minerals = self.resources[0] if len(self.resources) >= 1 else None @@ -609,40 +685,45 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format(self.minerals, self.vespene, self.terrazine, self.custom) + return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( + self.minerals, self.vespene, self.terrazine, self.custom + ) class ResourceRequestFulfillEvent(GameEvent): """ Generated when a player accepts a resource request. """ + def __init__(self, frame, pid, data): super(ResourceRequestFulfillEvent, self).__init__(frame, pid) #: The id of the request being fulfilled - self.request_id = data['request_id'] + self.request_id = data["request_id"] class ResourceRequestCancelEvent(GameEvent): """ Generated when a player cancels their resource request. """ + def __init__(self, frame, pid, data): super(ResourceRequestCancelEvent, self).__init__(frame, pid) #: The id of the request being cancelled - self.request_id = data['request_id'] + self.request_id = data["request_id"] class HijackReplayGameEvent(GameEvent): """ Generated when players take over from a replay. """ + def __init__(self, frame, pid, data): super(HijackReplayGameEvent, self).__init__(frame, pid) #: The method used. Not sure what 0/1 represent - self.method = data['method'] + self.method = data["method"] #: Information on the users hijacking the game - self.user_infos = data['user_infos'] + self.user_infos = data["user_infos"] diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index e9859733..2fdb9cfd 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -11,6 +11,7 @@ class MessageEvent(Event): """ Parent class for all message events. """ + def __init__(self, frame, pid): #: The user id (or player id for older replays) of the person that generated the event. self.pid = pid @@ -25,7 +26,7 @@ def __init__(self, frame, pid): self.name = self.__class__.__name__ def _str_prefix(self): - player_name = self.player.name if getattr(self, 'pid', 16) != 16 else "Global" + player_name = self.player.name if getattr(self, "pid", 16) != 16 else "Global" return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) def __str__(self): @@ -37,6 +38,7 @@ class ChatEvent(MessageEvent): """ Records in-game chat events. """ + def __init__(self, frame, pid, target, text): super(ChatEvent, self).__init__(frame, pid) #: The numerical target type. 0 = to all; 2 = to allies; 4 = to observers. @@ -46,13 +48,13 @@ def __init__(self, frame, pid, target, text): self.text = text #: Flag marked true of message was to all. - self.to_all = (self.target == 0) + self.to_all = self.target == 0 #: Flag marked true of message was to allies. - self.to_allies = (self.target == 2) + self.to_allies = self.target == 2 #: Flag marked true of message was to observers. - self.to_observers = (self.target == 4) + self.to_observers = self.target == 4 @loggable @@ -60,6 +62,7 @@ class ProgressEvent(MessageEvent): """ Sent during the load screen to update load process for other clients. """ + def __init__(self, frame, pid, progress): super(ProgressEvent, self).__init__(frame, pid) @@ -72,6 +75,7 @@ class PingEvent(MessageEvent): """ Records pings made by players in game. """ + def __init__(self, frame, pid, target, x, y): super(PingEvent, self).__init__(frame, pid) @@ -79,13 +83,13 @@ def __init__(self, frame, pid, target, x, y): self.target = target #: Flag marked true of message was to all. - self.to_all = (self.target == 0) + self.to_all = self.target == 0 #: Flag marked true of message was to allies. - self.to_allies = (self.target == 2) + self.to_allies = self.target == 2 #: Flag marked true of message was to observers. - self.to_observers = (self.target == 4) + self.to_observers = self.target == 4 #: The x coordinate of the target location self.x = x diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index fe3e6f78..985a9107 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -13,10 +13,11 @@ class TrackerEvent(Event): """ Parent class for all tracker events. """ + def __init__(self, frames): #: The frame of the game this event was applied #: Ignore all but the lowest 32 bits of the frame - self.frame = frames % 2**32 + self.frame = frames % 2 ** 32 #: The second of the game (game time not real time) this event was applied self.second = self.frame >> 4 @@ -36,6 +37,7 @@ def __str__(self): class PlayerSetupEvent(TrackerEvent): """ Sent during game setup to help us organize players better """ + def __init__(self, frames, data, build): super(PlayerSetupEvent, self).__init__(frames) @@ -64,6 +66,7 @@ class PlayerStatsEvent(TrackerEvent): In 1v1 games, the above behavior can cause the losing player to have 2 events generated at the end of the game. One for leaving and one for the end of the game. """ + def __init__(self, frames, data, build): super(PlayerStatsEvent, self).__init__(frames) @@ -101,7 +104,11 @@ def __init__(self, frames, data, build): self.minerals_used_in_progress_technology = clamp(self.stats[7]) #: The total mineral cost of all things in progress - self.minerals_used_in_progress = self.minerals_used_in_progress_army + self.minerals_used_in_progress_economy + self.minerals_used_in_progress_technology + self.minerals_used_in_progress = ( + self.minerals_used_in_progress_army + + self.minerals_used_in_progress_economy + + self.minerals_used_in_progress_technology + ) #: The total vespene cost of army units (buildings?) currently being built/queued self.vespene_used_in_progress_army = clamp(self.stats[8]) @@ -113,10 +120,16 @@ def __init__(self, frames, data, build): self.vespene_used_in_progress_technology = clamp(self.stats[10]) #: The total vespene cost of all things in progress - self.vespene_used_in_progress = self.vespene_used_in_progress_army + self.vespene_used_in_progress_economy + self.vespene_used_in_progress_technology + self.vespene_used_in_progress = ( + self.vespene_used_in_progress_army + + self.vespene_used_in_progress_economy + + self.vespene_used_in_progress_technology + ) #: The total cost of all things in progress - self.resources_used_in_progress = self.minerals_used_in_progress + self.vespene_used_in_progress + self.resources_used_in_progress = ( + self.minerals_used_in_progress + self.vespene_used_in_progress + ) #: The total mineral cost of current army units (buildings?) self.minerals_used_current_army = clamp(self.stats[11]) @@ -128,7 +141,11 @@ def __init__(self, frames, data, build): self.minerals_used_current_technology = clamp(self.stats[13]) #: The total mineral cost of all current things - self.minerals_used_current = self.minerals_used_current_army + self.minerals_used_current_economy + self.minerals_used_current_technology + self.minerals_used_current = ( + self.minerals_used_current_army + + self.minerals_used_current_economy + + self.minerals_used_current_technology + ) #: The total vespene cost of current army units (buildings?) self.vespene_used_current_army = clamp(self.stats[14]) @@ -140,10 +157,16 @@ def __init__(self, frames, data, build): self.vespene_used_current_technology = clamp(self.stats[16]) #: The total vepsene cost of all current things - self.vespene_used_current = self.vespene_used_current_army + self.vespene_used_current_economy + self.vespene_used_current_technology + self.vespene_used_current = ( + self.vespene_used_current_army + + self.vespene_used_current_economy + + self.vespene_used_current_technology + ) #: The total cost of all things current - self.resources_used_current = self.minerals_used_current + self.vespene_used_current + self.resources_used_current = ( + self.minerals_used_current + self.vespene_used_current + ) #: The total mineral cost of all army units (buildings?) lost self.minerals_lost_army = clamp(self.stats[17]) @@ -155,7 +178,11 @@ def __init__(self, frames, data, build): self.minerals_lost_technology = clamp(self.stats[19]) #: The total mineral cost of all lost things - self.minerals_lost = self.minerals_lost_army + self.minerals_lost_economy + self.minerals_lost_technology + self.minerals_lost = ( + self.minerals_lost_army + + self.minerals_lost_economy + + self.minerals_lost_technology + ) #: The total vespene cost of all army units (buildings?) lost self.vespene_lost_army = clamp(self.stats[20]) @@ -167,7 +194,11 @@ def __init__(self, frames, data, build): self.vespene_lost_technology = clamp(self.stats[22]) #: The total vepsene cost of all lost things - self.vespene_lost = self.vespene_lost_army + self.vespene_lost_economy + self.vespene_lost_technology + self.vespene_lost = ( + self.vespene_lost_army + + self.vespene_lost_economy + + self.vespene_lost_technology + ) #: The total resource cost of all lost things self.resources_lost = self.minerals_lost + self.vespene_lost @@ -182,7 +213,11 @@ def __init__(self, frames, data, build): self.minerals_killed_technology = clamp(self.stats[25]) #: The total mineral value of all killed things - self.minerals_killed = self.minerals_killed_army + self.minerals_killed_economy + self.minerals_killed_technology + self.minerals_killed = ( + self.minerals_killed_army + + self.minerals_killed_economy + + self.minerals_killed_technology + ) #: The total vespene value of enemy army units (buildings?) killed self.vespene_killed_army = clamp(self.stats[26]) @@ -194,7 +229,11 @@ def __init__(self, frames, data, build): self.vespene_killed_technology = clamp(self.stats[28]) #: The total vespene cost of all killed things - self.vespene_killed = self.vespene_killed_army + self.vespene_killed_economy + self.vespene_killed_technology + self.vespene_killed = ( + self.vespene_killed_army + + self.vespene_killed_economy + + self.vespene_killed_technology + ) #: The total resource cost of all killed things self.resources_killed = self.minerals_killed + self.vespene_killed @@ -215,10 +254,14 @@ def __init__(self, frames, data, build): self.ff_minerals_lost_army = clamp(self.stats[33]) if build >= 26490 else None #: Minerals of economy value lost to friendly fire - self.ff_minerals_lost_economy = clamp(self.stats[34]) if build >= 26490 else None + self.ff_minerals_lost_economy = ( + clamp(self.stats[34]) if build >= 26490 else None + ) #: Minerals of technology value lost to friendly fire - self.ff_minerals_lost_technology = clamp(self.stats[35]) if build >= 26490 else None + self.ff_minerals_lost_technology = ( + clamp(self.stats[35]) if build >= 26490 else None + ) #: Vespene of army value lost to friendly fire self.ff_vespene_lost_army = clamp(self.stats[36]) if build >= 26490 else None @@ -227,7 +270,9 @@ def __init__(self, frames, data, build): self.ff_vespene_lost_economy = clamp(self.stats[37]) if build >= 26490 else None #: Vespene of technology value lost to friendly fire - self.ff_vespene_lost_technology = clamp(self.stats[38]) if build >= 26490 else None + self.ff_vespene_lost_technology = ( + clamp(self.stats[38]) if build >= 26490 else None + ) def __str__(self): return self._str_prefix() + "{0: >15} - Stats Update".format(str(self.player)) @@ -244,6 +289,7 @@ class UnitBornEvent(TrackerEvent): :class:`~sc2reader.event.game.CommandEvent` game events where the command is a train unit command. """ + def __init__(self, frames, data, build): super(UnitBornEvent, self).__init__(frames) @@ -260,7 +306,7 @@ def __init__(self, frames, data, build): self.unit = None #: The unit type name of the unit being born - self.unit_type_name = data[2].decode('utf8') + self.unit_type_name = data[2].decode("utf8") #: The id of the player that controls this unit. self.control_pid = data[3] @@ -291,7 +337,9 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit born {1}".format(str(self.unit_upkeeper), self.unit) + return self._str_prefix() + "{0: >15} - Unit born {1}".format( + str(self.unit_upkeeper), self.unit + ) class UnitDiedEvent(TrackerEvent): @@ -299,6 +347,7 @@ class UnitDiedEvent(TrackerEvent): Generated when a unit dies or is removed from the game for any reason. Reasons include morphing, merging, and getting killed. """ + def __init__(self, frames, data, build): super(UnitDiedEvent, self).__init__(frames) @@ -358,10 +407,14 @@ def __init__(self, frames, data, build): self.killing_unit_index = data[5] self.killing_unit_recycle = data[6] if self.killing_unit_index: - self.killing_unit_id = self.killing_unit_index << 18 | self.killing_unit_recycle + self.killing_unit_id = ( + self.killing_unit_index << 18 | self.killing_unit_recycle + ) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit died {1}.".format(str(self.unit.owner), self.unit) + return self._str_prefix() + "{0: >15} - Unit died {1}.".format( + str(self.unit.owner), self.unit + ) class UnitOwnerChangeEvent(TrackerEvent): @@ -369,6 +422,7 @@ class UnitOwnerChangeEvent(TrackerEvent): Generated when either ownership or control of a unit is changed. Neural Parasite is an example of an action that would generate this event. """ + def __init__(self, frames, data, build): super(UnitOwnerChangeEvent, self).__init__(frames) @@ -397,7 +451,9 @@ def __init__(self, frames, data, build): self.unit_controller = None def __str__(self): - return self._str_prefix() + "{0: >15} took {1}".format(str(self.unit_upkeeper), self.unit) + return self._str_prefix() + "{0: >15} took {1}".format( + str(self.unit_upkeeper), self.unit + ) class UnitTypeChangeEvent(TrackerEvent): @@ -406,6 +462,7 @@ class UnitTypeChangeEvent(TrackerEvent): Lair, Hive) and mode switches (Sieging Tanks, Phasing prisms, Burrowing roaches). There may be some other situations where a unit transformation is a type change and not a new unit. """ + def __init__(self, frames, data, build): super(UnitTypeChangeEvent, self).__init__(frames) @@ -422,16 +479,19 @@ def __init__(self, frames, data, build): self.unit = None #: The the new unit type name - self.unit_type_name = data[2].decode('utf8') + self.unit_type_name = data[2].decode("utf8") def __str__(self): - return self._str_prefix() + "{0: >15} - Unit {0} type changed to {1}".format(str(self.unit.owner), self.unit, self.unit_type_name) + return self._str_prefix() + "{0: >15} - Unit {0} type changed to {1}".format( + str(self.unit.owner), self.unit, self.unit_type_name + ) class UpgradeCompleteEvent(TrackerEvent): """ Generated when a player completes an upgrade. """ + def __init__(self, frames, data, build): super(UpgradeCompleteEvent, self).__init__(frames) @@ -442,13 +502,15 @@ def __init__(self, frames, data, build): self.player = None #: The name of the upgrade - self.upgrade_type_name = data[1].decode('utf8') + self.upgrade_type_name = data[1].decode("utf8") #: The number of times this upgrade as been researched self.count = data[2] def __str__(self): - return self._str_prefix() + "{0: >15} - {1} upgrade completed".format(str(self.player), self.upgrade_type_name) + return self._str_prefix() + "{0: >15} - {1} upgrade completed".format( + str(self.player), self.upgrade_type_name + ) class UnitInitEvent(TrackerEvent): @@ -457,6 +519,7 @@ class UnitInitEvent(TrackerEvent): initiated. This applies only to units which are started in game before they are finished. Primary examples being buildings and warp-in units. """ + def __init__(self, frames, data, build): super(UnitInitEvent, self).__init__(frames) @@ -473,7 +536,7 @@ def __init__(self, frames, data, build): self.unit = None #: The the new unit type name - self.unit_type_name = data[2].decode('utf8') + self.unit_type_name = data[2].decode("utf8") #: The id of the player that controls this unit. self.control_pid = data[3] @@ -504,7 +567,9 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit initiated {1}".format(str(self.unit_upkeeper), self.unit) + return self._str_prefix() + "{0: >15} - Unit initiated {1}".format( + str(self.unit_upkeeper), self.unit + ) class UnitDoneEvent(TrackerEvent): @@ -512,6 +577,7 @@ class UnitDoneEvent(TrackerEvent): The counter part to the :class:`UnitInitEvent`, generated by the game engine when an initiated unit is completed. E.g. warp-in finished, building finished, morph complete. """ + def __init__(self, frames, data, build): super(UnitDoneEvent, self).__init__(frames) @@ -528,7 +594,9 @@ def __init__(self, frames, data, build): self.unit = None def __str__(self): - return self._str_prefix() + "{0: >15} - Unit {1} done".format(str(self.unit.owner), self.unit) + return self._str_prefix() + "{0: >15} - Unit {1} done".format( + str(self.unit.owner), self.unit + ) class UnitPositionsEvent(TrackerEvent): @@ -537,6 +605,7 @@ class UnitPositionsEvent(TrackerEvent): the last interval. If more than 255 units were damaged, then the first 255 are reported and the remaining units are carried into the next interval. """ + def __init__(self, frames, data, build): super(UnitPositionsEvent, self).__init__(frames) diff --git a/sc2reader/exceptions.py b/sc2reader/exceptions.py index caf13b86..dff2d31b 100644 --- a/sc2reader/exceptions.py +++ b/sc2reader/exceptions.py @@ -27,7 +27,7 @@ class MultipleMatchingFilesError(SC2ReaderError): class ReadError(SC2ReaderError): - def __init__(self, msg, type, location, replay=None, game_events=[], buffer=None): + def __init__(self, msg, type, location, replay=None, game_events=[], buffer=None): self.__dict__.update(locals()) super(ReadError, self).__init__(msg) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index ad465609..f7669ef7 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -6,7 +6,12 @@ from sc2reader import log_utils from sc2reader.utils import Length -from sc2reader.factories.plugins.utils import PlayerSelection, GameState, JSONDateEncoder, plugin +from sc2reader.factories.plugins.utils import ( + PlayerSelection, + GameState, + JSONDateEncoder, + plugin, +) @plugin @@ -23,70 +28,78 @@ def toDict(replay): observers = list() for observer in replay.observers: messages = list() - for message in getattr(observer, 'messages', list()): - messages.append({ - 'time': message.time.seconds, - 'text': message.text, - 'is_public': message.to_all - }) - observers.append({ - 'name': getattr(observer, 'name', None), - 'pid': getattr(observer, 'pid', None), - 'messages': messages, - }) + for message in getattr(observer, "messages", list()): + messages.append( + { + "time": message.time.seconds, + "text": message.text, + "is_public": message.to_all, + } + ) + observers.append( + { + "name": getattr(observer, "name", None), + "pid": getattr(observer, "pid", None), + "messages": messages, + } + ) # Build players into dictionary players = list() for player in replay.players: messages = list() for message in player.messages: - messages.append({ - 'time': message.time.seconds, - 'text': message.text, - 'is_public': message.to_all - }) - players.append({ - 'avg_apm': getattr(player, 'avg_apm', None), - 'color': player.color.__dict__ if hasattr(player, 'color') else None, - 'handicap': getattr(player, 'handicap', None), - 'name': getattr(player, 'name', None), - 'pick_race': getattr(player, 'pick_race', None), - 'pid': getattr(player, 'pid', None), - 'play_race': getattr(player, 'play_race', None), - 'result': getattr(player, 'result', None), - 'type': getattr(player, 'type', None), - 'uid': getattr(player, 'uid', None), - 'url': getattr(player, 'url', None), - 'messages': messages, - }) + messages.append( + { + "time": message.time.seconds, + "text": message.text, + "is_public": message.to_all, + } + ) + players.append( + { + "avg_apm": getattr(player, "avg_apm", None), + "color": player.color.__dict__ if hasattr(player, "color") else None, + "handicap": getattr(player, "handicap", None), + "name": getattr(player, "name", None), + "pick_race": getattr(player, "pick_race", None), + "pid": getattr(player, "pid", None), + "play_race": getattr(player, "play_race", None), + "result": getattr(player, "result", None), + "type": getattr(player, "type", None), + "uid": getattr(player, "uid", None), + "url": getattr(player, "url", None), + "messages": messages, + } + ) # Consolidate replay metadata into dictionary return { - 'region': getattr(replay, 'region', None), - 'map_name': getattr(replay, 'map_name', None), - 'file_time': getattr(replay, 'file_time', None), - 'filehash': getattr(replay, 'filehash', None), - 'unix_timestamp': getattr(replay, 'unix_timestamp', None), - 'date': getattr(replay, 'date', None), - 'utc_date': getattr(replay, 'utc_date', None), - 'speed': getattr(replay, 'speed', None), - 'category': getattr(replay, 'category', None), - 'type': getattr(replay, 'type', None), - 'is_ladder': getattr(replay, 'is_ladder', False), - 'is_private': getattr(replay, 'is_private', False), - 'filename': getattr(replay, 'filename', None), - 'file_time': getattr(replay, 'file_time', None), - 'frames': getattr(replay, 'frames', None), - 'build': getattr(replay, 'build', None), - 'release': getattr(replay, 'release_string', None), - 'game_fps': getattr(replay, 'game_fps', None), - 'game_length': getattr(getattr(replay, 'game_length', None), 'seconds', None), - 'players': players, - 'observers': observers, - 'real_length': getattr(getattr(replay, 'real_length', None), 'seconds', None), - 'real_type': getattr(replay, 'real_type', None), - 'time_zone': getattr(replay, 'time_zone', None), - 'versions': getattr(replay, 'versions', None) + "region": getattr(replay, "region", None), + "map_name": getattr(replay, "map_name", None), + "file_time": getattr(replay, "file_time", None), + "filehash": getattr(replay, "filehash", None), + "unix_timestamp": getattr(replay, "unix_timestamp", None), + "date": getattr(replay, "date", None), + "utc_date": getattr(replay, "utc_date", None), + "speed": getattr(replay, "speed", None), + "category": getattr(replay, "category", None), + "type": getattr(replay, "type", None), + "is_ladder": getattr(replay, "is_ladder", False), + "is_private": getattr(replay, "is_private", False), + "filename": getattr(replay, "filename", None), + "file_time": getattr(replay, "file_time", None), + "frames": getattr(replay, "frames", None), + "build": getattr(replay, "build", None), + "release": getattr(replay, "release_string", None), + "game_fps": getattr(replay, "game_fps", None), + "game_length": getattr(getattr(replay, "game_length", None), "seconds", None), + "players": players, + "observers": observers, + "real_length": getattr(getattr(replay, "real_length", None), "seconds", None), + "real_type": getattr(replay, "real_type", None), + "time_zone": getattr(replay, "time_zone", None), + "versions": getattr(replay, "versions", None), } @@ -106,23 +119,30 @@ def APMTracker(replay): player.seconds_played = replay.length.seconds for event in player.events: - if event.name == 'SelectionEvent' or 'CommandEvent' in event.name or 'ControlGroup' in event.name: + if ( + event.name == "SelectionEvent" + or "CommandEvent" in event.name + or "ControlGroup" in event.name + ): player.aps[event.second] += 1.4 - player.apm[int(event.second/60)] += 1.4 + player.apm[int(event.second / 60)] += 1.4 - elif event.name == 'PlayerLeaveEvent': + elif event.name == "PlayerLeaveEvent": player.seconds_played = event.second if len(player.apm) > 0: - player.avg_apm = sum(player.aps.values())/float(player.seconds_played)*60 + player.avg_apm = ( + sum(player.aps.values()) / float(player.seconds_played) * 60 + ) else: player.avg_apm = 0 return replay + @plugin def SelectionTracker(replay): - debug = replay.opt['debug'] + debug = replay.opt["debug"] logger = log_utils.get_logger(SelectionTracker) for person in replay.entities: @@ -131,37 +151,60 @@ def SelectionTracker(replay): player_selections = GameState(PlayerSelection()) for event in person.events: error = False - if event.name == 'SelectionEvent': + if event.name == "SelectionEvent": selections = player_selections[event.frame] control_group = selections[event.control_group].copy() error = not control_group.deselect(event.mask_type, event.mask_data) control_group.select(event.new_units) selections[event.control_group] = control_group if debug: - logger.info("[{0}] {1} selected {2} units: {3}".format(Length(seconds=event.second), person.name, len(selections[0x0A].objects), selections[0x0A])) - - elif event.name == 'SetControlGroupEvent': + logger.info( + "[{0}] {1} selected {2} units: {3}".format( + Length(seconds=event.second), + person.name, + len(selections[0x0A].objects), + selections[0x0A], + ) + ) + + elif event.name == "SetControlGroupEvent": selections = player_selections[event.frame] selections[event.control_group] = selections[0x0A].copy() if debug: - logger.info("[{0}] {1} set hotkey {2} to current selection".format(Length(seconds=event.second), person.name, event.hotkey)) + logger.info( + "[{0}] {1} set hotkey {2} to current selection".format( + Length(seconds=event.second), person.name, event.hotkey + ) + ) - elif event.name == 'AddToControlGroupEvent': + elif event.name == "AddToControlGroupEvent": selections = player_selections[event.frame] control_group = selections[event.control_group].copy() error = not control_group.deselect(event.mask_type, event.mask_data) control_group.select(selections[0x0A].objects) selections[event.control_group] = control_group if debug: - logger.info("[{0}] {1} added current selection to hotkey {2}".format(Length(seconds=event.second), person.name, event.hotkey)) + logger.info( + "[{0}] {1} added current selection to hotkey {2}".format( + Length(seconds=event.second), person.name, event.hotkey + ) + ) - elif event.name == 'GetControlGroupEvent': + elif event.name == "GetControlGroupEvent": selections = player_selections[event.frame] control_group = selections[event.control_group].copy() error = not control_group.deselect(event.mask_type, event.mask_data) selections[0xA] = control_group if debug: - logger.info("[{0}] {1} retrieved hotkey {2}, {3} units: {4}".format(Length(seconds=event.second), person.name, event.control_group, len(selections[0x0A].objects), selections[0x0A])) + logger.info( + "[{0}] {1} retrieved hotkey {2}, {3} units: {4}".format( + Length(seconds=event.second), + person.name, + event.control_group, + len(selections[0x0A].objects), + selections[0x0A], + ) + ) else: continue @@ -172,7 +215,11 @@ def SelectionTracker(replay): if error: person.selection_errors += 1 if debug: - logger.warn("Error detected in deselection mode {0}.".format(event.mask_type)) + logger.warn( + "Error detected in deselection mode {0}.".format( + event.mask_type + ) + ) person.selection = player_selections # Not a real lock, so don't change it! diff --git a/sc2reader/factories/plugins/utils.py b/sc2reader/factories/plugins/utils.py index cf3559ea..e472b86e 100644 --- a/sc2reader/factories/plugins/utils.py +++ b/sc2reader/factories/plugins/utils.py @@ -18,7 +18,9 @@ def call(*args, **kwargs): opt = kwargs.copy() opt.update(options) return func(*args, **opt) + return call + return wrapper @@ -56,7 +58,7 @@ def __getitem__(self, frame): else: # Copy the previous state and use it as our basis here state = self[prev_frame] - if hasattr(state, 'copy'): + if hasattr(state, "copy"): state = state.copy() self[frame] = state @@ -76,34 +78,41 @@ def __init__(self, objects=None): self.objects = objects or list() def select(self, new_objects): - new_set = set(self.objects+list(new_objects)) + new_set = set(self.objects + list(new_objects)) self.objects = sorted(new_set, key=lambda obj: obj.id) def deselect(self, mode, data): """Returns false if there was a data error when deselecting""" size = len(self.objects) - if mode == 'None': + if mode == "None": return True - elif mode == 'Mask': + elif mode == "Mask": """ Deselect objects according to deselect mask """ mask = data if len(mask) < size: # pad to the right - mask = mask+[False]*(len(self.objects)-len(mask)) + mask = mask + [False] * (len(self.objects) - len(mask)) self.logger.debug("Deselection Mask: {0}".format(mask)) - self.objects = [obj for (slct, obj) in filter(lambda slct_obj: not slct_obj[0], zip(mask, self.objects))] + self.objects = [ + obj + for (slct, obj) in filter( + lambda slct_obj: not slct_obj[0], zip(mask, self.objects) + ) + ] return len(mask) <= size - elif mode == 'OneIndices': + elif mode == "OneIndices": """ Deselect objects according to indexes """ clean_data = list(filter(lambda i: i < size, data)) - self.objects = [self.objects[i] for i in range(len(self.objects)) if i not in clean_data] + self.objects = [ + self.objects[i] for i in range(len(self.objects)) if i not in clean_data + ] return len(clean_data) == len(data) - elif mode == 'ZeroIndices': + elif mode == "ZeroIndices": """ Deselect objects according to indexes """ clean_data = list(filter(lambda i: i < size, data)) self.objects = [self.objects[i] for i in clean_data] @@ -113,7 +122,7 @@ def deselect(self, mode, data): return False def __str__(self): - return ', '.join(str(obj) for obj in self.objects) + return ", ".join(str(obj) for obj in self.objects) def copy(self): return UnitSelection(self.objects[:]) diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index 36020b2d..690797a3 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -64,8 +64,8 @@ class SC2Factory(object): _resource_name_map = dict(replay=Replay, map=Map) default_options = { - Resource: {'debug': False}, - Replay: {'load_level': 4, 'load_map': False}, + Resource: {"debug": False}, + Replay: {"load_level": 4, "load_map": False}, } def __init__(self, **options): @@ -86,7 +86,9 @@ def load_replay(self, source, options=None, **new_options): def load_replays(self, sources, options=None, **new_options): """Loads a collection of sc2replay files, returns a generator.""" - return self.load_all(Replay, sources, options, extension='SC2Replay', **new_options) + return self.load_all( + Replay, sources, options, extension="SC2Replay", **new_options + ) def load_localization(self, source, options=None, **new_options): """Loads a single s2ml file. Accepts file path, url, or file object.""" @@ -94,7 +96,9 @@ def load_localization(self, source, options=None, **new_options): def load_localizations(self, sources, options=None, **new_options): """Loads a collection of s2ml files, returns a generator.""" - return self.load_all(Localization, sources, options, extension='s2ml', **new_options) + return self.load_all( + Localization, sources, options, extension="s2ml", **new_options + ) def load_map(self, source, options=None, **new_options): """Loads a single s2ma file. Accepts file path, url, or file object.""" @@ -102,7 +106,7 @@ def load_map(self, source, options=None, **new_options): def load_maps(self, sources, options=None, **new_options): """Loads a collection of s2ma files, returns a generator.""" - return self.load_all(Map, sources, options, extension='s2ma', **new_options) + return self.load_all(Map, sources, options, extension="s2ma", **new_options) def load_game_summary(self, source, options=None, **new_options): """Loads a single s2gs file. Accepts file path, url, or file object.""" @@ -110,7 +114,9 @@ def load_game_summary(self, source, options=None, **new_options): def load_game_summaries(self, sources, options=None, **new_options): """Loads a collection of s2gs files, returns a generator.""" - return self.load_all(GameSummary, sources, options, extension='s2gs', **new_options) + return self.load_all( + GameSummary, sources, options, extension="s2gs", **new_options + ) def configure(self, cls=None, **options): """ Configures the factory to use the supplied options. If cls is specified @@ -144,7 +150,7 @@ def load_all(self, cls, sources, options=None, **new_options): # Internal Functions def _load(self, cls, resource, filename, options): obj = cls(resource, filename=filename, factory=self, **options) - for plugin in options.get('plugins', self._get_plugins(cls)): + for plugin in options.get("plugins", self._get_plugins(cls)): obj = plugin(obj) return obj @@ -180,7 +186,7 @@ def load_remote_resource_contents(self, resource, **options): def load_local_resource_contents(self, location, **options): # Extract the contents so we can close the file - with open(location, 'rb') as resource_file: + with open(location, "rb") as resource_file: return resource_file.read() def _load_resource(self, resource, options=None, **new_options): @@ -191,11 +197,11 @@ def _load_resource(self, resource, options=None, **new_options): resource = resource.url if isinstance(resource, basestring): - if re.match(r'https?://', resource): + if re.match(r"https?://", resource): contents = self.load_remote_resource_contents(resource, **options) else: - directory = options.get('directory', '') + directory = options.get("directory", "") location = os.path.join(directory, resource) contents = self.load_local_resource_contents(location, **options) @@ -206,31 +212,32 @@ def _load_resource(self, resource, options=None, **new_options): else: # Totally not designed for large files!! # We need a multiread resource, so wrap it in BytesIO - if not hasattr(resource, 'seek'): + if not hasattr(resource, "seek"): resource = BytesIO(resource.read()) - resource_name = getattr(resource, 'name', 'Unknown') + resource_name = getattr(resource, "name", "Unknown") - if options.get('verbose', None): + if options.get("verbose", None): print(resource_name) return (resource, resource_name) class CachedSC2Factory(SC2Factory): - def get_remote_cache_key(self, remote_resource): # Strip the port and use the domain as the bucket # and use the full path as the key parseresult = urlparse(remote_resource) - bucket = re.sub(r':.*', '', parseresult.netloc) - key = parseresult.path.strip('/') + bucket = re.sub(r":.*", "", parseresult.netloc) + key = parseresult.path.strip("/") return (bucket, key) def load_remote_resource_contents(self, remote_resource, **options): cache_key = self.get_remote_cache_key(remote_resource) if not self.cache_has(cache_key): - resource = super(CachedSC2Factory, self).load_remote_resource_contents(remote_resource, **options) + resource = super(CachedSC2Factory, self).load_remote_resource_contents( + remote_resource, **options + ) self.cache_set(cache_key, resource) else: resource = self.cache_get(cache_key) @@ -254,13 +261,20 @@ class FileCachedSC2Factory(CachedSC2Factory): Caches remote depot resources on the file system in the ``cache_dir``. """ + def __init__(self, cache_dir, **options): super(FileCachedSC2Factory, self).__init__(**options) self.cache_dir = os.path.abspath(cache_dir) if not os.path.isdir(self.cache_dir): - raise ValueError("cache_dir ({0}) must be an existing directory.".format(self.cache_dir)) + raise ValueError( + "cache_dir ({0}) must be an existing directory.".format(self.cache_dir) + ) elif not os.access(self.cache_dir, os.F_OK | os.W_OK | os.R_OK): - raise ValueError("Must have read/write access to {0} for local file caching.".format(self.cache_dir)) + raise ValueError( + "Must have read/write access to {0} for local file caching.".format( + self.cache_dir + ) + ) def cache_has(self, cache_key): return os.path.exists(self.cache_path(cache_key)) @@ -274,7 +288,7 @@ def cache_set(self, cache_key, value): if not os.path.exists(bucket_dir): os.makedirs(bucket_dir) - with open(cache_path, 'wb') as out: + with open(cache_path, "wb") as out: out.write(value) def cache_path(self, cache_key): @@ -290,6 +304,7 @@ class DictCachedSC2Factory(CachedSC2Factory): Caches remote depot resources in memory. Does not write to the file system. The cache is effectively cleared when the process exits. """ + def __init__(self, cache_max_size=0, **options): super(DictCachedSC2Factory, self).__init__(**options) self.cache_dict = dict() @@ -322,8 +337,11 @@ class DoubleCachedSC2Factory(DictCachedSC2Factory, FileCachedSC2Factory): Caches remote depot resources to the file system AND holds a subset of them in memory for more efficient access. """ + def __init__(self, cache_dir, cache_max_size=0, **options): - super(DoubleCachedSC2Factory, self).__init__(cache_max_size, cache_dir=cache_dir, **options) + super(DoubleCachedSC2Factory, self).__init__( + cache_max_size, cache_dir=cache_dir, **options + ) def load_remote_resource_contents(self, remote_resource, **options): cache_key = self.get_remote_cache_key(remote_resource) @@ -332,7 +350,9 @@ def load_remote_resource_contents(self, remote_resource, **options): return DictCachedSC2Factory.cache_get(self, cache_key) if not FileCachedSC2Factory.cache_has(self, cache_key): - resource = SC2Factory.load_remote_resource_contents(self, remote_resource, **options) + resource = SC2Factory.load_remote_resource_contents( + self, remote_resource, **options + ) FileCachedSC2Factory.cache_set(self, cache_key, resource) else: resource = FileCachedSC2Factory.cache_get(self, cache_key) diff --git a/sc2reader/log_utils.py b/sc2reader/log_utils.py index 58a442d9..a53656c8 100644 --- a/sc2reader/log_utils.py +++ b/sc2reader/log_utils.py @@ -22,6 +22,7 @@ class NullHandler(logging.Handler): a NullHandler and add it to the top-level logger of the library module or package. """ + def handle(self, record): pass @@ -31,34 +32,35 @@ def emit(self, record): def createLock(self): self.lock = None + LEVEL_MAP = dict( DEBUG=logging.DEBUG, INFO=logging.INFO, WARN=logging.WARN, ERROR=logging.ERROR, - CRITICAL=logging.CRITICAL + CRITICAL=logging.CRITICAL, ) def setup(): - logging.getLogger('sc2reader').addHandler(NullHandler()) + logging.getLogger("sc2reader").addHandler(NullHandler()) -def log_to_file(filename, level='WARN', format=None, datefmt=None, **options): +def log_to_file(filename, level="WARN", format=None, datefmt=None, **options): add_log_handler(logging.FileHandler(filename, **options), level, format, datefmt) -def log_to_console(level='WARN', format=None, datefmt=None, **options): +def log_to_console(level="WARN", format=None, datefmt=None, **options): add_log_handler(logging.StreamHandler(**options), level, format, datefmt) -def add_log_handler(handler, level='WARN', format=None, datefmt=None): +def add_log_handler(handler, level="WARN", format=None, datefmt=None): handler.setFormatter(logging.Formatter(format, datefmt)) if isinstance(level, basestring): level = LEVEL_MAP[level] - logger = logging.getLogger('sc2reader') + logger = logging.getLogger("sc2reader") logger.setLevel(level) logger.addHandler(handler) @@ -73,7 +75,7 @@ def get_logger(entity): :param entity: The entity for which we want a logger. """ try: - return logging.getLogger(entity.__module__+'.'+entity.__name__) + return logging.getLogger(entity.__module__ + "." + entity.__name__) except AttributeError: raise TypeError("Cannot retrieve logger for {0}.".format(entity)) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index eaa2e28b..04dcfc84 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -9,7 +9,7 @@ from sc2reader.decoders import ByteDecoder from sc2reader.constants import * -Location = namedtuple('Location', ['x', 'y']) +Location = namedtuple("Location", ["x", "y"]) class Team(object): @@ -48,15 +48,17 @@ def lineup(self): A string representation of the team play races like PP or TPZZ. Random pick races are not reflected in this string """ - return ''.join(sorted(p.play_race[0].upper() for p in self.players)) + return "".join(sorted(p.play_race[0].upper() for p in self.players)) @property def hash(self): - raw_hash = ','.join(sorted(p.url for p in self.players)) + raw_hash = ",".join(sorted(p.url for p in self.players)) return hashlib.sha256(raw_hash).hexdigest() def __str__(self): - return "Team {0}: {1}".format(self.number, ", ".join([str(p) for p in self.players])) + return "Team {0}: {1}".format( + self.number, ", ".join([str(p) for p in self.players]) + ) def __repr__(self): return str(self) @@ -64,7 +66,6 @@ def __repr__(self): @log_utils.loggable class Attribute(object): - def __init__(self, header, attr_id, player, value): self.header = header self.id = attr_id @@ -90,6 +91,7 @@ class Entity(object): :param integer sid: The entity's unique slot id. :param dict slot_data: The slot data associated with this entity """ + def __init__(self, sid, slot_data): #: The entity's unique in-game slot id self.sid = int(sid) @@ -98,36 +100,36 @@ def __init__(self, sid, slot_data): self.slot_data = slot_data #: The player's handicap as set prior to game start, ranges from 50-100 - self.handicap = slot_data['handicap'] + self.handicap = slot_data["handicap"] #: The entity's team number. None for observers self.team_id = None - if slot_data['team_id'] is not None: - self.team_id = slot_data['team_id'] + 1 + if slot_data["team_id"] is not None: + self.team_id = slot_data["team_id"] + 1 #: A flag indicating if the person is a human or computer #: Really just a shortcut for isinstance(entity, User) - self.is_human = slot_data['control'] == 2 + self.is_human = slot_data["control"] == 2 #: A flag indicating the entity's observer status. #: Really just a shortcut for isinstance(entity, Observer). - self.is_observer = slot_data['observe'] != 0 + self.is_observer = slot_data["observe"] != 0 #: A flag marking this entity as a referee (can talk to players) - self.is_referee = slot_data['observe'] == 2 + self.is_referee = slot_data["observe"] == 2 #: - self.hero_name = slot_data['hero'] + self.hero_name = slot_data["hero"] #: - self.hero_skin = slot_data['skin'] + self.hero_skin = slot_data["skin"] #: - self.hero_mount = slot_data['mount'] + self.hero_mount = slot_data["mount"] #: The unique Battle.net account identifier in the form of #: -S2-- - self.toon_handle = slot_data['toon_handle'] + self.toon_handle = slot_data["toon_handle"] toon_handle = self.toon_handle or "0-S2-0-0" parts = toon_handle.split("-") @@ -144,7 +146,7 @@ def __init__(self, sid, slot_data): self.toon_id = int(parts[3]) #: A index to the user that is the leader of the archon team - self.archon_leader_id = slot_data['tandem_leader_user_id'] + self.archon_leader_id = slot_data["tandem_leader_user_id"] #: A list of :class:`Event` objects representing all the game events #: generated by the person over the course of the game @@ -164,6 +166,7 @@ class Player(object): :param dict detail_data: The detail data associated with this player :param dict attribute_data: The attribute data associated with this player """ + def __init__(self, pid, slot_data, detail_data, attribute_data): #: The player's unique in-game player id self.pid = int(pid) @@ -179,9 +182,9 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): #: The player result, one of "Win", "Loss", or None self.result = None - if detail_data['result'] == 1: + if detail_data["result"] == 1: self.result = "Win" - elif detail_data['result'] == 2: + elif detail_data["result"] == 2: self.result = "Loss" #: A reference to the player's :class:`Team` object @@ -189,37 +192,37 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): #: The race the player picked prior to the game starting. #: One of Protoss, Terran, Zerg, Random - self.pick_race = attribute_data.get('Race', 'Unknown') + self.pick_race = attribute_data.get("Race", "Unknown") #: The difficulty setting for the player. Always Medium for human players. #: Very Easy, Easy, Medium, Hard, Harder, Very hard, Elite, Insane, #: Cheater 2 (Resources), Cheater 1 (Vision) - self.difficulty = attribute_data.get('Difficulty', 'Unknown') + self.difficulty = attribute_data.get("Difficulty", "Unknown") #: The race the player played the game with. #: One of Protoss, Terran, Zerg - self.play_race = LOCALIZED_RACES.get(detail_data['race'], detail_data['race']) + self.play_race = LOCALIZED_RACES.get(detail_data["race"], detail_data["race"]) #: The co-op commander the player picked #: Kerrigan, Raynor, etc. - self.commander = slot_data['commander'] + self.commander = slot_data["commander"] if self.commander is not None: - self.commander = self.commander.decode('utf8') + self.commander = self.commander.decode("utf8") #: The level of the co-op commander #: 1-15 or None - self.commander_level = slot_data['commander_level'] + self.commander_level = slot_data["commander_level"] #: The mastery level of the co-op commander #: >0 or None - self.commander_mastery_level = slot_data['commander_mastery_talents'] + self.commander_mastery_level = slot_data["commander_mastery_talents"] #: The mastery talents picked for the co-op commander #: list of longs of length 6, each between 0 and 30 - self.commander_mastery_talents = slot_data['commander_mastery_talents'] + self.commander_mastery_talents = slot_data["commander_mastery_talents"] #: A reference to a :class:`~sc2reader.utils.Color` object representing the player's color - self.color = utils.Color(**detail_data['color']) + self.color = utils.Color(**detail_data["color"]) #: A list of references to the :class:`~sc2reader.data.Unit` objects the player had this game self.units = list() @@ -228,15 +231,15 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): self.killed_units = list() #: The Battle.net region the entity is registered to - self.region = GATEWAY_LOOKUP[detail_data['bnet']['region']] + self.region = GATEWAY_LOOKUP[detail_data["bnet"]["region"]] #: The Battle.net subregion the entity is registered to - self.subregion = detail_data['bnet']['subregion'] + self.subregion = detail_data["bnet"]["subregion"] #: The Battle.net acount identifier. Used to construct the #: bnet profile url. This value can be zero for games #: played offline when a user was not logged in to battle.net. - self.toon_id = detail_data['bnet']['uid'] + self.toon_id = detail_data["bnet"]["uid"] class User(object): @@ -244,8 +247,11 @@ class User(object): :param integer uid: The user's unique user id :param dict init_data: The init data associated with this user """ + #: The Battle.net profile url template - URL_TEMPLATE = "http://{region}.battle.net/sc2/en/profile/{toon_id}/{subregion}/{name}/" + URL_TEMPLATE = ( + "http://{region}.battle.net/sc2/en/profile/{toon_id}/{subregion}/{name}/" + ) def __init__(self, uid, init_data): #: The user's unique in-game user id @@ -255,17 +261,17 @@ def __init__(self, uid, init_data): self.init_data = init_data #: The user's Battle.net clan tag at the time of the game - self.clan_tag = init_data['clan_tag'] + self.clan_tag = init_data["clan_tag"] #: The user's Battle.net name at the time of the game - self.name = init_data['name'] + self.name = init_data["name"] #: The user's combined Battle.net race levels - self.combined_race_levels = init_data['combined_race_levels'] + self.combined_race_levels = init_data["combined_race_levels"] #: The highest 1v1 league achieved by the user in the current season with 1 as Bronze and #: 7 as Grandmaster. 8 seems to indicate that there is no current season 1v1 ranking. - self.highest_league = init_data['highest_league'] + self.highest_league = init_data["highest_league"] #: A flag indicating if this person was the one who recorded the game. #: This is deprecated because it doesn't actually work. @@ -274,7 +280,9 @@ def __init__(self, uid, init_data): @property def url(self): """The player's formatted Battle.net profile url""" - return self.URL_TEMPLATE.format(**self.__dict__) # region=self.region, toon_id=self.toon_id, subregion=self.subregion, name=self.name.('utf8')) + return self.URL_TEMPLATE.format( + **self.__dict__ + ) # region=self.region, toon_id=self.toon_id, subregion=self.subregion, name=self.name.('utf8')) class Observer(Entity, User): @@ -286,6 +294,7 @@ class Observer(Entity, User): :param dict init_data: The init data associated with this user :param integer pid: The player's unique player id. """ + def __init__(self, sid, slot_data, uid, init_data, pid): Entity.__init__(self, sid, slot_data) User.__init__(self, uid, init_data) @@ -309,12 +318,13 @@ class Computer(Entity, Player): :param dict detail_data: The detail data associated with this player :param dict attribute_data: The attribute data associated with this player """ + def __init__(self, sid, slot_data, pid, detail_data, attribute_data): Entity.__init__(self, sid, slot_data) Player.__init__(self, pid, slot_data, detail_data, attribute_data) #: The auto-generated in-game name for this computer player - self.name = detail_data['name'] + self.name = detail_data["name"] def __str__(self): return "Player {0} - {1} ({2})".format(self.pid, self.name, self.play_race) @@ -334,7 +344,10 @@ class Participant(Entity, User, Player): :param dict detail_data: The detail data associated with this player :param dict attribute_data: The attribute data associated with this player """ - def __init__(self, sid, slot_data, uid, init_data, pid, detail_data, attribute_data): + + def __init__( + self, sid, slot_data, uid, init_data, pid, detail_data, attribute_data + ): Entity.__init__(self, sid, slot_data) User.__init__(self, uid, init_data) Player.__init__(self, pid, slot_data, detail_data, attribute_data) @@ -346,7 +359,7 @@ def __repr__(self): return str(self) -class PlayerSummary(): +class PlayerSummary: """ Resents a player as loaded from a :class:`~sc2reader.resources.GameSummary` file. @@ -400,25 +413,29 @@ def __init__(self, pid): def __str__(self): if not self.is_ai: - return 'User {0}-S2-{1}-{2}'.format(self.region.upper(), self.subregion, self.bnetid) + return "User {0}-S2-{1}-{2}".format( + self.region.upper(), self.subregion, self.bnetid + ) else: - return 'AI ({0})'.format(self.play_race) + return "AI ({0})".format(self.play_race) def __repr__(self): return str(self) def get_stats(self): - s = '' + s = "" for k in self.stats: - s += '{0}: {1}\n'.format(self.stats_pretty_names[k], self.stats[k]) + s += "{0}: {1}\n".format(self.stats_pretty_names[k], self.stats[k]) return s.strip() -BuildEntry = namedtuple('BuildEntry', ['supply', 'total_supply', 'time', 'order', 'build_index']) +BuildEntry = namedtuple( + "BuildEntry", ["supply", "total_supply", "time", "order", "build_index"] +) # TODO: Are there libraries with classes like this in them -class Graph(): +class Graph: """ A class to represent a graph on the score screen. Derived from data in the :class:`~sc2reader.resources.GameSummary` file. @@ -454,6 +471,7 @@ class MapInfoPlayer(object): """ Describes the player data as found in the MapInfo document of SC2Map archives. """ + def __init__(self, pid, control, color, race, unknown, start_point, ai, decal): #: The pid of the player self.pid = pid @@ -512,12 +530,13 @@ class MapInfo(object): """ Represents the data encoded into the MapInfo file inside every SC2Map archive """ + def __init__(self, contents): # According to http://www.galaxywiki.net/MapInfo_(File_Format) # With a couple small changes for version 0x20+ - data = ByteDecoder(contents, endian='LITTLE') + data = ByteDecoder(contents, endian="LITTLE") magic = data.read_string(4) - if magic != 'MapI': + if magic != "MapI": self.logger.warn("Invalid MapInfo file: {0}".format(magic)) return @@ -549,13 +568,13 @@ def __init__(self, contents): if self.large_preview_type == 2: self.large_preview_path = data.read_cstring() - if self.version >= 0x1f: + if self.version >= 0x1F: self.unknown3 = data.read_cstring() if self.version >= 0x26: self.unknown4 = data.read_cstring() - if self.version >= 0x1f: + if self.version >= 0x1F: self.unknown5 = data.read_uint32() self.unknown6 = data.read_uint32() @@ -579,7 +598,7 @@ def __init__(self, contents): self.camera_top = data.read_uint32() #: The map base height (what is that?). This value is 4096*Base Height in the editor (giving a decimal value). - self.base_height = data.read_uint32()/4096 + self.base_height = data.read_uint32() / 4096 # Leave early so we dont barf. Turns out ggtracker doesnt need # any of the map data thats loaded below. @@ -592,7 +611,7 @@ def __init__(self, contents): self.load_screen_path = data.read_cstring() #: Unknown string, usually empty - self.unknown7 = data.read_bytes(data.read_uint16()).decode('utf8') + self.unknown7 = data.read_bytes(data.read_uint16()).decode("utf8") #: Load screen image scaling strategy: 0 = normal, 1 = aspect scaling, 2 = stretch the image. self.load_screen_scaling = data.read_uint32() @@ -639,7 +658,7 @@ def __init__(self, contents): if self.version >= 0x19: self.unknown9 = data.read_bytes(8) - if self.version >= 0x1f: + if self.version >= 0x1F: self.unknown10 = data.read_bytes(9) if self.version >= 0x20: @@ -651,16 +670,18 @@ def __init__(self, contents): #: A list of references to :class:`MapInfoPlayer` objects self.players = list() for i in range(self.player_count): - self.players.append(MapInfoPlayer( - pid=data.read_uint8(), - control=data.read_uint32(), - color=data.read_uint32(), - race=data.read_cstring(), - unknown=data.read_uint32(), - start_point=data.read_uint32(), - ai=data.read_uint32(), - decal=data.read_cstring(), - )) + self.players.append( + MapInfoPlayer( + pid=data.read_uint8(), + control=data.read_uint32(), + color=data.read_uint32(), + race=data.read_cstring(), + unknown=data.read_uint32(), + start_point=data.read_uint32(), + ai=data.read_uint32(), + decal=data.read_cstring(), + ) + ) #: A list of the start location point indexes used in Basic Team Settings. #: The editor limits these to only Start Locations and not regular points. @@ -686,7 +707,9 @@ def __init__(self, contents): # } # } #: A bit array of flags mapping out the player alliances - self.alliance_flags = data.read_uint(int(math.ceil(self.alliance_flags_length/8.0))) + self.alliance_flags = data.read_uint( + int(math.ceil(self.alliance_flags_length / 8.0)) + ) #: A list of the advanced start location point indexes used in Advanced Team Settings. #: The editor limits these to only Start Locations and not regular points. @@ -722,7 +745,7 @@ def __init__(self, contents): # } # } #: A bit array of flags mapping out the player enemies. - self.enemy_flags = data.read_uint(int(math.ceil(self.enemy_flags_length/8.0))) + self.enemy_flags = data.read_uint(int(math.ceil(self.enemy_flags_length / 8.0))) if data.length != data.tell(): self.logger.warn("Not all of the MapInfo file was read!") diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 54bf3f54..8b24f60e 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -16,28 +16,54 @@ class InitDataReader(object): def __call__(self, data, replay): data = BitPackedDecoder(data) result = dict( - user_initial_data=[dict( - name=data.read_aligned_string(data.read_uint8()), - clan_tag=data.read_aligned_string(data.read_uint8()) if replay.base_build >= 24764 and data.read_bool() else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) if replay.base_build >= 27950 and data.read_bool() else None, - highest_league=data.read_uint8() if replay.base_build >= 24764 and data.read_bool() else None, - combined_race_levels=data.read_uint32() if replay.base_build >= 24764 and data.read_bool() else None, - random_seed=data.read_uint32(), - race_preference=data.read_uint8() if data.read_bool() else None, - team_preference=data.read_uint8() if replay.base_build >= 16561 and data.read_bool() else None, - test_map=data.read_bool(), - test_auto=data.read_bool(), - examine=data.read_bool() if replay.base_build >= 21955 else None, - custom_interface=data.read_bool() if replay.base_build >= 24764 else None, - test_type=data.read_uint32() if replay.base_build >= 34784 else None, - observe=data.read_bits(2), - hero=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - skin=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - mount=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - toon_handle=data.read_aligned_string(data.read_bits(7)) if replay.base_build >= 34784 else None, - scaled_rating=data.read_uint32()-2147483648 if replay.base_build >= 54518 and data.read_bool() else None, - ) for i in range(data.read_bits(5))], - + user_initial_data=[ + dict( + name=data.read_aligned_string(data.read_uint8()), + clan_tag=data.read_aligned_string(data.read_uint8()) + if replay.base_build >= 24764 and data.read_bool() + else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) + if replay.base_build >= 27950 and data.read_bool() + else None, + highest_league=data.read_uint8() + if replay.base_build >= 24764 and data.read_bool() + else None, + combined_race_levels=data.read_uint32() + if replay.base_build >= 24764 and data.read_bool() + else None, + random_seed=data.read_uint32(), + race_preference=data.read_uint8() if data.read_bool() else None, + team_preference=data.read_uint8() + if replay.base_build >= 16561 and data.read_bool() + else None, + test_map=data.read_bool(), + test_auto=data.read_bool(), + examine=data.read_bool() if replay.base_build >= 21955 else None, + custom_interface=data.read_bool() + if replay.base_build >= 24764 + else None, + test_type=data.read_uint32() + if replay.base_build >= 34784 + else None, + observe=data.read_bits(2), + hero=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + skin=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + mount=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + toon_handle=data.read_aligned_string(data.read_bits(7)) + if replay.base_build >= 34784 + else None, + scaled_rating=data.read_uint32() - 2147483648 + if replay.base_build >= 54518 and data.read_bool() + else None, + ) + for i in range(data.read_bits(5)) + ], game_description=dict( random_value=data.read_uint32(), game_cache_name=data.read_aligned_string(data.read_bits(10)), @@ -48,110 +74,213 @@ def __call__(self, data, replay): random_races=data.read_bool(), battle_net=data.read_bool(), amm=data.read_bool(), - ranked=data.read_bool() if replay.base_build >= 34784 and replay.base_build < 38215 else None, + ranked=data.read_bool() + if replay.base_build >= 34784 and replay.base_build < 38215 + else None, competitive=data.read_bool(), practice=data.read_bool() if replay.base_build >= 34784 else None, - cooperative=data.read_bool() if replay.base_build >= 34784 else None, + cooperative=data.read_bool() + if replay.base_build >= 34784 + else None, no_victory_or_defeat=data.read_bool(), - hero_duplicates_allowed=data.read_bool() if replay.base_build >= 34784 else None, + hero_duplicates_allowed=data.read_bool() + if replay.base_build >= 34784 + else None, fog=data.read_bits(2), observers=data.read_bits(2), user_difficulty=data.read_bits(2), - client_debug_flags=data.read_uint64() if replay.base_build >= 22612 else None, - build_coach_enabled=data.read_bool() if replay.base_build >= 59587 else None, + client_debug_flags=data.read_uint64() + if replay.base_build >= 22612 + else None, + build_coach_enabled=data.read_bool() + if replay.base_build >= 59587 + else None, ), game_speed=data.read_bits(3), game_type=data.read_bits(3), max_users=data.read_bits(5), max_observers=data.read_bits(5), max_players=data.read_bits(5), - max_teams=data.read_bits(4)+1, - max_colors=data.read_bits(6) if replay.base_build >= 17266 else data.read_bits(5)+1, - max_races=data.read_uint8()+1, - max_controls=data.read_uint8()+(0 if replay.base_build >= 26490 else 1), + max_teams=data.read_bits(4) + 1, + max_colors=data.read_bits(6) + if replay.base_build >= 17266 + else data.read_bits(5) + 1, + max_races=data.read_uint8() + 1, + max_controls=data.read_uint8() + + (0 if replay.base_build >= 26490 else 1), map_size_x=data.read_uint8(), map_size_y=data.read_uint8(), map_file_sync_checksum=data.read_uint32(), map_file_name=data.read_aligned_string(data.read_bits(11)), map_author_name=data.read_aligned_string(data.read_uint8()), mod_file_sync_checksum=data.read_uint32(), - slot_descriptions=[dict( - allowed_colors=data.read_bits(data.read_bits(6)), - allowed_races=data.read_bits(data.read_uint8()), - allowedDifficulty=data.read_bits(data.read_bits(6)), - allowedControls=data.read_bits(data.read_uint8()), - allowed_observe_types=data.read_bits(data.read_bits(2)), - allowed_ai_builds=data.read_bits(data.read_bits(8 if replay.base_build >= 38749 else 7)) if replay.base_build >= 23925 else None, - ) for i in range(data.read_bits(5))], + slot_descriptions=[ + dict( + allowed_colors=data.read_bits(data.read_bits(6)), + allowed_races=data.read_bits(data.read_uint8()), + allowedDifficulty=data.read_bits(data.read_bits(6)), + allowedControls=data.read_bits(data.read_uint8()), + allowed_observe_types=data.read_bits(data.read_bits(2)), + allowed_ai_builds=data.read_bits( + data.read_bits(8 if replay.base_build >= 38749 else 7) + ) + if replay.base_build >= 23925 + else None, + ) + for i in range(data.read_bits(5)) + ], default_difficulty=data.read_bits(6), - default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) if replay.base_build >= 23925 else None, - cache_handles=[DepotFile(data.read_aligned_bytes(40)) for i in range(data.read_bits(6 if replay.base_build >= 21955 else 4))], - has_extension_mod=data.read_bool() if replay.base_build >= 27950 else None, - has_nonBlizzardExtensionMod=data.read_bool() if replay.base_build >= 42932 else None, + default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 23925 + else None, + cache_handles=[ + DepotFile(data.read_aligned_bytes(40)) + for i in range( + data.read_bits(6 if replay.base_build >= 21955 else 4) + ) + ], + has_extension_mod=data.read_bool() + if replay.base_build >= 27950 + else None, + has_nonBlizzardExtensionMod=data.read_bool() + if replay.base_build >= 42932 + else None, is_blizzardMap=data.read_bool(), is_premade_ffa=data.read_bool(), is_coop_mode=data.read_bool() if replay.base_build >= 23925 else None, - is_realtime_mode=data.read_bool() if replay.base_build >= 54518 else None, + is_realtime_mode=data.read_bool() + if replay.base_build >= 54518 + else None, ), - lobby_state=dict( phase=data.read_bits(3), max_users=data.read_bits(5), max_observers=data.read_bits(5), - slots=[dict( - control=data.read_uint8(), - user_id=data.read_bits(4) if data.read_bool() else None, - team_id=data.read_bits(4), - colorPref=data.read_bits(5) if data.read_bool() else None, - race_pref=data.read_uint8() if data.read_bool() else None, - difficulty=data.read_bits(6), - ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) if replay.base_build >= 23925 else None, - handicap=data.read_bits(7), - observe=data.read_bits(2), - logo_index=data.read_uint32() if replay.base_build >= 32283 else None, - hero=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - skin=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - mount=data.read_aligned_string(data.read_bits(9)) if replay.base_build >= 34784 else None, - artifacts=[dict( - type_struct=data.read_aligned_string(data.read_bits(9)), - ) for i in range(data.read_bits(4))] if replay.base_build >= 34784 else None, - working_set_slot_id=data.read_uint8() if replay.base_build >= 24764 and data.read_bool() else None, - rewards=[data.read_uint32() for i in range(data.read_bits(17 if replay.base_build >= 34784 else 6 if replay.base_build >= 24764 else 5))], - toon_handle=data.read_aligned_string(data.read_bits(7)) if replay.base_build >= 17266 else None, - licenses=[data.read_uint32() for i in range(data.read_bits(13 if replay.base_build >= 70154 else 9))] if replay.base_build >= 19132 else [], - tandem_leader_user_id=data.read_bits(4) if replay.base_build >= 34784 and data.read_bool() else None, - commander=data.read_aligned_bytes(data.read_bits(9)) if replay.base_build >= 34784 else None, - commander_level=data.read_uint32() if replay.base_build >= 36442 else None, - has_silence_penalty=data.read_bool() if replay.base_build >= 38215 else None, - tandem_id=data.read_bits(4) if replay.base_build >= 39576 and data.read_bool() else None, - commander_mastery_level=data.read_uint32() if replay.base_build >= 42932 else None, - commander_mastery_talents=[data.read_uint32() for i in range(data.read_bits(3))] if replay.base_build >= 42932 else None, - reward_overrides=[[data.read_uint32(), [data.read_uint32() for i in range(data.read_bits(17))]] for j in range(data.read_bits(17))] if replay.base_build >= 47185 else None, - ) for i in range(data.read_bits(5))], + slots=[ + dict( + control=data.read_uint8(), + user_id=data.read_bits(4) if data.read_bool() else None, + team_id=data.read_bits(4), + colorPref=data.read_bits(5) if data.read_bool() else None, + race_pref=data.read_uint8() if data.read_bool() else None, + difficulty=data.read_bits(6), + ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 23925 + else None, + handicap=data.read_bits(7), + observe=data.read_bits(2), + logo_index=data.read_uint32() + if replay.base_build >= 32283 + else None, + hero=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + skin=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + mount=data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + artifacts=[ + dict( + type_struct=data.read_aligned_string(data.read_bits(9)) + ) + for i in range(data.read_bits(4)) + ] + if replay.base_build >= 34784 + else None, + working_set_slot_id=data.read_uint8() + if replay.base_build >= 24764 and data.read_bool() + else None, + rewards=[ + data.read_uint32() + for i in range( + data.read_bits( + 17 + if replay.base_build >= 34784 + else 6 + if replay.base_build >= 24764 + else 5 + ) + ) + ], + toon_handle=data.read_aligned_string(data.read_bits(7)) + if replay.base_build >= 17266 + else None, + licenses=[ + data.read_uint32() + for i in range( + data.read_bits(13 if replay.base_build >= 70154 else 9) + ) + ] + if replay.base_build >= 19132 + else [], + tandem_leader_user_id=data.read_bits(4) + if replay.base_build >= 34784 and data.read_bool() + else None, + commander=data.read_aligned_bytes(data.read_bits(9)) + if replay.base_build >= 34784 + else None, + commander_level=data.read_uint32() + if replay.base_build >= 36442 + else None, + has_silence_penalty=data.read_bool() + if replay.base_build >= 38215 + else None, + tandem_id=data.read_bits(4) + if replay.base_build >= 39576 and data.read_bool() + else None, + commander_mastery_level=data.read_uint32() + if replay.base_build >= 42932 + else None, + commander_mastery_talents=[ + data.read_uint32() for i in range(data.read_bits(3)) + ] + if replay.base_build >= 42932 + else None, + reward_overrides=[ + [ + data.read_uint32(), + [data.read_uint32() for i in range(data.read_bits(17))], + ] + for j in range(data.read_bits(17)) + ] + if replay.base_build >= 47185 + else None, + ) + for i in range(data.read_bits(5)) + ], random_seed=data.read_uint32(), host_user_id=data.read_bits(4) if data.read_bool() else None, is_single_player=data.read_bool(), - picked_map_tag=data.read_uint8() if replay.base_build >= 36442 else None, + picked_map_tag=data.read_uint8() + if replay.base_build >= 36442 + else None, game_duration=data.read_uint32(), default_difficulty=data.read_bits(6), - default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) if replay.base_build >= 24764 else None, + default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 24764 + else None, ), ) if not data.done(): - raise ValueError("{0} bytes left!".format(data.length-data.tell())) + raise ValueError("{0} bytes left!".format(data.length - data.tell())) return result class AttributesEventsReader(object): def __call__(self, data, replay): - data = ByteDecoder(data, endian='LITTLE') + data = ByteDecoder(data, endian="LITTLE") data.read_bytes(5 if replay.base_build >= 17326 else 4) - result = [Attribute( - data.read_uint32(), - data.read_uint32(), - data.read_uint8(), - ''.join(reversed(data.read_string(4))), - ) for i in range(data.read_uint32())] + result = [ + Attribute( + data.read_uint32(), + data.read_uint32(), + data.read_uint8(), + "".join(reversed(data.read_string(4))), + ) + for i in range(data.read_uint32()) + ] if not data.done(): raise ValueError("Not all bytes used up!") return result @@ -161,44 +290,46 @@ class DetailsReader(object): def __call__(self, data, replay): details = BitPackedDecoder(data).read_struct() return dict( - players=[dict( - name=p[0].decode('utf8'), - bnet=dict( - region=p[1][0], - program_id=p[1][1], - subregion=p[1][2], - # name=p[1][3].decode('utf8'), # This is documented but never available - uid=p[1][4], - ), - race=p[2].decode('utf8'), - color=dict( - a=p[3][0], - r=p[3][1], - g=p[3][2], - b=p[3][3], - ), - control=p[4], - team=p[5], - handicap=p[6], - observe=p[7], - result=p[8], - working_set_slot=p[9] if replay.build >= 24764 else None, - hero=p[10] if replay.build >= 34784 and 10 in p else None, # hero appears to be present in Heroes replays but not StarCraft 2 replays - ) for p in details[0]], - map_name=details[1].decode('utf8'), + players=[ + dict( + name=p[0].decode("utf8"), + bnet=dict( + region=p[1][0], + program_id=p[1][1], + subregion=p[1][2], + # name=p[1][3].decode('utf8'), # This is documented but never available + uid=p[1][4], + ), + race=p[2].decode("utf8"), + color=dict(a=p[3][0], r=p[3][1], g=p[3][2], b=p[3][3]), + control=p[4], + team=p[5], + handicap=p[6], + observe=p[7], + result=p[8], + working_set_slot=p[9] if replay.build >= 24764 else None, + hero=p[10] + if replay.build >= 34784 and 10 in p + else None, # hero appears to be present in Heroes replays but not StarCraft 2 replays + ) + for p in details[0] + ], + map_name=details[1].decode("utf8"), difficulty=details[2], thumbnail=details[3][0], blizzard_map=details[4], file_time=details[5], utc_adjustment=details[6], description=details[7], - image_file_path=details[8].decode('utf8'), - map_file_name=details[9].decode('utf8'), + image_file_path=details[8].decode("utf8"), + map_file_name=details[9].decode("utf8"), cache_handles=[DepotFile(bytes) for bytes in details[10]], mini_save=details[11], game_speed=details[12], default_difficulty=details[13], - mod_paths=details[14] if (replay.build >= 22612 and replay.versions[1] == 1) else None, + mod_paths=details[14] + if (replay.build >= 22612 and replay.versions[1] == 1) + else None, campaign_index=details[15] if replay.versions[1] == 2 else None, restartAsTransitionMap=details[16] if replay.build > 26490 else None, ) @@ -223,12 +354,12 @@ def __call__(self, data, replay): elif flag == 1: # Client ping message recipient = data.read_bits(3 if replay.base_build >= 21955 else 2) - x = data.read_uint32()-2147483648 - y = data.read_uint32()-2147483648 + x = data.read_uint32() - 2147483648 + y = data.read_uint32() - 2147483648 pings.append(PingEvent(frame, pid, recipient, x, y)) elif flag == 2: # Loading progress message - progress = data.read_uint32()-2147483648 + progress = data.read_uint32() - 2147483648 packets.append(ProgressEvent(frame, pid, progress)) elif flag == 3: # Server ping message @@ -244,7 +375,6 @@ def __call__(self, data, replay): class GameEventsReader_Base(object): - def __init__(self): self.EVENT_DISPATCH = { 0: (None, self.unknown_event), @@ -318,8 +448,14 @@ def __init__(self): 90: (None, self.trigger_custom_dialog_dismissed_event), 91: (None, self.trigger_game_menu_item_selected_event), 92: (None, self.trigger_camera_move_event), - 93: (None, self.trigger_purchase_panel_selected_purchase_item_changed_event), - 94: (None, self.trigger_purchase_panel_selected_purchase_category_changed_event), + 93: ( + None, + self.trigger_purchase_panel_selected_purchase_item_changed_event, + ), + 94: ( + None, + self.trigger_purchase_panel_selected_purchase_category_changed_event, + ), 95: (None, self.trigger_button_pressed_event), 96: (None, self.trigger_game_credits_finished_event), } @@ -330,7 +466,7 @@ def __call__(self, data, replay): # method short cuts, avoid dict lookups EVENT_DISPATCH = self.EVENT_DISPATCH - debug = replay.opt['debug'] + debug = replay.opt["debug"] tell = data.tell read_frames = data.read_frames read_bits = data.read_bits @@ -358,33 +494,60 @@ def __call__(self, data, replay): # Otherwise throw a read error else: - raise ReadError("Event type {0} unknown at position {1}.".format(hex(event_type), hex(event_start)), event_type, event_start, replay, game_events, data) + raise ReadError( + "Event type {0} unknown at position {1}.".format( + hex(event_type), hex(event_start) + ), + event_type, + event_start, + replay, + game_events, + data, + ) byte_align() event_start = tell() return game_events except ParseError as e: - raise ReadError("Parse error '{0}' unknown at position {1}.".format(e.msg, hex(event_start)), event_type, event_start, replay, game_events, data) + raise ReadError( + "Parse error '{0}' unknown at position {1}.".format( + e.msg, hex(event_start) + ), + event_type, + event_start, + replay, + game_events, + data, + ) except EOFError as e: - raise ReadError("EOFError error '{0}' unknown at position {1}.".format(e.msg, hex(event_start)), event_type, event_start, replay, game_events, data) + raise ReadError( + "EOFError error '{0}' unknown at position {1}.".format( + e.msg, hex(event_start) + ), + event_type, + event_start, + replay, + game_events, + data, + ) # Don't want to do this more than once - SINGLE_BIT_MASKS = [0x1 << i for i in range(2**9)] + SINGLE_BIT_MASKS = [0x1 << i for i in range(2 ** 9)] def read_selection_bitmask(self, data, mask_length): bits_left = mask_length bits = data.read_bits(mask_length) mask = list() - shift_diff = (mask_length+data._bit_shift) % 8 - data._bit_shift + shift_diff = (mask_length + data._bit_shift) % 8 - data._bit_shift if shift_diff > 0: mask = [bits & data._lo_masks[shift_diff]] bits = bits >> shift_diff bits_left -= shift_diff elif shift_diff < 0: - mask = [bits & data._lo_masks[8+shift_diff]] - bits = bits >> (8+shift_diff) - bits_left -= 8+shift_diff + mask = [bits & data._lo_masks[8 + shift_diff]] + bits = bits >> (8 + shift_diff) + bits_left -= 8 + shift_diff # Now shift the rest of the bits off into the mask in byte-sized # chunks in reverse order. No idea why it'd be stored like this. @@ -394,7 +557,7 @@ def read_selection_bitmask(self, data, mask_length): bits_left -= 8 # Compile the finished mask into a large integer for bit checks - bit_mask = sum([c << (i*8) for i, c in enumerate(mask)]) + bit_mask = sum([c << (i * 8) for i, c in enumerate(mask)]) # Change mask representation from an int to a bit array with # True => Deselect, False => Keep @@ -402,24 +565,17 @@ def read_selection_bitmask(self, data, mask_length): class GameEventsReader_15405(GameEventsReader_Base): - def unknown_event(self, data): - return dict( - unknown=data.read_bytes(2) - ) + return dict(unknown=data.read_bytes(2)) def finished_loading_sync_event(self, data): return None def bank_file_event(self, data): - return dict( - name=data.read_aligned_string(data.read_bits(7)), - ) + return dict(name=data.read_aligned_string(data.read_bits(7))) def bank_section_event(self, data): - return dict( - name=data.read_aligned_string(data.read_bits(6)), - ) + return dict(name=data.read_aligned_string(data.read_bits(6))) def bank_key_event(self, data): return dict( @@ -473,10 +629,9 @@ def player_leave_event(self, data): def game_cheat_event(self, data): return dict( point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, y=data.read_uint32() - 2147483648 ), - time=data.read_uint32()-2147483648, + time=data.read_uint32() - 2147483648, verb=data.read_aligned_string(data.read_bits(10)), arguments=data.read_aligned_string(data.read_bits(10)), ) @@ -488,23 +643,25 @@ def command_event(self, data): ability_command_index=data.read_uint8(), ability_command_data=data.read_uint8(), ) - target_data = ('TargetUnit', dict( - flags=data.read_uint8(), - timer=data.read_uint8(), - )) + target_data = ( + "TargetUnit", + dict(flags=data.read_uint8(), timer=data.read_uint8()), + ) other_unit_tag = data.read_uint32() - target_data[1].update(dict( - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, - ), - )) + target_data[1].update( + dict( + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=None, + upkeep_player_id=data.read_bits(4) if data.read_bool() else None, + point=dict( + x=data.read_uint32() - 2147483648, + y=data.read_uint32() - 2147483648, + z=data.read_uint32() - 2147483648, + ), + ) + ) return dict( flags=flags, ability=ability, @@ -516,13 +673,16 @@ def selection_delta_event(self, data): return dict( control_group_index=data.read_bits(4), subgroup_index=data.read_uint8(), - remove_mask=('Mask', self.read_selection_bitmask(data, data.read_uint8())), - add_subgroups=[dict( - unit_link=data.read_uint16(), - subgroup_priority=None, - intra_subgroup_priority=data.read_uint8(), - count=data.read_uint8(), - ) for i in range(data.read_uint8())], + remove_mask=("Mask", self.read_selection_bitmask(data, data.read_uint8())), + add_subgroups=[ + dict( + unit_link=data.read_uint16(), + subgroup_priority=None, + intra_subgroup_priority=data.read_uint8(), + count=data.read_uint8(), + ) + for i in range(data.read_uint8()) + ], add_unit_tags=[data.read_uint32() for i in range(data.read_uint8())], ) @@ -530,7 +690,9 @@ def control_group_update_event(self, data): return dict( control_group_index=data.read_bits(4), control_group_update=data.read_bits(2), - remove_mask=('Mask', self.read_selection_bitmask(data, data.read_uint8())) if data.read_bool() else ('None', None), + remove_mask=("Mask", self.read_selection_bitmask(data, data.read_uint8())) + if data.read_bool() + else ("None", None), ) def selection_sync_check_event(self, data): @@ -543,46 +705,42 @@ def selection_sync_check_event(self, data): unit_tags_checksum=data.read_uint32(), subgroup_indices_checksum=data.read_uint32(), subgroups_checksum=data.read_uint32(), - ) + ), ) def resource_trade_event(self, data): return dict( recipient_id=data.read_bits(4), - resources=[data.read_uint32()-2147483648 for i in range(data.read_bits(3))], + resources=[ + data.read_uint32() - 2147483648 for i in range(data.read_bits(3)) + ], ) def trigger_chat_message_event(self, data): - return dict( - message=data.read_aligned_string(data.read_bits(10)), - ) + return dict(message=data.read_aligned_string(data.read_bits(10))) def ai_communicate_event(self, data): return dict( - beacon=data.read_uint8()-128, - ally=data.read_uint8()-128, - flags=data.read_uint8()-128, + beacon=data.read_uint8() - 128, + ally=data.read_uint8() - 128, + flags=data.read_uint8() - 128, build=None, target_unit_tag=data.read_uint32(), target_unit_link=data.read_uint16(), target_upkeep_player_id=data.read_bits(4) if data.read_bool() else None, target_control_player_id=None, target_point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, + y=data.read_uint32() - 2147483648, + z=data.read_uint32() - 2147483648, ), ) def set_absolute_game_speed_event(self, data): - return dict( - speed=data.read_bits(3), - ) + return dict(speed=data.read_bits(3)) def add_absolute_game_speed_event(self, data): - return dict( - delta=data.read_uint8()-128, - ) + return dict(delta=data.read_uint8() - 128) def broadcast_cheat_event(self, data): return dict( @@ -591,58 +749,38 @@ def broadcast_cheat_event(self, data): ) def alliance_event(self, data): - return dict( - alliance=data.read_uint32(), - control=data.read_uint32(), - ) + return dict(alliance=data.read_uint32(), control=data.read_uint32()) def unit_click_event(self, data): - return dict( - unit_tag=data.read_uint32(), - ) + return dict(unit_tag=data.read_uint32()) def unit_highlight_event(self, data): - return dict( - unit_tag=data.read_uint32(), - flags=data.read_uint8(), - ) + return dict(unit_tag=data.read_uint32(), flags=data.read_uint8()) def trigger_reply_selected_event(self, data): return dict( - conversation_id=data.read_uint32()-2147483648, - reply_id=data.read_uint32()-2147483648, + conversation_id=data.read_uint32() - 2147483648, + reply_id=data.read_uint32() - 2147483648, ) def trigger_skipped_event(self, data): return None def trigger_sound_length_query_event(self, data): - return dict( - sound_hash=data.read_uint32(), - length=data.read_uint32(), - ) + return dict(sound_hash=data.read_uint32(), length=data.read_uint32()) def trigger_sound_offset_event(self, data): - return dict( - sound=data.read_uint32(), - ) + return dict(sound=data.read_uint32()) def trigger_transmission_offset_event(self, data): - return dict( - transmission_id=data.read_uint32()-2147483648, - ) + return dict(transmission_id=data.read_uint32() - 2147483648) def trigger_transmission_complete_event(self, data): - return dict( - transmission_id=data.read_uint32()-2147483648, - ) + return dict(transmission_id=data.read_uint32() - 2147483648) def camera_update_event(self, data): return dict( - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ), + target=dict(x=data.read_uint16(), y=data.read_uint16()), distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -653,31 +791,30 @@ def trigger_abort_mission_event(self, data): return None def trigger_purchase_made_event(self, data): - return dict( - purchase_item_id=data.read_uint32()-2147483648, - ) + return dict(purchase_item_id=data.read_uint32() - 2147483648) def trigger_purchase_exit_event(self, data): return None def trigger_planet_mission_launched_event(self, data): - return dict( - difficulty_level=data.read_uint32()-2147483648, - ) + return dict(difficulty_level=data.read_uint32() - 2147483648) def trigger_planet_panel_canceled_event(self, data): return None def trigger_dialog_control_event(self, data): return dict( - control_id=data.read_uint32()-2147483648, - event_type=data.read_uint32()-2147483648, + control_id=data.read_uint32() - 2147483648, + event_type=data.read_uint32() - 2147483648, event_data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Checked', data.read_bool()), - 2: lambda: ('ValueChanged', data.read_uint32()), - 3: lambda: ('SelectionChanged', data.read_uint32()-2147483648), - 4: lambda: ('TextChanged', data.read_aligned_string(data.read_bits(11))), + 0: lambda: ("None", None), + 1: lambda: ("Checked", data.read_bool()), + 2: lambda: ("ValueChanged", data.read_uint32()), + 3: lambda: ("SelectionChanged", data.read_uint32() - 2147483648), + 4: lambda: ( + "TextChanged", + data.read_aligned_string(data.read_bits(11)), + ), }[data.read_bits(3)](), ) @@ -690,22 +827,17 @@ def trigger_sound_length_sync_event(self, data): ) def trigger_conversation_skipped_event(self, data): - return dict( - skip_type=data.read_int(1), - ) + return dict(skip_type=data.read_int(1)) def trigger_mouse_clicked_event(self, data): return dict( button=data.read_uint32(), down=data.read_bool(), - position_ui=dict( - x=data.read_uint32(), - y=data.read_uint32(), - ), + position_ui=dict(x=data.read_uint32(), y=data.read_uint32()), position_world=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, + y=data.read_uint32() - 2147483648, + z=data.read_uint32() - 2147483648, ), ) @@ -713,25 +845,16 @@ def trigger_planet_panel_replay_event(self, data): return None def trigger_soundtrack_done_event(self, data): - return dict( - soundtrack=data.read_uint32(), - ) + return dict(soundtrack=data.read_uint32()) def trigger_planet_mission_selected_event(self, data): - return dict( - planet_id=data.read_uint32()-2147483648, - ) + return dict(planet_id=data.read_uint32() - 2147483648) def trigger_key_pressed_event(self, data): - return dict( - key=data.read_uint8()-128, - flags=data.read_uint8()-128, - ) + return dict(key=data.read_uint8() - 128, flags=data.read_uint8() - 128) def trigger_movie_function_event(self, data): - return dict( - function_name=data.read_aligned_string(data.read_bits(7)), - ) + return dict(function_name=data.read_aligned_string(data.read_bits(7))) def trigger_planet_panel_birth_complete_event(self, data): return None @@ -741,18 +864,16 @@ def trigger_planet_panel_death_complete_event(self, data): def resource_request_event(self, data): return dict( - resources=[data.read_uint32()-2147483648 for i in range(data.read_bits(3))], + resources=[ + data.read_uint32() - 2147483648 for i in range(data.read_bits(3)) + ] ) def resource_request_fulfill_event(self, data): - return dict( - request_id=data.read_uint32()-2147483648, - ) + return dict(request_id=data.read_uint32() - 2147483648) def resource_request_cancel_event(self, data): - return dict( - request_id=data.read_uint32()-2147483648, - ) + return dict(request_id=data.read_uint32() - 2147483648) def trigger_research_panel_exit_event(self, data): return None @@ -761,14 +882,10 @@ def trigger_research_panel_purchase_event(self, data): return None def trigger_research_panel_selection_changed_event(self, data): - return dict( - item_id=data.read_uint32()-2147483648, - ) + return dict(item_id=data.read_uint32() - 2147483648) def lag_message_event(self, data): - return dict( - player_id=data.read_bits(4), - ) + return dict(player_id=data.read_bits(4)) def trigger_mercenary_panel_exit_event(self, data): return None @@ -777,9 +894,7 @@ def trigger_mercenary_panel_purchase_event(self, data): return None def trigger_mercenary_panel_selection_changed_event(self, data): - return dict( - item_id=data.read_uint32()-2147483648, - ) + return dict(item_id=data.read_uint32() - 2147483648) def trigger_victory_panel_exit_event(self, data): return None @@ -789,24 +904,18 @@ def trigger_battle_report_panel_exit_event(self, data): def trigger_battle_report_panel_play_mission_event(self, data): return dict( - battle_report_id=data.read_uint32()-2147483648, - difficulty_level=data.read_uint32()-2147483648, + battle_report_id=data.read_uint32() - 2147483648, + difficulty_level=data.read_uint32() - 2147483648, ) def trigger_battle_report_panel_play_scene_event(self, data): - return dict( - battle_report_id=data.read_uint32()-2147483648, - ) + return dict(battle_report_id=data.read_uint32() - 2147483648) def trigger_battle_report_panel_selection_changed_event(self, data): - return dict( - battle_report_id=data.read_uint32()-2147483648, - ) + return dict(battle_report_id=data.read_uint32() - 2147483648) def trigger_victory_panel_play_mission_again_event(self, data): - return dict( - difficulty_level=data.read_uint32()-2147483648, - ) + return dict(difficulty_level=data.read_uint32() - 2147483648) def trigger_movie_started_event(self, data): return None @@ -815,51 +924,34 @@ def trigger_movie_finished_event(self, data): return None def decrement_game_time_remaining_event(self, data): - return dict( - decrement_ms=data.read_uint32(), - ) + return dict(decrement_ms=data.read_uint32()) def trigger_portrait_loaded_event(self, data): - return dict( - portrait_id=data.read_uint32()-2147483648, - ) + return dict(portrait_id=data.read_uint32() - 2147483648) def trigger_custom_dialog_dismissed_event(self, data): - return dict( - result=data.read_uint32()-2147483648, - ) + return dict(result=data.read_uint32() - 2147483648) def trigger_game_menu_item_selected_event(self, data): - return dict( - game_menu_item_index=data.read_uint32()-2147483648, - ) + return dict(game_menu_item_index=data.read_uint32() - 2147483648) def trigger_camera_move_event(self, data): - return dict( - reason=data.read_uint8()-128, - ) + return dict(reason=data.read_uint8() - 128) def trigger_purchase_panel_selected_purchase_item_changed_event(self, data): - return dict( - item_id=data.read_uint32()-2147483648, - ) + return dict(item_id=data.read_uint32() - 2147483648) def trigger_purchase_panel_selected_purchase_category_changed_event(self, data): - return dict( - category_id=data.read_uint32()-2147483648, - ) + return dict(category_id=data.read_uint32() - 2147483648) def trigger_button_pressed_event(self, data): - return dict( - button=data.read_uint16(), - ) + return dict(button=data.read_uint16()) def trigger_game_credits_finished_event(self, data): return None class GameEventsReader_16561(GameEventsReader_15405): - def command_event(self, data): return dict( flags=data.read_bits(17), @@ -867,32 +959,42 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint8(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) + ), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint8(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), ), - )), - 3: lambda: ('Data', dict(data=data.read_uint32())), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), - other_unit_tag=data.read_uint32() if data.read_bool() else None + other_unit_tag=data.read_uint32() if data.read_bool() else None, ) def selection_delta_event(self, data): @@ -900,17 +1002,29 @@ def selection_delta_event(self, data): control_group_index=data.read_bits(4), subgroup_index=data.read_uint8(), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_uint8())), - 2: lambda: ('OneIndices', [data.read_uint8() for i in range(data.read_uint8())]), - 3: lambda: ('ZeroIndices', [data.read_uint8() for i in range(data.read_uint8())]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_uint8()), + ), + 2: lambda: ( + "OneIndices", + [data.read_uint8() for i in range(data.read_uint8())], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_uint8() for i in range(data.read_uint8())], + ), }[data.read_bits(2)](), - add_subgroups=[dict( - unit_link=data.read_uint16(), - subgroup_priority=None, - intra_subgroup_priority=data.read_uint8(), - count=data.read_uint8(), - ) for i in range(data.read_uint8())], + add_subgroups=[ + dict( + unit_link=data.read_uint16(), + subgroup_priority=None, + intra_subgroup_priority=data.read_uint8(), + count=data.read_uint8(), + ) + for i in range(data.read_uint8()) + ], add_unit_tags=[data.read_uint32() for i in range(data.read_uint8())], ) @@ -919,19 +1033,26 @@ def control_group_update_event(self, data): control_group_index=data.read_bits(4), control_group_update=data.read_bits(2), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_uint8())), - 2: lambda: ('OneIndices', [data.read_uint8() for i in range(data.read_uint8())]), - 3: lambda: ('ZeroIndices', [data.read_uint8() for i in range(data.read_uint8())]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_uint8()), + ), + 2: lambda: ( + "OneIndices", + [data.read_uint8() for i in range(data.read_uint8())], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_uint8() for i in range(data.read_uint8())], + ), }[data.read_bits(2)](), ) def decrement_game_time_remaining_event(self, data): # really this should be set to 19, and a new GameEventsReader_41743 should be introduced that specifies 32 bits. # but I dont care about ability to read old replays. - return dict( - decrement_ms=data.read_bits(32) - ) + return dict(decrement_ms=data.read_bits(32)) class GameEventsReader_16605(GameEventsReader_16561): @@ -947,13 +1068,10 @@ class GameEventsReader_16939(GameEventsReader_16755): class GameEventsReader_17326(GameEventsReader_16939): - def __init__(self): super(GameEventsReader_17326, self).__init__() - self.EVENT_DISPATCH.update({ - 59: (None, self.trigger_mouse_moved_event), - }) + self.EVENT_DISPATCH.update({59: (None, self.trigger_mouse_moved_event)}) def bank_signature_event(self, data): return dict( @@ -965,10 +1083,7 @@ def trigger_mouse_clicked_event(self, data): return dict( button=data.read_uint32(), down=data.read_bool(), - position_ui=dict( - x=data.read_bits(11), - y=data.read_bits(11), - ), + position_ui=dict(x=data.read_bits(11), y=data.read_bits(11)), position_world=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -978,14 +1093,11 @@ def trigger_mouse_clicked_event(self, data): def trigger_mouse_moved_event(self, data): return dict( - position_ui=dict( - x=data.read_bits(11), - y=data.read_bits(11), - ), + position_ui=dict(x=data.read_bits(11), y=data.read_bits(11)), position_world=dict( x=data.read_bits(20), y=data.read_bits(20), - z=data.read_uint32()-2147483648, + z=data.read_uint32() - 2147483648, ), ) @@ -995,7 +1107,6 @@ class GameEventsReader_18092(GameEventsReader_17326): class GameEventsReader_18574(GameEventsReader_18092): - def command_event(self, data): return dict( flags=data.read_bits(18), @@ -1003,32 +1114,42 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint8(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) ), - )), - 3: lambda: ('Data', dict(data=data.read_uint32())), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint8(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), + ), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), - other_unit_tag=data.read_uint32() if data.read_bool() else None + other_unit_tag=data.read_uint32() if data.read_bool() else None, ) @@ -1037,7 +1158,6 @@ class GameEventsReader_19132(GameEventsReader_18574): class GameEventsReader_19595(GameEventsReader_19132): - def command_event(self, data): return dict( flags=data.read_bits(18), @@ -1045,48 +1165,60 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint8(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) ), - )), - 3: lambda: ('Data', dict(data=data.read_uint32())), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint8(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), + ), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), - other_unit_tag=data.read_uint32() if data.read_bool() else None + other_unit_tag=data.read_uint32() if data.read_bool() else None, ) def ai_communicate_event(self, data): return dict( - beacon=data.read_uint8()-128, - ally=data.read_uint8()-128, - flags=data.read_uint8()-128, # autocast?? + beacon=data.read_uint8() - 128, + ally=data.read_uint8() - 128, + flags=data.read_uint8() - 128, # autocast?? build=None, target_unit_tag=data.read_uint32(), target_unit_link=data.read_uint16(), target_upkeep_player_id=data.read_bits(4) if data.read_bool() else None, target_control_player_id=data.read_bits(4) if data.read_bool() else None, target_point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, + y=data.read_uint32() - 2147483648, + z=data.read_uint32() - 2147483648, ), ) @@ -1096,18 +1228,19 @@ class GameEventsReader_21029(GameEventsReader_19595): class GameEventsReader_22612(GameEventsReader_21029): - def __init__(self): super(GameEventsReader_22612, self).__init__() - self.EVENT_DISPATCH.update({ - 36: (None, self.trigger_ping_event), - 60: (None, self.achievement_awarded_event), - 97: (None, self.trigger_cutscene_bookmark_fired_event), - 98: (None, self.trigger_cutscene_end_scene_fired_event), - 99: (None, self.trigger_cutscene_conversation_line_event), - 100: (None, self.trigger_cutscene_conversation_line_missing_event), - }) + self.EVENT_DISPATCH.update( + { + 36: (None, self.trigger_ping_event), + 60: (None, self.achievement_awarded_event), + 97: (None, self.trigger_cutscene_bookmark_fired_event), + 98: (None, self.trigger_cutscene_end_scene_fired_event), + 99: (None, self.trigger_cutscene_conversation_line_event), + 100: (None, self.trigger_cutscene_conversation_line_missing_event), + } + ) def user_options_event(self, data): return dict( @@ -1129,32 +1262,44 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint8(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32()-2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) + ), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint8(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), ), - )), - 3: lambda: ('Data', dict(data=data.read_uint32())), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), - other_unit_tag=data.read_uint32() if data.read_bool() else None + other_unit_tag=data.read_uint32() if data.read_bool() else None, ) def selection_delta_event(self, data): @@ -1162,17 +1307,29 @@ def selection_delta_event(self, data): control_group_index=data.read_bits(4), subgroup_index=data.read_bits(9), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_bits(9))), - 2: lambda: ('OneIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), - 3: lambda: ('ZeroIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_bits(9)), + ), + 2: lambda: ( + "OneIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), }[data.read_bits(2)](), - add_subgroups=[dict( - unit_link=data.read_uint16(), - subgroup_priority=None, - intra_subgroup_priority=data.read_uint8(), - count=data.read_bits(9), - ) for i in range(data.read_bits(9))], + add_subgroups=[ + dict( + unit_link=data.read_uint16(), + subgroup_priority=None, + intra_subgroup_priority=data.read_uint8(), + count=data.read_bits(9), + ) + for i in range(data.read_bits(9)) + ], add_unit_tags=[data.read_uint32() for i in range(data.read_bits(9))], ) @@ -1181,10 +1338,19 @@ def control_group_update_event(self, data): control_group_index=data.read_bits(4), control_group_update=data.read_bits(2), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_bits(9))), - 2: lambda: ('OneIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), - 3: lambda: ('ZeroIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_bits(9)), + ), + 2: lambda: ( + "OneIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), }[data.read_bits(2)](), ) @@ -1198,31 +1364,30 @@ def selection_sync_check_event(self, data): unit_tags_checksum=data.read_uint32(), subgroup_indices_checksum=data.read_uint32(), subgroups_checksum=data.read_uint32(), - ) + ), ) def ai_communicate_event(self, data): return dict( - beacon=data.read_uint8()-128, - ally=data.read_uint8()-128, - flags=data.read_uint8()-128, - build=data.read_uint8()-128, + beacon=data.read_uint8() - 128, + ally=data.read_uint8() - 128, + flags=data.read_uint8() - 128, + build=data.read_uint8() - 128, target_unit_tag=data.read_uint32(), target_unit_link=data.read_uint16(), target_upkeep_player_id=data.read_uint8(), target_control_player_id=data.read_uint8(), target_point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, - z=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, + y=data.read_uint32() - 2147483648, + z=data.read_uint32() - 2147483648, ), ) def trigger_ping_event(self, data): return dict( point=dict( - x=data.read_uint32()-2147483648, - y=data.read_uint32()-2147483648, + x=data.read_uint32() - 2147483648, y=data.read_uint32() - 2147483648 ), unit_tag=data.read_uint32(), pinged_minimap=data.read_bool(), @@ -1231,42 +1396,36 @@ def trigger_ping_event(self, data): def trigger_transmission_offset_event(self, data): # I'm not actually sure when this second int is introduced.. return dict( - transmission_id=data.read_uint32()-2147483648, - thread=data.read_uint32(), + transmission_id=data.read_uint32() - 2147483648, thread=data.read_uint32() ) def achievement_awarded_event(self, data): - return dict( - achievement_link=data.read_uint16(), - ) + return dict(achievement_link=data.read_uint16()) def trigger_cutscene_bookmark_fired_event(self, data): return dict( - cutscene_id=data.read_uint32()-2147483648, + cutscene_id=data.read_uint32() - 2147483648, bookmark_name=data.read_aligned_string(data.read_bits(7)), ) def trigger_cutscene_end_scene_fired_event(self, data): - return dict( - cutscene_id=data.read_uint32()-2147483648, - ) + return dict(cutscene_id=data.read_uint32() - 2147483648) def trigger_cutscene_conversation_line_event(self, data): return dict( - cutscene_id=data.read_uint32()-2147483648, + cutscene_id=data.read_uint32() - 2147483648, conversation_line=data.read_aligned_string(data.read_bits(7)), alt_conversation_line=data.read_aligned_string(data.read_bits(7)), ) def trigger_cutscene_conversation_line_missing_event(self, data): return dict( - cutscene_id=data.read_uint32()-2147483648, + cutscene_id=data.read_uint32() - 2147483648, conversation_line=data.read_aligned_string(data.read_bits(7)), ) class GameEventsReader_23260(GameEventsReader_22612): - def trigger_sound_length_sync_event(self, data): return dict( sync_info=dict( @@ -1290,7 +1449,6 @@ def user_options_event(self, data): class GameEventsReader_HotSBeta(GameEventsReader_23260): - def user_options_event(self, data): return dict( game_fully_downloaded=data.read_bool(), @@ -1309,26 +1467,37 @@ def selection_delta_event(self, data): control_group_index=data.read_bits(4), subgroup_index=data.read_bits(9), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_bits(9))), - 2: lambda: ('OneIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), - 3: lambda: ('ZeroIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_bits(9)), + ), + 2: lambda: ( + "OneIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), }[data.read_bits(2)](), - add_subgroups=[dict( - unit_link=data.read_uint16(), - subgroup_priority=data.read_uint8(), - intra_subgroup_priority=data.read_uint8(), - count=data.read_bits(9), - ) for i in range(data.read_bits(9))], + add_subgroups=[ + dict( + unit_link=data.read_uint16(), + subgroup_priority=data.read_uint8(), + intra_subgroup_priority=data.read_uint8(), + count=data.read_bits(9), + ) + for i in range(data.read_bits(9)) + ], add_unit_tags=[data.read_uint32() for i in range(data.read_bits(9))], ) def camera_update_event(self, data): return dict( - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ) if data.read_bool() else None, + target=dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None, distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1336,40 +1505,44 @@ def camera_update_event(self, data): def trigger_dialog_control_event(self, data): return dict( - control_id=data.read_uint32()-2147483648, - event_type=data.read_uint32()-2147483648, + control_id=data.read_uint32() - 2147483648, + event_type=data.read_uint32() - 2147483648, event_data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Checked', data.read_bool()), - 2: lambda: ('ValueChanged', data.read_uint32()), - 3: lambda: ('SelectionChanged', data.read_uint32()-2147483648), - 4: lambda: ('TextChanged', data.read_aligned_string(data.read_bits(11))), - 5: lambda: ('MouseButton', data.read_uint32()) + 0: lambda: ("None", None), + 1: lambda: ("Checked", data.read_bool()), + 2: lambda: ("ValueChanged", data.read_uint32()), + 3: lambda: ("SelectionChanged", data.read_uint32() - 2147483648), + 4: lambda: ( + "TextChanged", + data.read_aligned_string(data.read_bits(11)), + ), + 5: lambda: ("MouseButton", data.read_uint32()), }[data.read_bits(3)](), ) class GameEventsReader_24247(GameEventsReader_HotSBeta): - def __init__(self): super(GameEventsReader_24247, self).__init__() - self.EVENT_DISPATCH.update({ - 7: (UserOptionsEvent, self.user_options_event), # Override - 9: (None, self.bank_file_event), # Override - 10: (None, self.bank_section_event), # Override - 11: (None, self.bank_key_event), # Override - 12: (None, self.bank_value_event), # Override - 13: (None, self.bank_signature_event), # New - 14: (None, self.camera_save_event), # New - 21: (None, self.save_game_event), # New - 22: (None, self.save_game_done_event), # Override - 23: (None, self.load_game_done_event), # Override - 43: (HijackReplayGameEvent, self.hijack_replay_game_event), # New - 62: (None, self.trigger_target_mode_update_event), # New - 101: (PlayerLeaveEvent, self.game_user_leave_event), # New - 102: (None, self.game_user_join_event), # New - }) + self.EVENT_DISPATCH.update( + { + 7: (UserOptionsEvent, self.user_options_event), # Override + 9: (None, self.bank_file_event), # Override + 10: (None, self.bank_section_event), # Override + 11: (None, self.bank_key_event), # Override + 12: (None, self.bank_value_event), # Override + 13: (None, self.bank_signature_event), # New + 14: (None, self.camera_save_event), # New + 21: (None, self.save_game_event), # New + 22: (None, self.save_game_done_event), # Override + 23: (None, self.load_game_done_event), # Override + 43: (HijackReplayGameEvent, self.hijack_replay_game_event), # New + 62: (None, self.trigger_target_mode_update_event), # New + 101: (PlayerLeaveEvent, self.game_user_leave_event), # New + 102: (None, self.game_user_join_event), # New + } + ) del self.EVENT_DISPATCH[8] del self.EVENT_DISPATCH[25] del self.EVENT_DISPATCH[76] @@ -1377,16 +1550,13 @@ def __init__(self): def bank_signature_event(self, data): return dict( signature=[data.read_uint8() for i in range(data.read_bits(5))], - toon_handle=data.read_aligned_string(data.read_bits(7)) + toon_handle=data.read_aligned_string(data.read_bits(7)), ) def camera_save_event(self, data): return dict( which=data.read_bits(3), - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ) + target=dict(x=data.read_uint16(), y=data.read_uint16()), ) def load_game_done_event(self, data): @@ -1394,23 +1564,29 @@ def load_game_done_event(self, data): def hijack_replay_game_event(self, data): return dict( - user_infos=[dict( - game_user_id=data.read_bits(4), - observe=data.read_bits(2), - name=data.read_aligned_string(data.read_uint8()), - toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, - clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=None, - ) for i in range(data.read_bits(5))], + user_infos=[ + dict( + game_user_id=data.read_bits(4), + observe=data.read_bits(2), + name=data.read_aligned_string(data.read_uint8()), + toon_handle=data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None, + clan_tag=data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None, + clan_logo=None, + ) + for i in range(data.read_bits(5)) + ], method=data.read_bits(1), ) def camera_update_event(self, data): return dict( - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ) if data.read_bool() else None, + target=dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None, distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1421,7 +1597,7 @@ def trigger_target_mode_update_event(self, data): return dict( ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), - state=data.read_uint8()-128, + state=data.read_uint8() - 128, ) def game_user_leave_event(self, data): @@ -1431,14 +1607,17 @@ def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, - clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, + toon_handle=data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None, + clan_tag=data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None, clan_log=None, ) class GameEventsReader_26490(GameEventsReader_24247): - def user_options_event(self, data): return dict( game_fully_downloaded=data.read_bool(), @@ -1456,10 +1635,7 @@ def trigger_mouse_clicked_event(self, data): return dict( button=data.read_uint32(), down=data.read_bool(), - position_ui=dict( - x=data.read_bits(11), - y=data.read_bits(11), - ), + position_ui=dict(x=data.read_bits(11), y=data.read_bits(11)), position_world=dict( x=data.read_bits(20) - 2147483648, y=data.read_bits(20) - 2147483648, @@ -1470,10 +1646,7 @@ def trigger_mouse_clicked_event(self, data): def trigger_mouse_moved_event(self, data): return dict( - position_ui=dict( - x=data.read_bits(11), - y=data.read_bits(11), - ), + position_ui=dict(x=data.read_bits(11), y=data.read_bits(11)), position_world=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1484,26 +1657,33 @@ def trigger_mouse_moved_event(self, data): class GameEventsReader_27950(GameEventsReader_26490): - def hijack_replay_game_event(self, data): return dict( - user_infos=[dict( - game_user_id=data.read_bits(4), - observe=data.read_bits(2), - name=data.read_aligned_string(data.read_uint8()), - toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, - clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None, - ) for i in range(data.read_bits(5))], + user_infos=[ + dict( + game_user_id=data.read_bits(4), + observe=data.read_bits(2), + name=data.read_aligned_string(data.read_uint8()), + toon_handle=data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None, + clan_tag=data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) + if data.read_bool() + else None, + ) + for i in range(data.read_bits(5)) + ], method=data.read_bits(1), ) def camera_update_event(self, data): return dict( - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ) if data.read_bool() else None, + target=dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None, distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1514,43 +1694,56 @@ def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, - clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None, + toon_handle=data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None, + clan_tag=data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) + if data.read_bool() + else None, ) -class GameEventsReader_34784(GameEventsReader_27950): +class GameEventsReader_34784(GameEventsReader_27950): def __init__(self): super(GameEventsReader_34784, self).__init__() - self.EVENT_DISPATCH.update({ - 25: (None, self.command_manager_reset_event), # Re-using this old number - 61: (None, self.trigger_hotkey_pressed_event), - 103: (None, self.command_manager_state_event), - 104: (UpdateTargetPointCommandEvent, self.command_update_target_point_event), - 105: (UpdateTargetUnitCommandEvent, self.command_update_target_unit_event), - 106: (None, self.trigger_anim_length_query_by_name_event), - 107: (None, self.trigger_anim_length_query_by_props_event), - 108: (None, self.trigger_anim_offset_event), - 109: (None, self.catalog_modify_event), - 110: (None, self.hero_talent_tree_selected_event), - 111: (None, self.trigger_profiler_logging_finished_event), - 112: (None, self.hero_talent_tree_selection_panel_toggled_event), - }) + self.EVENT_DISPATCH.update( + { + 25: ( + None, + self.command_manager_reset_event, + ), # Re-using this old number + 61: (None, self.trigger_hotkey_pressed_event), + 103: (None, self.command_manager_state_event), + 104: ( + UpdateTargetPointCommandEvent, + self.command_update_target_point_event, + ), + 105: ( + UpdateTargetUnitCommandEvent, + self.command_update_target_unit_event, + ), + 106: (None, self.trigger_anim_length_query_by_name_event), + 107: (None, self.trigger_anim_length_query_by_props_event), + 108: (None, self.trigger_anim_offset_event), + 109: (None, self.catalog_modify_event), + 110: (None, self.hero_talent_tree_selected_event), + 111: (None, self.trigger_profiler_logging_finished_event), + 112: (None, self.hero_talent_tree_selection_panel_toggled_event), + } + ) def hero_talent_tree_selection_panel_toggled_event(self, data): - return dict( - shown=data.read_bool(), - ) + return dict(shown=data.read_bool()) def trigger_profiler_logging_finished_event(self, data): return dict() def hero_talent_tree_selected_event(self, data): - return dict( - index=data.read_uint32() - ) + return dict(index=data.read_uint32()) def catalog_modify_event(self, data): return dict( @@ -1561,15 +1754,10 @@ def catalog_modify_event(self, data): ) def trigger_anim_offset_event(self, data): - return dict( - anim_wait_query_id=data.read_uint16(), - ) + return dict(anim_wait_query_id=data.read_uint16()) def trigger_anim_length_query_by_props_event(self, data): - return dict( - query_id=data.read_uint16(), - length_ms=data.read_uint32(), - ) + return dict(query_id=data.read_uint16(), length_ms=data.read_uint32()) def trigger_anim_length_query_by_name_event(self, data): return dict( @@ -1579,9 +1767,7 @@ def trigger_anim_length_query_by_name_event(self, data): ) def command_manager_reset_event(self, data): - return dict( - sequence=data.read_uint32(), - ) + return dict(sequence=data.read_uint32()) def command_manager_state_event(self, data): return dict( @@ -1593,13 +1779,16 @@ def command_update_target_point_event(self, data): return dict( flags=0, # fill me with previous TargetPointEvent.flags ability=None, # fill me with previous TargetPointEvent.ability - data=('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_bits(32) - 2147483648, + data=( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_bits(32) - 2147483648, + ) ), - )), + ), sequence=0, # fill me with previous TargetPointEvent.flags other_unit_tag=None, # fill me with previous TargetPointEvent.flags unit_group=None, # fill me with previous TargetPointEvent.flags @@ -1607,24 +1796,27 @@ def command_update_target_point_event(self, data): def command_update_target_unit_event(self, data): return dict( - flags=0, # fill me with previous TargetUnitEvent.flags + flags=0, # fill me with previous TargetUnitEvent.flags ability=None, # fill me with previous TargetUnitEvent.ability - data=('TargetUnit', dict( - flags=data.read_uint16(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_bits(32) - 2147483648, + data=( + "TargetUnit", + dict( + flags=data.read_uint16(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) if data.read_bool() else None, + upkeep_player_id=data.read_bits(4) if data.read_bool() else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_bits(32) - 2147483648, + ), ), - )), - sequence=0, # fill me with previous TargetUnitEvent.flags - other_unit_tag=None, # fill me with previous TargetUnitEvent.flags - unit_group=None, # fill me with previous TargetUnitEvent.flags + ), + sequence=0, # fill me with previous TargetUnitEvent.flags + other_unit_tag=None, # fill me with previous TargetUnitEvent.flags + unit_group=None, # fill me with previous TargetUnitEvent.flags ) def command_event(self, data): @@ -1634,32 +1826,42 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint16(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) ), - )), - 3: lambda: ('Data', dict( - data=data.read_uint32() - )), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint16(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), + ), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), sequence=data.read_uint32() + 1, other_unit_tag=data.read_uint32() if data.read_bool() else None, @@ -1689,8 +1891,7 @@ def user_options_event(self, data): def trigger_ping_event(self, data): return dict( point=dict( - x=data.read_uint32() - 2147483648, - y=data.read_uint32() - 2147483648, + x=data.read_uint32() - 2147483648, y=data.read_uint32() - 2147483648 ), unit_tag=data.read_uint32(), pinged_minimap=data.read_bool(), @@ -1699,10 +1900,9 @@ def trigger_ping_event(self, data): def camera_update_event(self, data): return dict( - target=dict( - x=data.read_uint16(), - y=data.read_uint16(), - ) if data.read_bool() else None, + target=dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None, distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1711,50 +1911,62 @@ def camera_update_event(self, data): ) def trigger_hotkey_pressed_event(self, data): - return dict( - hotkey=data.read_uint32(), - down=data.read_bool(), - ) + return dict(hotkey=data.read_uint32(), down=data.read_bool()) def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) if data.read_bool() else None, - clan_tag=data.read_aligned_string(data.read_uint8()) if data.read_bool() else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None, + toon_handle=data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None, + clan_tag=data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None, + clan_logo=DepotFile(data.read_aligned_bytes(40)) + if data.read_bool() + else None, hijack=data.read_bool(), hijack_clone_game_user_id=data.read_bits(4) if data.read_bool() else None, ) def game_user_leave_event(self, data): - return dict( - leave_reason=data.read_bits(4) - ) + return dict(leave_reason=data.read_bits(4)) -class GameEventsReader_36442(GameEventsReader_34784): +class GameEventsReader_36442(GameEventsReader_34784): def control_group_update_event(self, data): return dict( control_group_index=data.read_bits(4), control_group_update=data.read_bits(3), remove_mask={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('Mask', self.read_selection_bitmask(data, data.read_bits(9))), - 2: lambda: ('OneIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), - 3: lambda: ('ZeroIndices', [data.read_bits(9) for i in range(data.read_bits(9))]), + 0: lambda: ("None", None), + 1: lambda: ( + "Mask", + self.read_selection_bitmask(data, data.read_bits(9)), + ), + 2: lambda: ( + "OneIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), + 3: lambda: ( + "ZeroIndices", + [data.read_bits(9) for i in range(data.read_bits(9))], + ), }[data.read_bits(2)](), ) -class GameEventsReader_38215(GameEventsReader_36442): +class GameEventsReader_38215(GameEventsReader_36442): def __init__(self): super(GameEventsReader_38215, self).__init__() - self.EVENT_DISPATCH.update({ - 76: (None, self.trigger_command_error_event), - 92: (None, self.trigger_mousewheel_event), # 172 in protocol38125.py - }) + self.EVENT_DISPATCH.update( + { + 76: (None, self.trigger_command_error_event), + 92: (None, self.trigger_mousewheel_event), # 172 in protocol38125.py + } + ) def trigger_command_error_event(self, data): return dict( @@ -1763,14 +1975,16 @@ def trigger_command_error_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, ) def trigger_mousewheel_event(self, data): # 172 in protocol38125.py return dict( - wheelspin=data.read_uint16()-32768, # 171 in protocol38125.py - flags=data.read_uint8() - 128, # 112 in protocol38125.py + wheelspin=data.read_uint16() - 32768, # 171 in protocol38125.py + flags=data.read_uint8() - 128, # 112 in protocol38125.py ) def command_event(self, data): @@ -1782,32 +1996,42 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint16(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) + ), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint16(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), ), - )), - 3: lambda: ('Data', dict( - data=data.read_uint32() - )), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), sequence=data.read_uint32() + 1, other_unit_tag=data.read_uint32() if data.read_bool() else None, @@ -1834,49 +2058,48 @@ def user_options_event(self, data): use_ai_beacons=None, ) -class GameEventsReader_38749(GameEventsReader_38215): +class GameEventsReader_38749(GameEventsReader_38215): def trigger_ping_event(self, data): return dict( point=dict( - x=data.read_uint32() - 2147483648, - y=data.read_uint32() - 2147483648, + x=data.read_uint32() - 2147483648, y=data.read_uint32() - 2147483648 ), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), unit_control_player_id=(data.read_bits(4) if data.read_bool() else None), unit_upkeep_player_id=(data.read_bits(4) if data.read_bool() else None), unit_position=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_bits(32) - 2147483648, - ), + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_bits(32) - 2147483648, + ), pinged_minimap=data.read_bool(), option=data.read_uint32() - 2147483648, ) -class GameEventsReader_38996(GameEventsReader_38749): +class GameEventsReader_38996(GameEventsReader_38749): def trigger_ping_event(self, data): return dict( point=dict( - x=data.read_uint32() - 2147483648, - y=data.read_uint32() - 2147483648, + x=data.read_uint32() - 2147483648, y=data.read_uint32() - 2147483648 ), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), unit_control_player_id=(data.read_bits(4) if data.read_bool() else None), unit_upkeep_player_id=(data.read_bits(4) if data.read_bool() else None), unit_position=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_bits(32) - 2147483648, - ), + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_bits(32) - 2147483648, + ), unit_is_under_construction=data.read_bool(), pinged_minimap=data.read_bool(), option=data.read_uint32() - 2147483648, ) + class GameEventsReader_64469(GameEventsReader_38996): # this function is exactly the same as command_event() from GameEventsReader_38996 @@ -1888,32 +2111,42 @@ def command_event(self, data): ability_link=data.read_uint16(), ability_command_index=data.read_bits(5), ability_command_data=data.read_uint8() if data.read_bool() else None, - ) if data.read_bool() else None, + ) + if data.read_bool() + else None, data={ # Choice - 0: lambda: ('None', None), - 1: lambda: ('TargetPoint', dict( - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, - ) - )), - 2: lambda: ('TargetUnit', dict( - flags=data.read_uint16(), - timer=data.read_uint8(), - unit_tag=data.read_uint32(), - unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) if data.read_bool() else None, - upkeep_player_id=data.read_bits(4) if data.read_bool() else None, - point=dict( - x=data.read_bits(20), - y=data.read_bits(20), - z=data.read_uint32() - 2147483648, + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) + ), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint16(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), ), - )), - 3: lambda: ('Data', dict( - data=data.read_uint32() - )), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), }[data.read_bits(2)](), sequence=data.read_uint32() + 1, other_unit_tag=data.read_uint32() if data.read_bool() else None, @@ -1929,24 +2162,18 @@ class GameEventsReader_65895(GameEventsReader_64469): def __init__(self): super(GameEventsReader_65895, self).__init__() - self.EVENT_DISPATCH.update({ - 116: (None, self.set_sync_loading), - 117: (None, self.set_sync_playing), - }) + self.EVENT_DISPATCH.update( + {116: (None, self.set_sync_loading), 117: (None, self.set_sync_playing)} + ) def set_sync_loading(self, data): - return dict( - sync_load=data.read_uint32() - ) + return dict(sync_load=data.read_uint32()) def set_sync_playing(self, data): - return dict( - sync_load=data.read_uint32() - ) + return dict(sync_load=data.read_uint32()) class TrackerEventsReader(object): - def __init__(self): self.EVENT_DISPATCH = { 0: PlayerStatsEvent, diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 32ac05bc..8c1d7110 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -16,7 +16,16 @@ from sc2reader import exceptions from sc2reader.data import datapacks from sc2reader.exceptions import SC2ReaderLocalizationError, CorruptTrackerFileError -from sc2reader.objects import Participant, Observer, Computer, Team, PlayerSummary, Graph, BuildEntry, MapInfo +from sc2reader.objects import ( + Participant, + Observer, + Computer, + Team, + PlayerSummary, + Graph, + BuildEntry, + MapInfo, +) from sc2reader.constants import GAME_SPEED_FACTOR, LOBBY_PROPERTIES @@ -25,9 +34,9 @@ def __init__(self, file_object, filename=None, factory=None, **options): self.factory = factory self.opt = options self.logger = log_utils.get_logger(self.__class__) - self.filename = filename or getattr(file_object, 'name', 'Unavailable') + self.filename = filename or getattr(file_object, "name", "Unavailable") - if hasattr(file_object, 'seek'): + if hasattr(file_object, "seek"): file_object.seek(0) self.filehash = hashlib.sha256(file_object.read()).hexdigest() file_object.seek(0) @@ -192,7 +201,15 @@ class Replay(Resource): #: Lists info for each user that is resuming from replay. resume_user_info = None - def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.engine, do_tracker_events=True, **options): + def __init__( + self, + replay_file, + filename=None, + load_level=4, + engine=sc2reader.engine, + do_tracker_events=True, + **options + ): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() @@ -254,7 +271,7 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en except Exception as e: raise exceptions.MPQError("Unable to construct the MPQArchive", e) - header_content = self.archive.header['user_data_header']['content'] + header_content = self.archive.header["user_data_header"]["content"] header_data = BitPackedDecoder(header_content).read_struct() self.versions = list(header_data[1].values()) self.frames = header_data[3] @@ -262,21 +279,23 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.base_build = self.versions[5] self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) fps = self.game_fps - if (34784 <= self.build): # lotv replay, adjust time + if 34784 <= self.build: # lotv replay, adjust time fps = self.game_fps * 1.4 - self.length = self.game_length = self.real_length = utils.Length(seconds=int(self.frames/fps)) + self.length = self.game_length = self.real_length = utils.Length( + seconds=int(self.frames / fps) + ) # Load basic details if requested # .backup files are read in case the main files are missing or removed if load_level >= 1: self.load_level = 1 files = [ - 'replay.initData.backup', - 'replay.details.backup', - 'replay.attributes.events', - 'replay.initData', - 'replay.details' + "replay.initData.backup", + "replay.details.backup", + "replay.attributes.events", + "replay.initData", + "replay.details", ] for data_file in files: self._read_data(data_file, self._get_reader(data_file)) @@ -284,13 +303,13 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.datapack = self._get_datapack() # Can only be effective if map data has been loaded - if options.get('load_map', False): + if options.get("load_map", False): self.load_map() # Load players if requested if load_level >= 2: self.load_level = 2 - for data_file in ['replay.message.events']: + for data_file in ["replay.message.events"]: self._read_data(data_file, self._get_reader(data_file)) self.load_message_events() self.load_players() @@ -298,87 +317,103 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en # Load tracker events if requested if load_level >= 3 and do_tracker_events: self.load_level = 3 - for data_file in ['replay.tracker.events']: + for data_file in ["replay.tracker.events"]: self._read_data(data_file, self._get_reader(data_file)) self.load_tracker_events() # Load events if requested if load_level >= 4: self.load_level = 4 - for data_file in ['replay.game.events']: + for data_file in ["replay.game.events"]: self._read_data(data_file, self._get_reader(data_file)) self.load_game_events() # Run this replay through the engine as indicated if engine: - resume_events = [ev for ev in self.game_events if ev.name == 'HijackReplayGameEvent'] - if self.base_build <= 26490 and self.tracker_events and len(resume_events) > 0: + resume_events = [ + ev for ev in self.game_events if ev.name == "HijackReplayGameEvent" + ] + if ( + self.base_build <= 26490 + and self.tracker_events + and len(resume_events) > 0 + ): raise CorruptTrackerFileError( - "Cannot run engine on resumed games with tracker events. Run again with the " + - "do_tracker_events=False option to generate context without tracker events.") + "Cannot run engine on resumed games with tracker events. Run again with the " + + "do_tracker_events=False option to generate context without tracker events." + ) engine.run(self) def load_init_data(self): - if 'replay.initData' in self.raw_data: - initData = self.raw_data['replay.initData'] - elif 'replay.initData.backup' in self.raw_data: - initData = self.raw_data['replay.initData.backup'] + if "replay.initData" in self.raw_data: + initData = self.raw_data["replay.initData"] + elif "replay.initData.backup" in self.raw_data: + initData = self.raw_data["replay.initData.backup"] else: return - options = initData['game_description']['game_options'] - self.amm = options['amm'] - self.ranked = options['ranked'] - self.competitive = options['competitive'] - self.practice = options['practice'] - self.cooperative = options['cooperative'] - self.battle_net = options['battle_net'] - self.hero_duplicates_allowed = options['hero_duplicates_allowed'] + options = initData["game_description"]["game_options"] + self.amm = options["amm"] + self.ranked = options["ranked"] + self.competitive = options["competitive"] + self.practice = options["practice"] + self.cooperative = options["cooperative"] + self.battle_net = options["battle_net"] + self.hero_duplicates_allowed = options["hero_duplicates_allowed"] def load_attribute_events(self): - if 'replay.attributes.events' in self.raw_data: + if "replay.attributes.events" in self.raw_data: # Organize the attribute data to be useful self.attributes = defaultdict(dict) - attributesEvents = self.raw_data['replay.attributes.events'] + attributesEvents = self.raw_data["replay.attributes.events"] for attr in attributesEvents: self.attributes[attr.player][attr.name] = attr.value # Populate replay with attributes - self.speed = self.attributes[16]['Game Speed'] - self.category = self.attributes[16]['Game Mode'] - self.type = self.game_type = self.attributes[16]['Teams'] - self.is_ladder = (self.category == "Ladder") - self.is_private = (self.category == "Private") + self.speed = self.attributes[16]["Game Speed"] + self.category = self.attributes[16]["Game Mode"] + self.type = self.game_type = self.attributes[16]["Teams"] + self.is_ladder = self.category == "Ladder" + self.is_private = self.category == "Private" def load_details(self): - if 'replay.details' in self.raw_data: - details = self.raw_data['replay.details'] - elif 'replay.details.backup' in self.raw_data: - details = self.raw_data['replay.details.backup'] + if "replay.details" in self.raw_data: + details = self.raw_data["replay.details"] + elif "replay.details.backup" in self.raw_data: + details = self.raw_data["replay.details.backup"] else: return - self.map_name = details['map_name'] - self.region = details['cache_handles'][0].server.lower() - self.map_hash = details['cache_handles'][-1].hash - self.map_file = details['cache_handles'][-1] + self.map_name = details["map_name"] + self.region = details["cache_handles"][0].server.lower() + self.map_hash = details["cache_handles"][-1].hash + self.map_file = details["cache_handles"][-1] # Expand this special case mapping - if self.region == 'sg': - self.region = 'sea' - - dependency_hashes = [d.hash for d in details['cache_handles']] - if hashlib.sha256('Standard Data: Void.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'LotV' - elif hashlib.sha256('Standard Data: Swarm.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'HotS' - elif hashlib.sha256('Standard Data: Liberty.SC2Mod'.encode('utf8')).hexdigest() in dependency_hashes: - self.expansion = 'WoL' + if self.region == "sg": + self.region = "sea" + + dependency_hashes = [d.hash for d in details["cache_handles"]] + if ( + hashlib.sha256("Standard Data: Void.SC2Mod".encode("utf8")).hexdigest() + in dependency_hashes + ): + self.expansion = "LotV" + elif ( + hashlib.sha256("Standard Data: Swarm.SC2Mod".encode("utf8")).hexdigest() + in dependency_hashes + ): + self.expansion = "HotS" + elif ( + hashlib.sha256("Standard Data: Liberty.SC2Mod".encode("utf8")).hexdigest() + in dependency_hashes + ): + self.expansion = "WoL" else: - self.expansion = '' + self.expansion = "" - self.windows_timestamp = details['file_time'] + self.windows_timestamp = details["file_time"] self.unix_timestamp = utils.windows_to_unix(self.windows_timestamp) self.end_time = datetime.utcfromtimestamp(self.unix_timestamp) @@ -386,14 +421,22 @@ def load_details(self): # the value required to get the adjusted timestamp. We know the upper # limit for any adjustment number so use that to distinguish between # the two cases. - if details['utc_adjustment'] < 10**7*60*60*24: - self.time_zone = details['utc_adjustment']/(10**7*60*60) + if details["utc_adjustment"] < 10 ** 7 * 60 * 60 * 24: + self.time_zone = details["utc_adjustment"] / (10 ** 7 * 60 * 60) else: - self.time_zone = (details['utc_adjustment']-details['file_time'])/(10**7*60*60) + self.time_zone = (details["utc_adjustment"] - details["file_time"]) / ( + 10 ** 7 * 60 * 60 + ) self.game_length = self.length - self.real_length = utils.Length(seconds=int(self.length.seconds/GAME_SPEED_FACTOR[self.expansion][self.speed])) - self.start_time = datetime.utcfromtimestamp(self.unix_timestamp-self.real_length.seconds) + self.real_length = utils.Length( + seconds=int( + self.length.seconds / GAME_SPEED_FACTOR[self.expansion][self.speed] + ) + ) + self.start_time = datetime.utcfromtimestamp( + self.unix_timestamp - self.real_length.seconds + ) self.date = self.end_time # backwards compatibility def load_all_details(self): @@ -407,18 +450,18 @@ def load_map(self): def load_players(self): # If we don't at least have details and attributes_events we can go no further # We can use the backup detail files if the main files have been removed - if 'replay.details' in self.raw_data: - details = self.raw_data['replay.details'] - elif 'replay.details.backup' in self.raw_data: - details = self.raw_data['replay.details.backup'] + if "replay.details" in self.raw_data: + details = self.raw_data["replay.details"] + elif "replay.details.backup" in self.raw_data: + details = self.raw_data["replay.details.backup"] else: return - if 'replay.attributes.events' not in self.raw_data: + if "replay.attributes.events" not in self.raw_data: return - if 'replay.initData' in self.raw_data: - initData = self.raw_data['replay.initData'] - elif 'replay.initData.backup' in self.raw_data: - initData = self.raw_data['replay.initData.backup'] + if "replay.initData" in self.raw_data: + initData = self.raw_data["replay.initData"] + elif "replay.initData.backup" in self.raw_data: + initData = self.raw_data["replay.initData.backup"] else: return @@ -433,30 +476,56 @@ def load_players(self): # Assume that the first X map slots starting at 1 are player slots # so that we can assign player ids without the map self.entities = list() - for slot_id, slot_data in enumerate(initData['lobby_state']['slots']): - user_id = slot_data['user_id'] - - if slot_data['control'] == 2: - if slot_data['observe'] == 0: - self.entities.append(Participant(slot_id, slot_data, user_id, initData['user_initial_data'][user_id], player_id, details['players'][detail_id], self.attributes.get(player_id, dict()))) + for slot_id, slot_data in enumerate(initData["lobby_state"]["slots"]): + user_id = slot_data["user_id"] + + if slot_data["control"] == 2: + if slot_data["observe"] == 0: + self.entities.append( + Participant( + slot_id, + slot_data, + user_id, + initData["user_initial_data"][user_id], + player_id, + details["players"][detail_id], + self.attributes.get(player_id, dict()), + ) + ) detail_id += 1 player_id += 1 else: - self.entities.append(Observer(slot_id, slot_data, user_id, initData['user_initial_data'][user_id], player_id)) + self.entities.append( + Observer( + slot_id, + slot_data, + user_id, + initData["user_initial_data"][user_id], + player_id, + ) + ) player_id += 1 - elif slot_data['control'] == 3 and detail_id < len(details['players']): + elif slot_data["control"] == 3 and detail_id < len(details["players"]): # detail_id check needed for coop - self.entities.append(Computer(slot_id, slot_data, player_id, details['players'][detail_id], self.attributes.get(player_id, dict()))) + self.entities.append( + Computer( + slot_id, + slot_data, + player_id, + details["players"][detail_id], + self.attributes.get(player_id, dict()), + ) + ) detail_id += 1 player_id += 1 def get_team(team_id): if team_id is not None and team_id not in self.team: - team = Team(team_id) - self.team[team_id] = team - self.teams.append(team) + team = Team(team_id) + self.team[team_id] = team + self.teams.append(team) return self.team[team_id] # Set up all our cross reference data structures @@ -485,11 +554,13 @@ def get_team(team_id): results = set([p.result for p in team.players]) if len(results) == 1: team.result = list(results)[0] - if team.result == 'Win': + if team.result == "Win": self.winner = team else: - self.logger.warn("Conflicting results for Team {0}: {1}".format(team.number, results)) - team.result = 'Unknown' + self.logger.warn( + "Conflicting results for Team {0}: {1}".format(team.number, results) + ) + team.result = "Unknown" self.teams.sort(key=lambda t: t.number) @@ -513,8 +584,8 @@ def get_team(team_id): self.recorder = None entity_names = sorted(map(lambda p: p.name, self.entities)) - hash_input = self.region+":"+','.join(entity_names) - self.people_hash = hashlib.sha256(hash_input.encode('utf8')).hexdigest() + hash_input = self.region + ":" + ",".join(entity_names) + self.people_hash = hashlib.sha256(hash_input.encode("utf8")).hexdigest() # The presence of observers and/or computer players makes this not actually ladder # This became an issue in HotS where Training, vs AI, Unranked, and Ranked @@ -523,35 +594,39 @@ def get_team(team_id): self.is_ladder = False def load_message_events(self): - if 'replay.message.events' not in self.raw_data: + if "replay.message.events" not in self.raw_data: return - self.messages = self.raw_data['replay.message.events']['messages'] - self.pings = self.raw_data['replay.message.events']['pings'] - self.packets = self.raw_data['replay.message.events']['packets'] + self.messages = self.raw_data["replay.message.events"]["messages"] + self.pings = self.raw_data["replay.message.events"]["pings"] + self.packets = self.raw_data["replay.message.events"]["packets"] - self.message_events = self.messages+self.pings+self.packets + self.message_events = self.messages + self.pings + self.packets self.events = sorted(self.events + self.message_events, key=lambda e: e.frame) def load_game_events(self): # Copy the events over # TODO: the events need to be fixed both on the reader and processor side - if 'replay.game.events' not in self.raw_data: + if "replay.game.events" not in self.raw_data: return - self.game_events = self.raw_data['replay.game.events'] - self.events = sorted(self.events+self.game_events, key=lambda e: e.frame) + self.game_events = self.raw_data["replay.game.events"] + self.events = sorted(self.events + self.game_events, key=lambda e: e.frame) # hideous hack for HotS 2.0.0.23925, see https://github.com/GraylinKim/sc2reader/issues/87 - if self.base_build == 23925 and self.events and self.events[-1].frame > self.frames: + if ( + self.base_build == 23925 + and self.events + and self.events[-1].frame > self.frames + ): self.frames = self.events[-1].frame - self.length = utils.Length(seconds=int(self.frames/self.game_fps)) + self.length = utils.Length(seconds=int(self.frames / self.game_fps)) def load_tracker_events(self): - if 'replay.tracker.events' not in self.raw_data: + if "replay.tracker.events" not in self.raw_data: return - self.tracker_events = self.raw_data['replay.tracker.events'] + self.tracker_events = self.raw_data["replay.tracker.events"] self.events = sorted(self.tracker_events + self.events, key=lambda e: e.frame) def register_reader(self, data_file, reader, filterfunc=lambda r: True): @@ -596,55 +671,195 @@ def register_datapack(self, datapack, filterfunc=lambda r: True): # Override points def register_default_readers(self): """Registers factory default readers.""" - self.register_reader('replay.details', readers.DetailsReader(), lambda r: True) - self.register_reader('replay.initData', readers.InitDataReader(), lambda r: True) - self.register_reader('replay.details.backup', readers.DetailsReader(), lambda r: True) - self.register_reader('replay.initData.backup', readers.InitDataReader(), lambda r: True) - self.register_reader('replay.tracker.events', readers.TrackerEventsReader(), lambda r: True) - self.register_reader('replay.message.events', readers.MessageEventsReader(), lambda r: True) - self.register_reader('replay.attributes.events', readers.AttributesEventsReader(), lambda r: True) - - self.register_reader('replay.game.events', readers.GameEventsReader_15405(), lambda r: 15405 <= r.base_build < 16561) - self.register_reader('replay.game.events', readers.GameEventsReader_16561(), lambda r: 16561 <= r.base_build < 17326) - self.register_reader('replay.game.events', readers.GameEventsReader_17326(), lambda r: 17326 <= r.base_build < 18574) - self.register_reader('replay.game.events', readers.GameEventsReader_18574(), lambda r: 18574 <= r.base_build < 19595) - self.register_reader('replay.game.events', readers.GameEventsReader_19595(), lambda r: 19595 <= r.base_build < 22612) - self.register_reader('replay.game.events', readers.GameEventsReader_22612(), lambda r: 22612 <= r.base_build < 23260) - self.register_reader('replay.game.events', readers.GameEventsReader_23260(), lambda r: 23260 <= r.base_build < 24247) - self.register_reader('replay.game.events', readers.GameEventsReader_24247(), lambda r: 24247 <= r.base_build < 26490) - self.register_reader('replay.game.events', readers.GameEventsReader_26490(), lambda r: 26490 <= r.base_build < 27950) - self.register_reader('replay.game.events', readers.GameEventsReader_27950(), lambda r: 27950 <= r.base_build < 34784) - self.register_reader('replay.game.events', readers.GameEventsReader_34784(), lambda r: 34784 <= r.base_build < 36442) - self.register_reader('replay.game.events', readers.GameEventsReader_36442(), lambda r: 36442 <= r.base_build < 38215) - self.register_reader('replay.game.events', readers.GameEventsReader_38215(), lambda r: 38215 <= r.base_build < 38749) - self.register_reader('replay.game.events', readers.GameEventsReader_38749(), lambda r: 38749 <= r.base_build < 38996) - self.register_reader('replay.game.events', readers.GameEventsReader_38996(), lambda r: 38996 <= r.base_build < 64469) - self.register_reader('replay.game.events', readers.GameEventsReader_64469(), lambda r: 64469 <= r.base_build < 65895) - self.register_reader('replay.game.events', readers.GameEventsReader_65895(), lambda r: 65895 <= r.base_build) - self.register_reader('replay.game.events', readers.GameEventsReader_HotSBeta(), lambda r: r.versions[1] == 2 and r.build < 24247) + self.register_reader("replay.details", readers.DetailsReader(), lambda r: True) + self.register_reader( + "replay.initData", readers.InitDataReader(), lambda r: True + ) + self.register_reader( + "replay.details.backup", readers.DetailsReader(), lambda r: True + ) + self.register_reader( + "replay.initData.backup", readers.InitDataReader(), lambda r: True + ) + self.register_reader( + "replay.tracker.events", readers.TrackerEventsReader(), lambda r: True + ) + self.register_reader( + "replay.message.events", readers.MessageEventsReader(), lambda r: True + ) + self.register_reader( + "replay.attributes.events", readers.AttributesEventsReader(), lambda r: True + ) + + self.register_reader( + "replay.game.events", + readers.GameEventsReader_15405(), + lambda r: 15405 <= r.base_build < 16561, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_16561(), + lambda r: 16561 <= r.base_build < 17326, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_17326(), + lambda r: 17326 <= r.base_build < 18574, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_18574(), + lambda r: 18574 <= r.base_build < 19595, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_19595(), + lambda r: 19595 <= r.base_build < 22612, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_22612(), + lambda r: 22612 <= r.base_build < 23260, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_23260(), + lambda r: 23260 <= r.base_build < 24247, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_24247(), + lambda r: 24247 <= r.base_build < 26490, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_26490(), + lambda r: 26490 <= r.base_build < 27950, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_27950(), + lambda r: 27950 <= r.base_build < 34784, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_34784(), + lambda r: 34784 <= r.base_build < 36442, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_36442(), + lambda r: 36442 <= r.base_build < 38215, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_38215(), + lambda r: 38215 <= r.base_build < 38749, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_38749(), + lambda r: 38749 <= r.base_build < 38996, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_38996(), + lambda r: 38996 <= r.base_build < 64469, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_64469(), + lambda r: 64469 <= r.base_build < 65895, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_65895(), + lambda r: 65895 <= r.base_build, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_HotSBeta(), + lambda r: r.versions[1] == 2 and r.build < 24247, + ) def register_default_datapacks(self): """Registers factory default datapacks.""" - self.register_datapack(datapacks['WoL']['16117'], lambda r: r.expansion == 'WoL' and 16117 <= r.build < 17326) - self.register_datapack(datapacks['WoL']['17326'], lambda r: r.expansion == 'WoL' and 17326 <= r.build < 18092) - self.register_datapack(datapacks['WoL']['18092'], lambda r: r.expansion == 'WoL' and 18092 <= r.build < 19458) - self.register_datapack(datapacks['WoL']['19458'], lambda r: r.expansion == 'WoL' and 19458 <= r.build < 22612) - self.register_datapack(datapacks['WoL']['22612'], lambda r: r.expansion == 'WoL' and 22612 <= r.build < 24944) - self.register_datapack(datapacks['WoL']['24944'], lambda r: r.expansion == 'WoL' and 24944 <= r.build) - self.register_datapack(datapacks['HotS']['base'], lambda r: r.expansion == 'HotS' and r.build < 23925) - self.register_datapack(datapacks['HotS']['23925'], lambda r: r.expansion == 'HotS' and 23925 <= r.build < 24247) - self.register_datapack(datapacks['HotS']['24247'], lambda r: r.expansion == 'HotS' and 24247 <= r.build < 24764) - self.register_datapack(datapacks['HotS']['24764'], lambda r: r.expansion == 'HotS' and 24764 <= r.build < 38215) - self.register_datapack(datapacks['HotS']['38215'], lambda r: r.expansion == 'HotS' and 38215 <= r.build) - self.register_datapack(datapacks['LotV']['base'], lambda r: r.expansion == 'LotV' and 34784 <= r.build) - self.register_datapack(datapacks['LotV']['44401'], lambda r: r.expansion == 'LotV' and 44401 <= r.build < 47185) - self.register_datapack(datapacks['LotV']['47185'], lambda r: r.expansion == 'LotV' and 47185 <= r.build < 48258) - self.register_datapack(datapacks['LotV']['48258'], lambda r: r.expansion == 'LotV' and 48258 <= r.build < 53644) - self.register_datapack(datapacks['LotV']['53644'], lambda r: r.expansion == 'LotV' and 53644 <= r.build < 54724) - self.register_datapack(datapacks['LotV']['54724'], lambda r: r.expansion == 'LotV' and 54724 <= r.build < 59587) - self.register_datapack(datapacks['LotV']['59587'], lambda r: r.expansion == 'LotV' and 59587 <= r.build < 70154) - self.register_datapack(datapacks['LotV']['70154'], lambda r: r.expansion == 'LotV' and 70154 <= r.build) - + self.register_datapack( + datapacks["WoL"]["16117"], + lambda r: r.expansion == "WoL" and 16117 <= r.build < 17326, + ) + self.register_datapack( + datapacks["WoL"]["17326"], + lambda r: r.expansion == "WoL" and 17326 <= r.build < 18092, + ) + self.register_datapack( + datapacks["WoL"]["18092"], + lambda r: r.expansion == "WoL" and 18092 <= r.build < 19458, + ) + self.register_datapack( + datapacks["WoL"]["19458"], + lambda r: r.expansion == "WoL" and 19458 <= r.build < 22612, + ) + self.register_datapack( + datapacks["WoL"]["22612"], + lambda r: r.expansion == "WoL" and 22612 <= r.build < 24944, + ) + self.register_datapack( + datapacks["WoL"]["24944"], + lambda r: r.expansion == "WoL" and 24944 <= r.build, + ) + self.register_datapack( + datapacks["HotS"]["base"], + lambda r: r.expansion == "HotS" and r.build < 23925, + ) + self.register_datapack( + datapacks["HotS"]["23925"], + lambda r: r.expansion == "HotS" and 23925 <= r.build < 24247, + ) + self.register_datapack( + datapacks["HotS"]["24247"], + lambda r: r.expansion == "HotS" and 24247 <= r.build < 24764, + ) + self.register_datapack( + datapacks["HotS"]["24764"], + lambda r: r.expansion == "HotS" and 24764 <= r.build < 38215, + ) + self.register_datapack( + datapacks["HotS"]["38215"], + lambda r: r.expansion == "HotS" and 38215 <= r.build, + ) + self.register_datapack( + datapacks["LotV"]["base"], + lambda r: r.expansion == "LotV" and 34784 <= r.build, + ) + self.register_datapack( + datapacks["LotV"]["44401"], + lambda r: r.expansion == "LotV" and 44401 <= r.build < 47185, + ) + self.register_datapack( + datapacks["LotV"]["47185"], + lambda r: r.expansion == "LotV" and 47185 <= r.build < 48258, + ) + self.register_datapack( + datapacks["LotV"]["48258"], + lambda r: r.expansion == "LotV" and 48258 <= r.build < 53644, + ) + self.register_datapack( + datapacks["LotV"]["53644"], + lambda r: r.expansion == "LotV" and 53644 <= r.build < 54724, + ) + self.register_datapack( + datapacks["LotV"]["54724"], + lambda r: r.expansion == "LotV" and 54724 <= r.build < 59587, + ) + self.register_datapack( + datapacks["LotV"]["59587"], + lambda r: r.expansion == "LotV" and 59587 <= r.build < 70154, + ) + self.register_datapack( + datapacks["LotV"]["70154"], + lambda r: r.expansion == "LotV" and 70154 <= r.build, + ) # Internal Methods def _get_reader(self, data_file): @@ -652,7 +867,11 @@ def _get_reader(self, data_file): if callback(self): return reader else: - raise ValueError("Valid {0} reader could not found for build {1}".format(data_file, self.build)) + raise ValueError( + "Valid {0} reader could not found for build {1}".format( + data_file, self.build + ) + ) def _get_datapack(self): for callback, datapack in self.registered_datapacks: @@ -665,18 +884,21 @@ def _read_data(self, data_file, reader): data = utils.extract_data_file(data_file, self.archive) if data: self.raw_data[data_file] = reader(data, self) - elif self.opt['debug'] and data_file not in ['replay.message.events', 'replay.tracker.events']: + elif self.opt["debug"] and data_file not in [ + "replay.message.events", + "replay.tracker.events", + ]: raise ValueError("{0} not found in archive".format(data_file)) def __getstate__(self): state = self.__dict__.copy() - del state['registered_readers'] - del state['registered_datapacks'] + del state["registered_readers"] + del state["registered_datapacks"] return state class Map(Resource): - url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma' + url_template = "http://{0}.depot.battle.net:1119/{1}.s2ma" def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): super(Map, self).__init__(map_file, filename, **options) @@ -706,48 +928,54 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option self.archive = mpyq.MPQArchive(map_file) #: A byte string representing the minimap in tga format. - self.minimap = self.archive.read_file('Minimap.tga') + self.minimap = self.archive.read_file("Minimap.tga") # This will only populate the fields for maps with enUS localizations. # Clearly this isn't a great solution but we can't be throwing exceptions # just because US English wasn't a concern of the map author. # TODO: Make this work regardless of the localizations available. - game_strings_file = self.archive.read_file('enUS.SC2Data\LocalizedData\GameStrings.txt') + game_strings_file = self.archive.read_file( + "enUS.SC2Data\LocalizedData\GameStrings.txt" + ) if game_strings_file: - for line in game_strings_file.decode('utf8').split('\r\n'): + for line in game_strings_file.decode("utf8").split("\r\n"): if len(line) == 0: continue - key, value = line.split('=', 1) - if key == 'DocInfo/Name': + key, value = line.split("=", 1) + if key == "DocInfo/Name": self.name = value - elif key == 'DocInfo/Author': + elif key == "DocInfo/Author": self.author = value - elif key == 'DocInfo/DescLong': + elif key == "DocInfo/DescLong": self.description = value - elif key == 'DocInfo/Website': + elif key == "DocInfo/Website": self.website = value #: A reference to the map's :class:`~sc2reader.objects.MapInfo` object self.map_info = None - map_info_file = self.archive.read_file('MapInfo') + map_info_file = self.archive.read_file("MapInfo") if map_info_file: self.map_info = MapInfo(map_info_file) - doc_info_file = self.archive.read_file('DocumentInfo') + doc_info_file = self.archive.read_file("DocumentInfo") if doc_info_file: - doc_info = ElementTree.fromstring(doc_info_file.decode('utf8')) + doc_info = ElementTree.fromstring(doc_info_file.decode("utf8")) - icon_path_node = doc_info.find('Icon/Value') + icon_path_node = doc_info.find("Icon/Value") #: (Optional) The path to the icon for the map, relative to the archive root self.icon_path = icon_path_node.text if icon_path_node is not None else None #: (Optional) The icon image for the map in tga format - self.icon = self.archive.read_file(self.icon_path) if self.icon_path is not None else None + self.icon = ( + self.archive.read_file(self.icon_path) + if self.icon_path is not None + else None + ) #: A list of module names this map depends on self.dependencies = list() - for dependency_node in doc_info.findall('Dependencies/Value'): + for dependency_node in doc_info.findall("Dependencies/Value"): self.dependencies.append(dependency_node.text) @classmethod @@ -755,24 +983,23 @@ def get_url(cls, region, map_hash): """Builds a download URL for the map from its components.""" if region and map_hash: # it seems like sea maps are stored on us depots. - region = 'us' if region == 'sea' else region + region = "us" if region == "sea" else region return cls.url_template.format(region, map_hash) else: return None class Localization(Resource, dict): - def __init__(self, s2ml_file, **options): Resource.__init__(self, s2ml_file, **options) xml = ElementTree.parse(s2ml_file) - for entry in xml.findall('e'): - self[int(entry.attrib['id'])] = entry.text + for entry in xml.findall("e"): + self[int(entry.attrib["id"])] = entry.text class GameSummary(Resource): - url_template = 'http://{0}.depot.battle.net:1119/{1}.s2gs' + url_template = "http://{0}.depot.battle.net:1119/{1}.s2gs" #: Game speed game_speed = str() @@ -801,7 +1028,7 @@ class GameSummary(Resource): #: Map localization urls localization_urls = dict() - def __init__(self, summary_file, filename=None, lang='enUS', **options): + def __init__(self, summary_file, filename=None, lang="enUS", **options): super(GameSummary, self).__init__(summary_file, filename, lang=lang, **options) #: A dict of team# -> teams @@ -842,32 +1069,40 @@ def __init__(self, summary_file, filename=None, lang='enUS', **options): self.parts.append(buffer.read_struct()) self.load_translations() - dependencies = [sheet[1] for sheet in self.lang_sheets['enUS']] - if 'Swarm (Mod)' in dependencies: - self.expansion = 'HotS' - elif 'Liberty (Mod)' in dependencies: - self.expansion = 'WoL' + dependencies = [sheet[1] for sheet in self.lang_sheets["enUS"]] + if "Swarm (Mod)" in dependencies: + self.expansion = "HotS" + elif "Liberty (Mod)" in dependencies: + self.expansion = "WoL" else: - self.expansion = '' + self.expansion = "" self.end_time = datetime.utcfromtimestamp(self.parts[0][8]) - self.game_speed = LOBBY_PROPERTIES[0xBB8][1][self.parts[0][0][1].decode('utf8')] + self.game_speed = LOBBY_PROPERTIES[0xBB8][1][self.parts[0][0][1].decode("utf8")] self.game_length = utils.Length(seconds=self.parts[0][7]) - self.real_length = utils.Length(seconds=int(self.parts[0][7]/GAME_SPEED_FACTOR[self.expansion][self.game_speed])) - self.start_time = datetime.utcfromtimestamp(self.parts[0][8] - self.real_length.seconds) + self.real_length = utils.Length( + seconds=int( + self.parts[0][7] / GAME_SPEED_FACTOR[self.expansion][self.game_speed] + ) + ) + self.start_time = datetime.utcfromtimestamp( + self.parts[0][8] - self.real_length.seconds + ) self.load_map_info() self.load_settings() self.load_player_stats() self.load_players() - self.game_type = self.settings['Teams'].replace(" ", "") + self.game_type = self.settings["Teams"].replace(" ", "") self.real_type = utils.get_real_type(self.teams) # The s2gs file also keeps reference to a series of s2mv files # Some of these appear to be encoded bytes and others appear to be # the preview images that authors may bundle with their maps. - self.s2mv_urls = [str(utils.DepotFile(file_hash)) for file_hash in self.parts[0][6][7]] + self.s2mv_urls = [ + str(utils.DepotFile(file_hash)) for file_hash in self.parts[0][6][7] + ] def load_translations(self): # This section of the file seems to map numerical ids to their @@ -908,11 +1143,11 @@ def load_translations(self): # # Sometimes these byte strings are all NULLed out and need to be ignored. for localization in self.parts[0][6][8]: - language = localization[0].decode('utf8') + language = localization[0].decode("utf8") files = list() for file_hash in localization[1]: - if file_hash[:4].decode('utf8') != '\x00\x00\x00\x00': + if file_hash[:4].decode("utf8") != "\x00\x00\x00\x00": files.append(utils.DepotFile(file_hash)) self.localization_urls[language] = files @@ -928,7 +1163,7 @@ def load_translations(self): self.lang_sheets = dict() self.translations = dict() for lang, files in self.localization_urls.items(): - if lang != self.opt['lang']: + if lang != self.opt["lang"]: continue sheets = list() @@ -939,9 +1174,11 @@ def load_translations(self): for uid, (sheet, item) in self.id_map.items(): if sheet < len(sheets) and item in sheets[sheet]: translation[uid] = sheets[sheet][item] - elif self.opt['debug']: + elif self.opt["debug"]: msg = "No {0} translation for sheet {1}, item {2}" - raise SC2ReaderLocalizationError(msg.format(self.opt['lang'], sheet, item)) + raise SC2ReaderLocalizationError( + msg.format(self.opt["lang"], sheet, item) + ) else: translation[uid] = "Unknown" @@ -949,17 +1186,21 @@ def load_translations(self): self.translations[lang] = translation def load_map_info(self): - map_strings = self.lang_sheets[self.opt['lang']][-1] + map_strings = self.lang_sheets[self.opt["lang"]][-1] self.map_name = map_strings[1] self.map_description = map_strings[2] self.map_tileset = map_strings[3] def load_settings(self): - Property = namedtuple('Property', ['id', 'values', 'requirements', 'defaults', 'is_lobby']) + Property = namedtuple( + "Property", ["id", "values", "requirements", "defaults", "is_lobby"] + ) properties = dict() for p in self.parts[0][5]: - properties[p[0][1]] = Property(p[0][1], p[1], p[3], p[8], isinstance(p[8], dict)) + properties[p[0][1]] = Property( + p[0][1], p[1], p[3], p[8], isinstance(p[8], dict) + ) settings = dict() for setting in self.parts[0][6][6]: @@ -1008,7 +1249,7 @@ def use_property(prop, player=None): activated[(prop.id, player)] = use return use - translation = self.translations[self.opt['lang']] + translation = self.translations[self.opt["lang"]] for uid, prop in properties.items(): name = translation.get(uid, "Unknown") if prop.is_lobby: @@ -1022,7 +1263,7 @@ def use_property(prop, player=None): self.player_settings[index][name] = translation[(uid, value)] def load_player_stats(self): - translation = self.translations[self.opt['lang']] + translation = self.translations[self.opt["lang"]] stat_items = sum([p[0] for p in self.parts[3:]], []) @@ -1043,7 +1284,12 @@ def load_player_stats(self): if not value: continue - if stat_name in ('Army Value', 'Resource Collection Rate', 'Upgrade Spending', 'Workers Active'): + if stat_name in ( + "Army Value", + "Resource Collection Rate", + "Upgrade Spending", + "Workers Active", + ): # Each point entry for the graph is laid out as follows # # {0:Value, 1:0, 2:Time} @@ -1062,13 +1308,15 @@ def load_player_stats(self): # up to the first 64 successful actions in the game. for pindex, commands in enumerate(item[1]): for command in commands: - self.build_orders[pindex].append(BuildEntry( - supply=command[0], - total_supply=command[1] & 0xff, - time=int((command[2] >> 8) / 16), - order=stat_name, - build_index=command[1] >> 16 - )) + self.build_orders[pindex].append( + BuildEntry( + supply=command[0], + total_supply=command[1] & 0xFF, + time=int((command[2] >> 8) / 16), + order=stat_name, + build_index=command[1] >> 16, + ) + ) elif stat_id != 83886080: # We know this one is always bad. self.logger.warn("Untranslatable key = {0}".format(stat_id)) @@ -1094,7 +1342,7 @@ def load_players(self): player.unknown2 = struct[0][1][1] # Either a referee or a spectator, nothing else to do - if settings.get('Participant Role', '') != 'Participant': + if settings.get("Participant Role", "") != "Participant": self.observers.append(player) continue @@ -1104,7 +1352,7 @@ def load_players(self): if player.is_winner: self.winners.append(player.pid) - team_id = int(settings['Team'].split(' ')[1]) + team_id = int(settings["Team"].split(" ")[1]) if team_id not in self.team: self.team[team_id] = Team(team_id) self.teams.append(self.team[team_id]) @@ -1113,49 +1361,51 @@ def load_players(self): self.team[team_id].players.append(player) # We can just copy these settings right over - player.color = utils.Color(name=settings.get('Color', None)) - player.pick_race = settings.get('Race', None) - player.handicap = settings.get('Handicap', None) + player.color = utils.Color(name=settings.get("Color", None)) + player.pick_race = settings.get("Race", None) + player.handicap = settings.get("Handicap", None) # Overview Tab - player.resource_score = stats.get('Resources', None) - player.structure_score = stats.get('Structures', None) - player.unit_score = stats.get('Units', None) - player.overview_score = stats.get('Overview', None) + player.resource_score = stats.get("Resources", None) + player.structure_score = stats.get("Structures", None) + player.unit_score = stats.get("Units", None) + player.overview_score = stats.get("Overview", None) # Units Tab - player.units_killed = stats.get('Killed Unit Count', None) - player.structures_built = stats.get('Structures Built', None) - player.units_trained = stats.get('Units Trained', None) - player.structures_razed = stats.get('Structures Razed Count', None) + player.units_killed = stats.get("Killed Unit Count", None) + player.structures_built = stats.get("Structures Built", None) + player.units_trained = stats.get("Units Trained", None) + player.structures_razed = stats.get("Structures Razed Count", None) # Graphs Tab # Keep income_graph for backwards compatibility - player.army_graph = stats.get('Army Value') - player.resource_collection_graph = stats.get('Resource Collection Rate', None) + player.army_graph = stats.get("Army Value") + player.resource_collection_graph = stats.get( + "Resource Collection Rate", None + ) player.income_graph = player.resource_collection_graph # HotS Stats - player.upgrade_spending_graph = stats.get('Upgrade Spending', None) - player.workers_active_graph = stats.get('Workers Active', None) - player.enemies_destroyed = stats.get('Enemies Destroyed:', None) - player.time_supply_capped = stats.get('Time Supply Capped', None) - player.idle_production_time = stats.get('Idle Production Time', None) - player.resources_spent = stats.get('Resources Spent:', None) - player.apm = stats.get('APM', None) + player.upgrade_spending_graph = stats.get("Upgrade Spending", None) + player.workers_active_graph = stats.get("Workers Active", None) + player.enemies_destroyed = stats.get("Enemies Destroyed:", None) + player.time_supply_capped = stats.get("Time Supply Capped", None) + player.idle_production_time = stats.get("Idle Production Time", None) + player.resources_spent = stats.get("Resources Spent:", None) + player.apm = stats.get("APM", None) # Economic Breakdown Tab if isinstance(player.income_graph, Graph): values = player.income_graph.values - player.resource_collection_rate = int(sum(values)/len(values)) + player.resource_collection_rate = int(sum(values) / len(values)) else: # In old s2gs files the field with this name was actually a number not a graph player.resource_collection_rate = player.income_graph player.resource_collection_graph = None player.income_graph = None - player.avg_unspent_resources = stats.get('Average Unspent Resources', None) - player.workers_created = stats.get('Workers Created', None) + player.avg_unspent_resources = stats.get("Average Unspent Resources", None) + player.workers_created = stats.get("Workers Created", None) # Build Orders Tab player.build_order = self.build_orders.get(index, None) @@ -1164,15 +1414,21 @@ def load_players(self): self.player[player.pid] = player def __str__(self): - return "{0} - {1} {2}".format(self.start_time, self.game_length, 'v'.join(''.join(p.play_race[0] for p in team.players) for team in self.teams)) + return "{0} - {1} {2}".format( + self.start_time, + self.game_length, + "v".join( + "".join(p.play_race[0] for p in team.players) for team in self.teams + ), + ) class MapHeader(Resource): """**Experimental**""" - base_url_template = 'http://{0}.depot.battle.net:1119/{1}.{2}' - url_template = 'http://{0}.depot.battle.net:1119/{1}.s2mh' - image_url_template = 'http://{0}.depot.battle.net:1119/{1}.s2mv' + base_url_template = "http://{0}.depot.battle.net:1119/{1}.{2}" + url_template = "http://{0}.depot.battle.net:1119/{1}.s2mh" + image_url_template = "http://{0}.depot.battle.net:1119/{1}.s2mv" #: The name of the map name = str() @@ -1203,20 +1459,26 @@ def __init__(self, header_file, filename=None, **options): self.name = self.data[0][1] # Blizzard - self.blizzard = (self.data[0][11] == 'BLIZ') + self.blizzard = self.data[0][11] == "BLIZ" # Parse image hash parsed_hash = utils.parse_hash(self.data[0][1]) - self.image_hash = parsed_hash['hash'] - self.image_url = self.image_url_template.format(parsed_hash['server'], parsed_hash['hash']) + self.image_hash = parsed_hash["hash"] + self.image_url = self.image_url_template.format( + parsed_hash["server"], parsed_hash["hash"] + ) # Parse map hash parsed_hash = utils.parse_hash(self.data[0][2]) - self.map_hash = parsed_hash['hash'] - self.map_url = self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']) + self.map_hash = parsed_hash["hash"] + self.map_url = self.base_url_template.format( + parsed_hash["server"], parsed_hash["hash"], parsed_hash["type"] + ) # Parse localization hashes l18n_struct = self.data[0][4][8] for l in l18n_struct: parsed_hash = utils.parse_hash(l[1][0]) - self.localization_urls[l[0]] = self.base_url_template.format(parsed_hash['server'], parsed_hash['hash'], parsed_hash['type']) + self.localization_urls[l[0]] = self.base_url_template.format( + parsed_hash["server"], parsed_hash["hash"], parsed_hash["type"] + ) diff --git a/sc2reader/scripts/sc2attributes.py b/sc2reader/scripts/sc2attributes.py index da0160e9..109cf30b 100644 --- a/sc2reader/scripts/sc2attributes.py +++ b/sc2reader/scripts/sc2attributes.py @@ -39,7 +39,7 @@ import sc2reader try: - raw_input # Python 2 + raw_input # Python 2 except NameError: raw_input = input # Python 3 @@ -49,51 +49,68 @@ def main(): global decisions - parser = argparse.ArgumentParser(description="Recursively parses replay files, inteded for debugging parse issues.") - parser.add_argument('folders', metavar='folder', type=str, nargs='+', help="Path to a folder") + parser = argparse.ArgumentParser( + description="Recursively parses replay files, inteded for debugging parse issues." + ) + parser.add_argument( + "folders", metavar="folder", type=str, nargs="+", help="Path to a folder" + ) args = parser.parse_args() scripts_dir = os.path.dirname(os.path.abspath(__file__)) - data_path = os.path.normpath(os.path.join(scripts_dir, '..', 'data', 'attributes.json')) + data_path = os.path.normpath( + os.path.join(scripts_dir, "..", "data", "attributes.json") + ) attributes = dict() if os.path.exists(data_path): - with open(data_path, 'r') as data_file: + with open(data_path, "r") as data_file: data = json.load(data_file) - attributes = data.get('attributes', attributes) - decisions = pickle.loads(data.get('decisions', '(dp0\n.')) + attributes = data.get("attributes", attributes) + decisions = pickle.loads(data.get("decisions", "(dp0\n.")) for folder in args.folders: - for path in sc2reader.utils.get_files(folder, extension='s2gs'): + for path in sc2reader.utils.get_files(folder, extension="s2gs"): try: summary = sc2reader.load_game_summary(path) for prop in summary.parts[0][5]: group_key = prop[0][1] - group_name = summary.translations['enUS'][group_key] + group_name = summary.translations["enUS"][group_key] attribute_values = dict() if str(group_key) in attributes: attribute_name, attribute_values = attributes[str(group_key)] if attribute_name != group_name: - group_name = get_choice(group_key, attribute_name, group_name) + group_name = get_choice( + group_key, attribute_name, group_name + ) for value in prop[1]: - value_key = value[0].strip("\x00 ").replace(' v ', 'v') - value_name = summary.lang_sheets['enUS'][value[1][0][1]][value[1][0][2]] + value_key = value[0].strip("\x00 ").replace(" v ", "v") + value_name = summary.lang_sheets["enUS"][value[1][0][1]][ + value[1][0][2] + ] if str(value_key) in attribute_values: attribute_value_name = attribute_values[str(value_key)] if value_name != attribute_value_name: - value_name = get_choice((group_key, value_key), attribute_value_name, value_name) + value_name = get_choice( + (group_key, value_key), + attribute_value_name, + value_name, + ) attribute_values[str(value_key)] = value_name - attributes["{0:0>4}".format(group_key)] = (group_name, attribute_values) + attributes["{0:0>4}".format(group_key)] = ( + group_name, + attribute_values, + ) except Exception as e: if isinstance(e, KeyboardInterrupt): raise else: traceback.print_exc() - with open(data_path, 'w') as data_file: + with open(data_path, "w") as data_file: data = dict(attributes=attributes, decisions=pickle.dumps(decisions)) json.dump(data, data_file, indent=2, sort_keys=True) @@ -104,20 +121,22 @@ def get_choice(s2gs_key, old_value, new_value): # This way old/new values can be swapped and decision is remembered key = frozenset([s2gs_key, old_value, new_value]) if key not in decisions: - print("Naming conflict on {0}: {1} != {2}".format(s2gs_key, old_value, new_value)) + print( + "Naming conflict on {0}: {1} != {2}".format(s2gs_key, old_value, new_value) + ) print("Which do you want to use?") print(" (o) Old value '{0}'".format(old_value)) print(" (n) New value '{0}'".format(new_value)) while True: answer = raw_input("Choose 'o' or 'n' then press enter: ").lower() - if answer not in ('o', 'n'): - print('Invalid choice `{0}`'.format(answer)) + if answer not in ("o", "n"): + print("Invalid choice `{0}`".format(answer)) else: break - decisions[key] = {'o': old_value, 'n': new_value}[answer] + decisions[key] = {"o": old_value, "n": new_value}[answer] print("") return decisions[key] -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/sc2reader/scripts/sc2json.py b/sc2reader/scripts/sc2json.py index bd42178f..7cc2f331 100755 --- a/sc2reader/scripts/sc2json.py +++ b/sc2reader/scripts/sc2json.py @@ -8,16 +8,38 @@ def main(): import argparse + parser = argparse.ArgumentParser(description="Prints replay data to a json string.") - parser.add_argument('--indent', '-i', type=int, default=None, help="The per-line indent to use when printing a human readable json string") - parser.add_argument('--encoding', '-e', type=str, default='UTF-8', help="The character encoding use..") - parser.add_argument('path', metavar='path', type=str, nargs=1, help="Path to the replay to serialize.") + parser.add_argument( + "--indent", + "-i", + type=int, + default=None, + help="The per-line indent to use when printing a human readable json string", + ) + parser.add_argument( + "--encoding", + "-e", + type=str, + default="UTF-8", + help="The character encoding use..", + ) + parser.add_argument( + "path", + metavar="path", + type=str, + nargs=1, + help="Path to the replay to serialize.", + ) args = parser.parse_args() factory = sc2reader.factories.SC2Factory() - factory.register_plugin("Replay", toJSON(encoding=args.encoding, indent=args.indent)) + factory.register_plugin( + "Replay", toJSON(encoding=args.encoding, indent=args.indent) + ) replay_json = factory.load_replay(args.path[0]) print(replay_json) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 571b0c05..311db343 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -22,20 +22,32 @@ import sc2reader import traceback -sc2reader.log_utils.log_to_console('INFO') +sc2reader.log_utils.log_to_console("INFO") def main(): - parser = argparse.ArgumentParser(description="Recursively parses replay files, inteded for debugging parse issues.") - parser.add_argument('--one_each', help="Attempt to parse only one Ladder replay for each release_string", action="store_true") - parser.add_argument('--ladder_only', help="If a non-ladder game fails, ignore it", action="store_true") - parser.add_argument('folders', metavar='folder', type=str, nargs='+', help="Path to a folder") + parser = argparse.ArgumentParser( + description="Recursively parses replay files, inteded for debugging parse issues." + ) + parser.add_argument( + "--one_each", + help="Attempt to parse only one Ladder replay for each release_string", + action="store_true", + ) + parser.add_argument( + "--ladder_only", + help="If a non-ladder game fails, ignore it", + action="store_true", + ) + parser.add_argument( + "folders", metavar="folder", type=str, nargs="+", help="Path to a folder" + ) args = parser.parse_args() releases_parsed = set() for folder in args.folders: print("dealing with {0}".format(folder)) - for path in sc2reader.utils.get_files(folder, extension='SC2Replay'): + for path in sc2reader.utils.get_files(folder, extension="SC2Replay"): try: rs = sc2reader.load_replay(path, load_level=0).release_string already_did = rs in releases_parsed @@ -46,18 +58,58 @@ def main(): replay = sc2reader.load_replay(path, debug=True) human_pids = set([human.pid for human in replay.humans]) - event_pids = set([event.player.pid for event in replay.events if getattr(event, 'player', None)]) - player_pids = set([player.pid for player in replay.players if player.is_human]) - ability_pids = set([event.player.pid for event in replay.events if 'CommandEvent' in event.name]) + event_pids = set( + [ + event.player.pid + for event in replay.events + if getattr(event, "player", None) + ] + ) + player_pids = set( + [player.pid for player in replay.players if player.is_human] + ) + ability_pids = set( + [ + event.player.pid + for event in replay.events + if "CommandEvent" in event.name + ] + ) if human_pids != event_pids: - print('Event Pid problem! pids={pids} but event pids={event_pids}'.format(pids=human_pids, event_pids=event_pids)) - print(' with {path}: {build} - {real_type} on {map_name} - Played {start_time}'.format(path=path, **replay.__dict__)) + print( + "Event Pid problem! pids={pids} but event pids={event_pids}".format( + pids=human_pids, event_pids=event_pids + ) + ) + print( + " with {path}: {build} - {real_type} on {map_name} - Played {start_time}".format( + path=path, **replay.__dict__ + ) + ) elif player_pids != ability_pids: - print('Ability Pid problem! pids={pids} but event pids={event_pids}'.format(pids=player_pids, event_pids=ability_pids)) - print(' with {path}: {build} - {real_type} on {map_name} - Played {start_time}'.format(path=path, **replay.__dict__)) + print( + "Ability Pid problem! pids={pids} but event pids={event_pids}".format( + pids=player_pids, event_pids=ability_pids + ) + ) + print( + " with {path}: {build} - {real_type} on {map_name} - Played {start_time}".format( + path=path, **replay.__dict__ + ) + ) else: - print('No problems with {path}: {build} - {real_type} on {map_name} - Played {start_time}'.format(path=path, **replay.__dict__)) - print('Units were: {units}'.format(units=set([obj.name for obj in replay.objects.values()]))) + print( + "No problems with {path}: {build} - {real_type} on {map_name} - Played {start_time}".format( + path=path, **replay.__dict__ + ) + ) + print( + "Units were: {units}".format( + units=set( + [obj.name for obj in replay.objects.values()] + ) + ) + ) except sc2reader.exceptions.ReadError as e: if args.ladder_only and not e.replay.is_ladder: @@ -65,19 +117,27 @@ def main(): print("") print(path) - print('{build} - {real_type} on {map_name} - Played {start_time}'.format(**e.replay.__dict__)) - print('[ERROR] {}', e) + print( + "{build} - {real_type} on {map_name} - Played {start_time}".format( + **e.replay.__dict__ + ) + ) + print("[ERROR] {}", e) for event in e.game_events[-5:]: - print('{0}'.format(event)) - print(e.buffer.read_range(e.location, e.location + 50).encode('hex')) + print("{0}".format(event)) + print(e.buffer.read_range(e.location, e.location + 50).encode("hex")) print except Exception as e: print("") print(path) try: replay = sc2reader.load_replay(path, debug=True, load_level=2) - print('{build} - {real_type} on {map_name} - Played {start_time}'.format(**replay.__dict__)) - print('[ERROR] {0}'.format(e)) + print( + "{build} - {real_type} on {map_name} - Played {start_time}".format( + **replay.__dict__ + ) + ) + print("[ERROR] {0}".format(e)) for pid, attributes in replay.attributes.items(): print("{0} {1}".format(pid, attributes)) for pid, info in enumerate(replay.players): @@ -88,12 +148,16 @@ def main(): print("") except Exception as e2: replay = sc2reader.load_replay(path, debug=True, load_level=0) - print('Total failure parsing {release_string}'.format(**replay.__dict__)) - print('[ERROR] {0}'.format(e)) - print('[ERROR] {0}'.format(e2)) + print( + "Total failure parsing {release_string}".format( + **replay.__dict__ + ) + ) + print("[ERROR] {0}".format(e)) + print("[ERROR] {0}".format(e2)) traceback.print_exc() print -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index c780d16e..92cfe686 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -25,9 +25,17 @@ def printReplay(filepath, arguments): lineups = [team.lineup for team in replay.teams] print(" Teams: {0}".format("v".join(lineups))) for team in replay.teams: - print(" Team {0}\t{1} ({2})".format(team.number, team.players[0].name, team.players[0].pick_race[0])) + print( + " Team {0}\t{1} ({2})".format( + team.number, team.players[0].name, team.players[0].pick_race[0] + ) + ) for player in team.players[1:]: - print(" \t{0} ({1})".format(player.name, player.pick_race[0])) + print( + " \t{0} ({1})".format( + player.name, player.pick_race[0] + ) + ) if arguments.observers: print(" Observers:") for observer in replay.observers: @@ -45,12 +53,16 @@ def printReplay(filepath, arguments): raise return prev = e.game_events[-1] - print("\nVersion {0} replay:\n\t{1}".format(e.replay.release_string, e.replay.filepath)) + print( + "\nVersion {0} replay:\n\t{1}".format( + e.replay.release_string, e.replay.filepath + ) + ) print("\t{0}, Type={1:X}".format(e.msg, e.type)) print("\tPrevious Event: {0}".format(prev.name)) - print("\t\t" + prev.bytes.encode('hex')) + print("\t\t" + prev.bytes.encode("hex")) print("\tFollowing Bytes:") - print("\t\t" + e.buffer.read_range(e.location, e.location + 30).encode('hex')) + print("\t\t" + e.buffer.read_range(e.location, e.location + 30).encode("hex")) print("Error with '{0}': ".format(filepath)) print(e) except Exception as e: @@ -80,54 +92,100 @@ def printGameSummary(filepath, arguments): print("\n== {0} ==\n".format(player)) for order in summary.build_orders[player.pid]: msg = " {0:0>2}:{1:0>2} {2:<35} {3:0>2}/{4}" - print(msg.format(order.time / 60, order.time % 60, order.order, order.supply, order.total_supply)) + print( + msg.format( + order.time / 60, + order.time % 60, + order.order, + order.supply, + order.total_supply, + ) + ) print("") def main(): parser = argparse.ArgumentParser( description="""Prints basic information from Starcraft II replay and - game summary files or directories.""") - parser.add_argument('--recursive', action="store_true", default=True, - help="Recursively read through directories of Starcraft II files [default on]") - - required = parser.add_argument_group('Required Arguments') - required.add_argument('paths', metavar='filename', type=str, nargs='+', - help="Paths to one or more Starcraft II files or directories") - - shared_args = parser.add_argument_group('Shared Arguments') - shared_args.add_argument('--date', action="store_true", default=True, - help="print game date [default on]") - shared_args.add_argument('--length', action="store_true", default=False, - help="print game duration mm:ss in game time (not real time) [default off]") - shared_args.add_argument('--map', action="store_true", default=True, - help="print map name [default on]") - shared_args.add_argument('--teams', action="store_true", default=True, - help="print teams, their players, and the race matchup [default on]") - shared_args.add_argument('--observers', action="store_true", default=True, - help="print observers") - - replay_args = parser.add_argument_group('Replay Options') - replay_args.add_argument('--messages', action="store_true", default=False, - help="print(in-game player chat messages [default off]") - replay_args.add_argument('--version', action="store_true", default=True, - help="print(the release string as seen in game [default on]") - - s2gs_args = parser.add_argument_group('Game Summary Options') - s2gs_args.add_argument('--builds', action="store_true", default=False, - help="print(player build orders (first 64 items) [default off]") + game summary files or directories.""" + ) + parser.add_argument( + "--recursive", + action="store_true", + default=True, + help="Recursively read through directories of Starcraft II files [default on]", + ) + + required = parser.add_argument_group("Required Arguments") + required.add_argument( + "paths", + metavar="filename", + type=str, + nargs="+", + help="Paths to one or more Starcraft II files or directories", + ) + + shared_args = parser.add_argument_group("Shared Arguments") + shared_args.add_argument( + "--date", action="store_true", default=True, help="print game date [default on]" + ) + shared_args.add_argument( + "--length", + action="store_true", + default=False, + help="print game duration mm:ss in game time (not real time) [default off]", + ) + shared_args.add_argument( + "--map", action="store_true", default=True, help="print map name [default on]" + ) + shared_args.add_argument( + "--teams", + action="store_true", + default=True, + help="print teams, their players, and the race matchup [default on]", + ) + shared_args.add_argument( + "--observers", action="store_true", default=True, help="print observers" + ) + + replay_args = parser.add_argument_group("Replay Options") + replay_args.add_argument( + "--messages", + action="store_true", + default=False, + help="print(in-game player chat messages [default off]", + ) + replay_args.add_argument( + "--version", + action="store_true", + default=True, + help="print(the release string as seen in game [default on]", + ) + + s2gs_args = parser.add_argument_group("Game Summary Options") + s2gs_args.add_argument( + "--builds", + action="store_true", + default=False, + help="print(player build orders (first 64 items) [default off]", + ) arguments = parser.parse_args() for path in arguments.paths: depth = -1 if arguments.recursive else 0 for filepath in utils.get_files(path, depth=depth): name, ext = os.path.splitext(filepath) - if ext.lower() == '.sc2replay': - print("\n--------------------------------------\n{0}\n".format(filepath)) + if ext.lower() == ".sc2replay": + print( + "\n--------------------------------------\n{0}\n".format(filepath) + ) printReplay(filepath, arguments) - elif ext.lower() == '.s2gs': - print("\n--------------------------------------\n{0}\n".format(filepath)) + elif ext.lower() == ".s2gs": + print( + "\n--------------------------------------\n{0}\n".format(filepath) + ) printGameSummary(filepath, arguments) -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index 7a72c7ed..d8711962 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -28,6 +28,7 @@ def getch(): termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm) fcntl.fcntl(fd, fcntl.F_SETFL, oldflags) + except ImportError as e: try: # Opps, we might be on windows, try this one @@ -49,17 +50,39 @@ def main(): key to advance through the events in sequential order.""" ) - parser.add_argument('FILE', type=str, help="The file you would like to replay") - parser.add_argument('--player', default=0, type=int, help="The number of the player you would like to watch. Defaults to 0 (All).") - parser.add_argument('--bytes', default=False, action="store_true", help="Displays the byte code of the event in hex after each event.") - parser.add_argument('--hotkeys', default=False, action="store_true", help="Shows the hotkey events in the event stream.") - parser.add_argument('--cameras', default=False, action="store_true", help="Shows the camera events in the event stream.") + parser.add_argument("FILE", type=str, help="The file you would like to replay") + parser.add_argument( + "--player", + default=0, + type=int, + help="The number of the player you would like to watch. Defaults to 0 (All).", + ) + parser.add_argument( + "--bytes", + default=False, + action="store_true", + help="Displays the byte code of the event in hex after each event.", + ) + parser.add_argument( + "--hotkeys", + default=False, + action="store_true", + help="Shows the hotkey events in the event stream.", + ) + parser.add_argument( + "--cameras", + default=False, + action="store_true", + help="Shows the camera events in the event stream.", + ) args = parser.parse_args() for filename in sc2reader.utils.get_files(args.FILE): replay = sc2reader.load_replay(filename, debug=True) print("Release {0}".format(replay.release_string)) - print("{0} on {1} at {2}".format(replay.type, replay.map_name, replay.start_time)) + print( + "{0} on {1} at {2}".format(replay.type, replay.map_name, replay.start_time) + ) print("") for team in replay.teams: print(team) @@ -77,17 +100,19 @@ def main(): # Loop through the events for event in events: - if isinstance(event, CommandEvent) or \ - isinstance(event, SelectionEvent) or \ - isinstance(event, PlayerLeaveEvent) or \ - isinstance(event, GameStartEvent) or \ - (args.hotkeys and isinstance(event, HotkeyEvent)) or \ - (args.cameras and isinstance(event, CameraEvent)): + if ( + isinstance(event, CommandEvent) + or isinstance(event, SelectionEvent) + or isinstance(event, PlayerLeaveEvent) + or isinstance(event, GameStartEvent) + or (args.hotkeys and isinstance(event, HotkeyEvent)) + or (args.cameras and isinstance(event, CameraEvent)) + ): print(event) getch() if args.bytes: - print("\t"+event.bytes.encode('hex')) + print("\t" + event.bytes.encode("hex")) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/sc2reader/scripts/utils.py b/sc2reader/scripts/utils.py index 3eb05188..cf998e5d 100644 --- a/sc2reader/scripts/utils.py +++ b/sc2reader/scripts/utils.py @@ -35,29 +35,29 @@ def new(cls, **options): def _split_lines(self, text, width): lines = list() - main_indent = len(re.match(r'( *)', text).group(1)) + main_indent = len(re.match(r"( *)", text).group(1)) # Wrap each line individually to allow for partial formatting for line in text.splitlines(): # Get this line's indent and figure out what indent to use # if the line wraps. Account for lists of small variety. - indent = len(re.match(r'( *)', line).group(1)) - list_match = re.match(r'( *)(([*-+>]+|\w+\)|\w+\.) +)', line) - if(list_match): + indent = len(re.match(r"( *)", line).group(1)) + list_match = re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line) + if list_match: sub_indent = indent + len(list_match.group(2)) else: sub_indent = indent # Textwrap will do all the hard work for us - line = self._whitespace_matcher.sub(' ', line).strip() + line = self._whitespace_matcher.sub(" ", line).strip() new_lines = textwrap.wrap( text=line, width=width, - initial_indent=' '*(indent-main_indent), - subsequent_indent=' '*(sub_indent-main_indent), + initial_indent=" " * (indent - main_indent), + subsequent_indent=" " * (sub_indent - main_indent), ) # Blank lines get eaten by textwrap, put it back with [' '] - lines.extend(new_lines or [' ']) + lines.extend(new_lines or [" "]) return lines diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 2ee3d382..41e4ed09 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,21 +20,21 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = 'http://{0}.depot.battle.net:1119/{1}.{2}' + url_template = "http://{0}.depot.battle.net:1119/{1}.{2}" def __init__(self, bytes): #: The server the file is hosted on - self.server = bytes[4:8].decode('utf-8').strip('\x00 ') + self.server = bytes[4:8].decode("utf-8").strip("\x00 ") # There is no SEA depot, use US instead - if self.server == 'SEA': - self.server = 'US' + if self.server == "SEA": + self.server = "US" #: The unique content based hash of the file - self.hash = binascii.b2a_hex(bytes[8:]).decode('utf8') + self.hash = binascii.b2a_hex(bytes[8:]).decode("utf8") #: The extension of the file on the server - self.type = bytes[0:4].decode('utf8') + self.type = bytes[0:4].decode("utf8") @property def url(self): @@ -71,11 +71,12 @@ class Color(object): Only standard Starcraft colors are supported. ValueErrors will be thrown on invalid names or hex values. """ + def __init__(self, name=None, r=0, g=0, b=0, a=255): if name: if name not in COLOR_CODES_INV: self.logger.warn("Invalid color name: " + name) - hexstr = COLOR_CODES_INV.get(name, '000000') + hexstr = COLOR_CODES_INV.get(name, "000000") self.r = int(hexstr[0:2], 16) self.g = int(hexstr[2:4], 16) self.b = int(hexstr[4:6], 16) @@ -114,7 +115,6 @@ def get_real_type(teams): def extract_data_file(data_file, archive): - def recovery_attempt(): try: return archive.read_file(data_file) @@ -151,7 +151,9 @@ def recovery_attempt(): raise MPQError("Unable to extract file: {0}".format(data_file), e) -def get_files(path, exclude=list(), depth=-1, followlinks=False, extension=None, **extras): +def get_files( + path, exclude=list(), depth=-1, followlinks=False, extension=None, **extras +): """ Retrieves files from the given path with configurable behavior. @@ -167,7 +169,9 @@ def get_files(path, exclude=list(), depth=-1, followlinks=False, extension=None, # If an extension is supplied, use it to do a type check if extension: - type_check = lambda path: os.path.splitext(path)[1][1:].lower() == extension.lower() + type_check = ( + lambda path: os.path.splitext(path)[1][1:].lower() == extension.lower() + ) else: type_check = lambda n: True @@ -237,68 +241,76 @@ def toDict(replay): observers = list() for observer in replay.observers: messages = list() - for message in getattr(observer, 'messages', list()): - messages.append({ - 'time': message.time.seconds, - 'text': message.text, - 'is_public': message.to_all - }) - observers.append({ - 'name': getattr(observer, 'name', None), - 'pid': getattr(observer, 'pid', None), - 'messages': messages, - }) + for message in getattr(observer, "messages", list()): + messages.append( + { + "time": message.time.seconds, + "text": message.text, + "is_public": message.to_all, + } + ) + observers.append( + { + "name": getattr(observer, "name", None), + "pid": getattr(observer, "pid", None), + "messages": messages, + } + ) # Build players into dictionary players = list() for player in replay.players: messages = list() for message in player.messages: - messages.append({ - 'time': message.time.seconds, - 'text': message.text, - 'is_public': message.to_all - }) - players.append({ - 'avg_apm': getattr(player, 'avg_apm', None), - 'color': player.color.__dict__ if hasattr(player, 'color') else None, - 'handicap': getattr(player, 'handicap', None), - 'name': getattr(player, 'name', None), - 'pick_race': getattr(player, 'pick_race', None), - 'pid': getattr(player, 'pid', None), - 'play_race': getattr(player, 'play_race', None), - 'result': getattr(player, 'result', None), - 'type': getattr(player, 'type', None), - 'uid': getattr(player, 'uid', None), - 'url': getattr(player, 'url', None), - 'messages': messages, - }) + messages.append( + { + "time": message.time.seconds, + "text": message.text, + "is_public": message.to_all, + } + ) + players.append( + { + "avg_apm": getattr(player, "avg_apm", None), + "color": player.color.__dict__ if hasattr(player, "color") else None, + "handicap": getattr(player, "handicap", None), + "name": getattr(player, "name", None), + "pick_race": getattr(player, "pick_race", None), + "pid": getattr(player, "pid", None), + "play_race": getattr(player, "play_race", None), + "result": getattr(player, "result", None), + "type": getattr(player, "type", None), + "uid": getattr(player, "uid", None), + "url": getattr(player, "url", None), + "messages": messages, + } + ) # Consolidate replay metadata into dictionary return { - 'region': getattr(replay, 'region', None), - 'map_name': getattr(replay, 'map_name', None), - 'file_time': getattr(replay, 'file_time', None), - 'filehash': getattr(replay, 'filehash', None), - 'unix_timestamp': getattr(replay, 'unix_timestamp', None), - 'date': getattr(replay, 'date', None), - 'utc_date': getattr(replay, 'utc_date', None), - 'speed': getattr(replay, 'speed', None), - 'category': getattr(replay, 'category', None), - 'type': getattr(replay, 'type', None), - 'is_ladder': getattr(replay, 'is_ladder', False), - 'is_private': getattr(replay, 'is_private', False), - 'filename': getattr(replay, 'filename', None), - 'file_time': getattr(replay, 'file_time', None), - 'frames': getattr(replay, 'frames', None), - 'build': getattr(replay, 'build', None), - 'release': getattr(replay, 'release_string', None), - 'game_fps': getattr(replay, 'game_fps', None), - 'game_length': getattr(getattr(replay, 'game_length', None), 'seconds', None), - 'players': players, - 'observers': observers, - 'real_length': getattr(getattr(replay, 'real_length', None), 'seconds', None), - 'real_type': getattr(replay, 'real_type', None), - 'time_zone': getattr(replay, 'time_zone', None), - 'versions': getattr(replay, 'versions', None) + "region": getattr(replay, "region", None), + "map_name": getattr(replay, "map_name", None), + "file_time": getattr(replay, "file_time", None), + "filehash": getattr(replay, "filehash", None), + "unix_timestamp": getattr(replay, "unix_timestamp", None), + "date": getattr(replay, "date", None), + "utc_date": getattr(replay, "utc_date", None), + "speed": getattr(replay, "speed", None), + "category": getattr(replay, "category", None), + "type": getattr(replay, "type", None), + "is_ladder": getattr(replay, "is_ladder", False), + "is_private": getattr(replay, "is_private", False), + "filename": getattr(replay, "filename", None), + "file_time": getattr(replay, "file_time", None), + "frames": getattr(replay, "frames", None), + "build": getattr(replay, "build", None), + "release": getattr(replay, "release_string", None), + "game_fps": getattr(replay, "game_fps", None), + "game_length": getattr(getattr(replay, "game_length", None), "seconds", None), + "players": players, + "observers": observers, + "real_length": getattr(getattr(replay, "real_length", None), "seconds", None), + "real_type": getattr(replay, "real_type", None), + "time_zone": getattr(replay, "time_zone", None), + "versions": getattr(replay, "versions", None), } diff --git a/setup.py b/setup.py index d067af22..7efebbad 100644 --- a/setup.py +++ b/setup.py @@ -4,52 +4,49 @@ setuptools.setup( license="MIT", name="sc2reader", - version='1.3.1', + version="1.3.1", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", - long_description=open("README.rst").read()+"\n\n"+open("CHANGELOG.rst").read(), - + long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), author="Kevin Leung", author_email="kkleung89@gmail.com", url="https://github.com/ggtracker/sc2reader", - platforms=["any"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Games/Entertainment", - "Topic :: Games/Entertainment :: Real Time Strategy", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Games/Entertainment", + "Topic :: Games/Entertainment :: Real Time Strategy", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", ], - entry_points={ - 'console_scripts': [ - 'sc2printer = sc2reader.scripts.sc2printer:main', - 'sc2replayer = sc2reader.scripts.sc2replayer:main', - 'sc2parse = sc2reader.scripts.sc2parse:main', - 'sc2attributes = sc2reader.scripts.sc2attributes:main', - 'sc2json = sc2reader.scripts.sc2json:main', + "console_scripts": [ + "sc2printer = sc2reader.scripts.sc2printer:main", + "sc2replayer = sc2reader.scripts.sc2replayer:main", + "sc2parse = sc2reader.scripts.sc2parse:main", + "sc2attributes = sc2reader.scripts.sc2attributes:main", + "sc2json = sc2reader.scripts.sc2json:main", ] }, - - install_requires=['mpyq>=0.2.3', 'argparse', 'ordereddict', 'unittest2', 'pil'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.4'], + install_requires=["mpyq>=0.2.3", "argparse", "ordereddict", "unittest2", "pil"] + if float(sys.version[:3]) < 2.7 + else ["mpyq>=0.2.4"], tests_require=["pytest"], packages=setuptools.find_packages(), include_package_data=True, - zip_safe=True + zip_safe=True, ) diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index f974aaf2..d6d8a6a2 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -79,8 +79,12 @@ def test_standard_1v1(self): self.assertEqual(emperor.result, "Win") self.assertEqual(boom.result, "Loss") - self.assertEqual(emperor.url, "http://eu.battle.net/sc2/en/profile/520049/1/Emperor/") - self.assertEqual(boom.url, "http://eu.battle.net/sc2/en/profile/1694745/1/Boom/") + self.assertEqual( + emperor.url, "http://eu.battle.net/sc2/en/profile/520049/1/Emperor/" + ) + self.assertEqual( + boom.url, "http://eu.battle.net/sc2/en/profile/1694745/1/Boom/" + ) self.assertEqual(len(replay.messages), 12) self.assertEqual(replay.messages[0].text, "hf") @@ -145,16 +149,25 @@ def test_random_player(self): self.assertEqual(gogeta.play_race, "Terran") replay = sc2reader.load_replay("test_replays/1.2.2.17811/6.SC2Replay") - permafrost = next(player for player in replay.players if player.name == "Permafrost") + permafrost = next( + player for player in replay.players if player.name == "Permafrost" + ) self.assertEqual(permafrost.pick_race, "Random") self.assertEqual(permafrost.play_race, "Protoss") def test_us_realm(self): replay = sc2reader.load_replay("test_replays/1.2.2.17811/5.SC2Replay") - shadesofgray = [player for player in replay.players if player.name == "ShadesofGray"][0] + shadesofgray = [ + player for player in replay.players if player.name == "ShadesofGray" + ][0] reddawn = [player for player in replay.players if player.name == "reddawn"][0] - self.assertEqual(shadesofgray.url, "http://us.battle.net/sc2/en/profile/2358439/1/ShadesofGray/") - self.assertEqual(reddawn.url, "http://us.battle.net/sc2/en/profile/2198663/1/reddawn/") + self.assertEqual( + shadesofgray.url, + "http://us.battle.net/sc2/en/profile/2358439/1/ShadesofGray/", + ) + self.assertEqual( + reddawn.url, "http://us.battle.net/sc2/en/profile/2198663/1/reddawn/" + ) def test_kr_realm_and_tampered_messages(self): """ @@ -165,8 +178,12 @@ def test_kr_realm_and_tampered_messages(self): self.assertEqual(replay.expansion, "WoL") first = [player for player in replay.players if player.name == "명지대학교"][0] second = [player for player in replay.players if player.name == "티에스엘사기수"][0] - self.assertEqual(first.url, "http://kr.battle.net/sc2/en/profile/258945/1/명지대학교/") - self.assertEqual(second.url, "http://kr.battle.net/sc2/en/profile/102472/1/티에스엘사기수/") + self.assertEqual( + first.url, "http://kr.battle.net/sc2/en/profile/258945/1/명지대학교/" + ) + self.assertEqual( + second.url, "http://kr.battle.net/sc2/en/profile/102472/1/티에스엘사기수/" + ) self.assertEqual(replay.messages[0].text, "sc2.replays.net") self.assertEqual(replay.messages[5].text, "sc2.replays.net") @@ -205,14 +222,30 @@ def test_hots_pids(self): replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") - player_pids = set([player.pid for player in replay.players if player.is_human]) - ability_pids = set([event.player.pid for event in replay.events if "CommandEvent" in event.name]) + player_pids = set( + [player.pid for player in replay.players if player.is_human] + ) + ability_pids = set( + [ + event.player.pid + for event in replay.events + if "CommandEvent" in event.name + ] + ) self.assertEqual(ability_pids, player_pids) def test_wol_pids(self): - replay = sc2reader.load_replay("test_replays/1.5.4.24540/ggtracker_1471849.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/1.5.4.24540/ggtracker_1471849.SC2Replay" + ) self.assertEqual(replay.expansion, "WoL") - ability_pids = set([event.player.pid for event in replay.events if "CommandEvent" in event.name]) + ability_pids = set( + [ + event.player.pid + for event in replay.events + if "CommandEvent" in event.name + ] + ) player_pids = set([player.pid for player in replay.players]) self.assertEqual(ability_pids, player_pids) @@ -223,40 +256,55 @@ def test_hots_hatchfun(self): [ event.player.pid for event in replay.events - if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" + if "TargetUnitCommandEvent" in event.name + and event.ability.name == "SpawnLarva" ] ) self.assertTrue(spawner_pids.issubset(player_pids)) def test_hots_vs_ai(self): - replay = sc2reader.load_replay("test_replays/2.0.0.24247/Cloud Kingdom LE (13).SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.0.24247/Cloud Kingdom LE (13).SC2Replay" + ) self.assertEqual(replay.expansion, "HotS") - replay = sc2reader.load_replay("test_replays/2.0.0.24247/Korhal City (19).SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.0.24247/Korhal City (19).SC2Replay" + ) self.assertEqual(replay.expansion, "HotS") def test_oracle_parsing(self): - replay = sc2reader.load_replay("test_replays/2.0.3.24764/ggtracker_1571740.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.3.24764/ggtracker_1571740.SC2Replay" + ) self.assertEqual(replay.expansion, "HotS") oracles = [unit for unit in replay.objects.values() if unit.name == "Oracle"] self.assertEqual(len(oracles), 2) def test_resume_from_replay(self): - replay = sc2reader.load_replay("test_replays/2.0.3.24764/resume_from_replay.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.3.24764/resume_from_replay.SC2Replay" + ) self.assertTrue(replay.resume_from_replay) self.assertEqual(replay.resume_method, 0) def test_clan_players(self): - replay = sc2reader.load_replay("test_replays/2.0.4.24944/Lunar Colony V.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.4.24944/Lunar Colony V.SC2Replay" + ) self.assertEqual(replay.expansion, "WoL") self.assertEqual(len(replay.people), 4) def test_WoL_204(self): - replay = sc2reader.load_replay("test_replays/2.0.4.24944/ggtracker_1789768.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.4.24944/ggtracker_1789768.SC2Replay" + ) self.assertEqual(replay.expansion, "WoL") self.assertEqual(len(replay.people), 2) def test_send_resources(self): - replay = sc2reader.load_replay("test_replays/2.0.4.24944/Backwater Complex (15).SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.4.24944/Backwater Complex (15).SC2Replay" + ) def test_cn_replays(self): replay = sc2reader.load_replay("test_replays/2.0.5.25092/cn1.SC2Replay") @@ -266,14 +314,20 @@ def test_cn_replays(self): def test_unit_types(self): """ sc2reader#136 regression test """ replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue136.SC2Replay") - hellion_times = [u.started_at for u in replay.players[0].units if u.name == "Hellion"] - hellbat_times = [u.started_at for u in replay.players[0].units if u.name == "BattleHellion"] + hellion_times = [ + u.started_at for u in replay.players[0].units if u.name == "Hellion" + ] + hellbat_times = [ + u.started_at for u in replay.players[0].units if u.name == "BattleHellion" + ] self.assertEqual(hellion_times, [5180, 5183]) self.assertEqual(hellbat_times, [6736, 6741, 7215, 7220, 12004, 12038]) @unittest.expectedFailure def test_outmatched_pids(self): - replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue131_arid_wastes.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.8.25604/issue131_arid_wastes.SC2Replay" + ) self.assertEqual(replay.players[0].pid, 1) self.assertEqual(replay.players[1].pid, 3) self.assertEqual(replay.players[2].pid, 4) @@ -291,7 +345,9 @@ def test_outmatched_pids(self): @unittest.expectedFailure def test_map_info(self): - replay = sc2reader.load_replay("test_replays/1.5.3.23260/ggtracker_109233.SC2Replay", load_map=True) + replay = sc2reader.load_replay( + "test_replays/1.5.3.23260/ggtracker_109233.SC2Replay", load_map=True + ) self.assertEqual(replay.map.map_info.tile_set, "Avernus") self.assertEqual(replay.map.map_info.fog_type, "Dark") self.assertEqual(replay.map.map_info.width, 176) @@ -308,7 +364,9 @@ def test_engine_plugins(self): replay = sc2reader.load_replay( "test_replays/2.0.5.25092/cn1.SC2Replay", - engine=sc2reader.engine.GameEngine(plugins=[ContextLoader(), APMTracker(), SelectionTracker()]), + engine=sc2reader.engine.GameEngine( + plugins=[ContextLoader(), APMTracker(), SelectionTracker()] + ), ) code, details = replay.plugins["ContextLoader"] @@ -317,7 +375,11 @@ def test_engine_plugins(self): @unittest.expectedFailure def test_factory_plugins(self): - from sc2reader.factories.plugins.replay import APMTracker, SelectionTracker, toJSON + from sc2reader.factories.plugins.replay import ( + APMTracker, + SelectionTracker, + toJSON, + ) factory = sc2reader.factories.SC2Factory() factory.register_plugin("Replay", APMTracker()) @@ -349,7 +411,8 @@ def test_gameheartnormalizer_plugin(self): [ event.player.pid for event in replay.events - if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" + if "TargetUnitCommandEvent" in event.name + and event.ability.name == "SpawnLarva" ] ) self.assertTrue(spawner_pids.issubset(player_pids)) @@ -395,7 +458,9 @@ def test_creepTracker(self): ]: factory = sc2reader.factories.SC2Factory() pluginEngine = sc2reader.engine.GameEngine(plugins=[CreepTracker()]) - replay = factory.load_replay(replayfilename, engine=pluginEngine, load_map=True, load_level=4) + replay = factory.load_replay( + replayfilename, engine=pluginEngine, load_map=True, load_level=4 + ) for player_id in replay.player: if replay.player[player_id].play_race == "Zerg": @@ -405,7 +470,10 @@ def test_creepTracker(self): # print("CSBM", replay.player[player_id].creep_spread_by_minute) replay = factory.load_replay( - "test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay", load_map=True, engine=pluginEngine, load_level=4 + "test_replays/2.0.8.25605/ggtracker_3621402.SC2Replay", + load_map=True, + engine=pluginEngine, + load_level=4, ) assert replay.player[2].max_creep_spread == (840, 24.83) assert replay.player[2].creep_spread_by_minute[420] == 9.4 @@ -413,18 +481,28 @@ def test_creepTracker(self): def test_bad_unit_ids(self): with self.assertRaises(CorruptTrackerFileError): - replay = sc2reader.load_replay("test_replays/2.0.11.26825/bad_unit_ids_1.SC2Replay", load_level=4) + replay = sc2reader.load_replay( + "test_replays/2.0.11.26825/bad_unit_ids_1.SC2Replay", load_level=4 + ) with self.assertRaises(CorruptTrackerFileError): - replay = sc2reader.load_replay("test_replays/2.0.9.26147/bad_unit_ids_2.SC2Replay", load_level=4) + replay = sc2reader.load_replay( + "test_replays/2.0.9.26147/bad_unit_ids_2.SC2Replay", load_level=4 + ) def test_daedalus_point(self): - replay = sc2reader.load_replay("test_replays/2.0.11.26825/DaedalusPoint.SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.0.11.26825/DaedalusPoint.SC2Replay" + ) def test_reloaded(self): - replay = sc2reader.load_replay("test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay") + replay = sc2reader.load_replay( + "test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay" + ) def test_214(self): - replay = sc2reader.load_replay("test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4) + replay = sc2reader.load_replay( + "test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4 + ) def test_lotv1(self): replay = sc2reader.load_replay("test_replays/lotv/lotv1.SC2Replay") @@ -438,7 +516,9 @@ def test_lotv_creepTracker(self): for replayfilename in ["test_replays/4.0.0.59587/1.SC2Replay"]: factory = sc2reader.factories.SC2Factory() pluginEngine = sc2reader.engine.GameEngine(plugins=[CreepTracker()]) - replay = factory.load_replay(replayfilename, engine=pluginEngine, load_map=True) + replay = factory.load_replay( + replayfilename, engine=pluginEngine, load_map=True + ) is_at_least_one_zerg_in_game = False for player_id in replay.player: @@ -469,7 +549,11 @@ def test_30_map(self): replay = factory.load_replay(replayfilename, load_level=1, load_map=True) def test_30_apms(self): - from sc2reader.factories.plugins.replay import APMTracker, SelectionTracker, toJSON + from sc2reader.factories.plugins.replay import ( + APMTracker, + SelectionTracker, + toJSON, + ) factory = sc2reader.factories.SC2Factory() factory.register_plugin("Replay", APMTracker()) @@ -497,10 +581,18 @@ def test_funny_minerals(self): xmldoc = minidom.parseString(replay.map.archive.read_file("Objects")) itemlist = xmldoc.getElementsByTagName("ObjectUnit") mineralPosStrs = [ - ou.attributes["Position"].value for ou in itemlist if "MineralField" in ou.attributes["UnitType"].value + ou.attributes["Position"].value + for ou in itemlist + if "MineralField" in ou.attributes["UnitType"].value ] mineralFieldNames = list( - set([ou.attributes["UnitType"].value for ou in itemlist if "MineralField" in ou.attributes["UnitType"].value]) + set( + [ + ou.attributes["UnitType"].value + for ou in itemlist + if "MineralField" in ou.attributes["UnitType"].value + ] + ) ) # print(mineralFieldNames) self.assertTrue(len(mineralPosStrs) > 0) @@ -515,7 +607,9 @@ def test_32(self): def test_33(self): for replaynum in range(1, 4): - replay = sc2reader.load_replay("test_replays/3.3.0/{}.SC2Replay".format(replaynum)) + replay = sc2reader.load_replay( + "test_replays/3.3.0/{}.SC2Replay".format(replaynum) + ) self.assertTrue(replay is not None) def test_33_shift_click_calldown_mule(self): @@ -635,7 +729,10 @@ def test_game_event_string(self): player.pid = 1 event = GameEvent(16, 1) event.player = player - self.assertEqual("{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), event._str_prefix()) + self.assertEqual( + "{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), + event._str_prefix(), + ) class TestGameEngine(unittest.TestCase): @@ -657,7 +754,9 @@ def handleInitGame(self, event, replay): def handleTestEvent(self, event, replay): if event.value == "d": - yield sc2reader.engine.PluginExit(self, code=1, details=dict(msg="Fail!")) + yield sc2reader.engine.PluginExit( + self, code=1, details=dict(msg="Fail!") + ) else: yield TestGameEngine.TestEvent("d") diff --git a/test_s2gs/test_all.py b/test_s2gs/test_all.py index a0c06b9b..be867add 100644 --- a/test_s2gs/test_all.py +++ b/test_s2gs/test_all.py @@ -2,28 +2,30 @@ # Newer unittest features aren't built in for python 2.6 import sys + if sys.version_info[:2] < (2, 7): import unittest2 as unittest else: import unittest import sc2reader + sc2reader.log_utils.log_to_console("INFO") class TestSummaries(unittest.TestCase): - def test_a_WoL_s2gs(self): summary = sc2reader.load_game_summary("test_s2gs/s2gs1.s2gs") self.assertEqual(summary.players[0].resource_collection_rate, 1276) - self.assertEqual(summary.players[0].build_order[0].order, 'Probe') - self.assertEqual(summary.expansion, 'WoL') + self.assertEqual(summary.players[0].build_order[0].order, "Probe") + self.assertEqual(summary.expansion, "WoL") def test_a_LotV_s2gs(self): summary = sc2reader.load_game_summary("test_s2gs/lotv.s2gs") self.assertEqual(summary.players[0].resource_collection_rate, 1619) - self.assertEqual(summary.players[0].build_order[0].order, 'Probe') - self.assertEqual(summary.expansion, 'HotS') + self.assertEqual(summary.players[0].build_order[0].order, "Probe") + self.assertEqual(summary.expansion, "HotS") + """ def test_another_HotS_s2gs(self): @@ -38,5 +40,5 @@ def test_another_HotS_s2gs(self): self.assertEqual(summary.expansion, 'HotS') """ -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 0cd9831bf470b83d5cadbffe918950aa9b92478f Mon Sep 17 00:00:00 2001 From: Blazej Czapp Date: Mon, 29 Jul 2019 19:45:34 +0100 Subject: [PATCH 019/136] Fix UnitTypeChangeEvent's __str__() --- sc2reader/events/tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 985a9107..47ebaf09 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -482,7 +482,7 @@ def __init__(self, frames, data, build): self.unit_type_name = data[2].decode("utf8") def __str__(self): - return self._str_prefix() + "{0: >15} - Unit {0} type changed to {1}".format( + return self._str_prefix() + "{0: >15} - Unit {1} type changed to {2}".format( str(self.unit.owner), self.unit, self.unit_type_name ) From 23da51dc9fb988a67ecb5b8aaf3fad17cfaf0bb4 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Fri, 9 Aug 2019 18:34:33 -0500 Subject: [PATCH 020/136] add Stetmann commander (#93) * add Stetmann commander * move to alphabetical --- sc2reader/data/attributes.json | 1 + 1 file changed, 1 insertion(+) diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index 773be8c3..e0d541bf 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -710,6 +710,7 @@ "Kerr": "Kerrigan", "Nova": "Nova", "Rayn": "Raynor", + "Stet": "Stetmann", "Stuk": "Stukov", "Swan": "Swann", "Tych": "Tychus", From 0699b2f0f97cff62e0bec7a5121d0ea1541913a0 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Fri, 9 Aug 2019 16:38:02 -0700 Subject: [PATCH 021/136] mark version 1.3.2 --- CHANGELOG.rst | 7 +++++++ setup.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a23a4c3f..83c9398b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ CHANGELOG ============ +1.3.2 - August 9, 2019 +---------------------- +* Allow pytest #84 +* Format code with black #87 +* Fix UnitTypeChangeEvent.__str__ #92 +* Add Stetmann #93 + 1.3.1 - November 29, 2018 ------------------------- * Parse backup if data is missing #69 diff --git a/setup.py b/setup.py index 7efebbad..f8922724 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.3.1", + version="1.3.2", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From b23ce7853ff392ec533490058b9acff3970ec6a6 Mon Sep 17 00:00:00 2001 From: Talv Date: Mon, 19 Aug 2019 18:28:08 +0200 Subject: [PATCH 022/136] Add support for decoding replays from 75689 New field called `trophy_id` was added to `initdata`. Besides that, nothing else was changed in `s2protocol`. https://github.com/Blizzard/s2protocol/blob/3ba4fcbef627b952e183eccd07e51f8184aa7a31/s2protocol/versions/protocol75689.py#L96 --- sc2reader/objects.py | 4 ++++ sc2reader/readers.py | 3 +++ .../4.10.0.75689/trophy_id_13.SC2Replay | Bin 0 -> 48367 bytes test_replays/test_replays.py | 6 ++++++ 4 files changed, 13 insertions(+) create mode 100644 test_replays/4.10.0.75689/trophy_id_13.SC2Replay diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 04dcfc84..a49c3012 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -217,6 +217,10 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): #: >0 or None self.commander_mastery_level = slot_data["commander_mastery_talents"] + #: Trophy ID + #: >0 or None + self.trophy_id = slot_data["trophy_id"] + #: The mastery talents picked for the co-op commander #: list of longs of length 6, each between 0 and 30 self.commander_mastery_talents = slot_data["commander_mastery_talents"] diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 8b24f60e..a61a5934 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -238,6 +238,9 @@ def __call__(self, data, replay): ] if replay.base_build >= 42932 else None, + trophy_id=data.read_uint32() + if replay.base_build >= 75689 + else None, reward_overrides=[ [ data.read_uint32(), diff --git a/test_replays/4.10.0.75689/trophy_id_13.SC2Replay b/test_replays/4.10.0.75689/trophy_id_13.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..050e6307dfbc149805a562b5b24ad936389bd711 GIT binary patch literal 48367 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

9pkLfM`QfcnK419NdFf)7f}t(s{fX@xOiI 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 Date: Thu, 30 Jul 2020 18:50:33 +0200 Subject: [PATCH 061/136] Add balance data for 80949 --- sc2reader/data/LotV/80949_abilities.csv | 409 +++++++++ sc2reader/data/LotV/80949_units.csv | 1058 +++++++++++++++++++++++ sc2reader/data/__init__.py | 1 + sc2reader/data/ability_lookup.csv | 1 + sc2reader/data/unit_lookup.csv | 20 + sc2reader/resources.py | 6 +- 6 files changed, 1494 insertions(+), 1 deletion(-) create mode 100644 sc2reader/data/LotV/80949_abilities.csv create mode 100644 sc2reader/data/LotV/80949_units.csv diff --git a/sc2reader/data/LotV/80949_abilities.csv b/sc2reader/data/LotV/80949_abilities.csv new file mode 100644 index 00000000..8444506e --- /dev/null +++ b/sc2reader/data/LotV/80949_abilities.csv @@ -0,0 +1,409 @@ +40,Taunt +41,stop +43,move +46,attack +62,SprayTerran +63,SprayZerg +64,SprayProtoss +65,SalvageShared +67,GhostHoldFire +68,GhostWeaponsFree +70,Explode +71,FleetBeaconResearch +72,FungalGrowth +73,GuardianShield +74,MULERepair +75,ZerglingTrain +76,NexusTrainMothership +77,Feedback +78,MassRecall +80,HallucinationArchon +81,HallucinationColossus +82,HallucinationHighTemplar +83,HallucinationImmortal +84,HallucinationPhoenix +85,HallucinationProbe +86,HallucinationStalker +87,HallucinationVoidRay +88,HallucinationWarpPrism +89,HallucinationZealot +90,MULEGather +92,CalldownMULE +93,GravitonBeam +97,SpawnChangeling +104,Rally +105,ProgressRally +106,RallyCommand +107,RallyNexus +108,RallyHatchery +109,RoachWarrenResearch +112,NeuralParasite +113,SpawnLarva +114,StimpackMarauder +115,SupplyDrop +119,UltraliskCavernResearch +121,SCVHarvest +122,ProbeHarvest +124,que1 +125,que5 +126,que5CancelToSelection +128,que5Addon +129,BuildInProgress +130,Repair +131,TerranBuild +133,Stimpack +134,GhostCloak +136,MedivacHeal +137,SiegeMode +138,Unsiege +139,BansheeCloak +140,MedivacTransport +141,ScannerSweep +142,Yamato +143,AssaultMode +144,FighterMode +145,BunkerTransport +146,CommandCenterTransport +147,CommandCenterLiftOff +148,CommandCenterLand +149,BarracksFlyingBuild +150,BarracksLiftOff +151,FactoryFlyingBuild +152,FactoryLiftOff +153,StarportFlyingBuild +154,StarportLiftOff +155,FactoryLand +156,StarportLand +157,CommandCenterTrain +158,BarracksLand +159,SupplyDepotLower +160,SupplyDepotRaise +161,BarracksTrain +162,FactoryTrain +163,StarportTrain +164,EngineeringBayResearch +166,GhostAcademyTrain +167,BarracksTechLabResearch +168,FactoryTechLabResearch +169,StarportTechLabResearch +170,GhostAcademyResearch +171,ArmoryResearch +172,ProtossBuild +173,WarpPrismTransport +174,GatewayTrain +175,StargateTrain +176,RoboticsFacilityTrain +177,NexusTrain +178,PsiStorm +179,HangarQueue5 +181,CarrierTrain +182,ForgeResearch +183,RoboticsBayResearch +184,TemplarArchiveResearch +185,ZergBuild +186,DroneHarvest +187,EvolutionChamberResearch +188,UpgradeToLair +189,UpgradeToHive +190,UpgradeToGreaterSpire +191,HiveResearch +192,SpawningPoolResearch +193,HydraliskDenResearch +194,GreaterSpireResearch +195,LarvaTrain +196,MorphToBroodLord +197,BurrowBanelingDown +198,BurrowBanelingUp +199,BurrowDroneDown +200,BurrowDroneUp +201,BurrowHydraliskDown +202,BurrowHydraliskUp +203,BurrowRoachDown +204,BurrowRoachUp +205,BurrowZerglingDown +206,BurrowZerglingUp +207,BurrowInfestorTerranDown +208,BurrowInfestorTerranUp +209,RedstoneLavaCritterBurrow +210,RedstoneLavaCritterInjuredBurrow +211,RedstoneLavaCritterUnburrow +212,RedstoneLavaCritterInjuredUnburrow +213,OverlordTransport +216,WarpGateTrain +217,BurrowQueenDown +218,BurrowQueenUp +219,NydusCanalTransport +220,Blink +221,BurrowInfestorDown +222,BurrowInfestorUp +223,MorphToOverseer +224,UpgradeToPlanetaryFortress +225,InfestationPitResearch +226,BanelingNestResearch +227,BurrowUltraliskDown +228,BurrowUltraliskUp +229,UpgradeToOrbital +230,UpgradeToWarpGate +231,MorphBackToGateway +232,OrbitalLiftOff +233,OrbitalCommandLand +234,ForceField +235,PhasingMode +236,TransportMode +237,FusionCoreResearch +238,CyberneticsCoreResearch +239,TwilightCouncilResearch +240,TacNukeStrike +243,EMP +245,HiveTrain +247,Transfusion +256,AttackRedirect +257,StimpackRedirect +258,StimpackMarauderRedirect +260,StopRedirect +261,GenerateCreep +262,QueenBuild +263,SpineCrawlerUproot +264,SporeCrawlerUproot +265,SpineCrawlerRoot +266,SporeCrawlerRoot +267,CreepTumorBurrowedBuild +268,BuildAutoTurret +269,ArchonWarp +270,NydusNetworkBuild +272,Charge +276,Contaminate +279,que5Passive +280,que5PassiveCancelToSelection +283,RavagerCorrosiveBile +305,BurrowLurkerMPDown +306,BurrowLurkerMPUp +309,BurrowRavagerDown +310,BurrowRavagerUp +311,MorphToRavager +312,MorphToTransportOverlord +314,ThorNormalMode +319,DigesterCreepSpray +323,MorphToMothership +348,XelNagaHealingShrine +357,MothershipCoreMassRecall +359,MorphToHellion +369,MorphToHellionTank +377,MorphToSwarmHostBurrowedMP +378,MorphToSwarmHostMP +383,BlindingCloud +385,Yoink +388,ViperConsumeStructure +391,TestZerg +392,VolatileBurstBuilding +399,WidowMineBurrow +400,WidowMineUnburrow +401,WidowMineAttack +402,TornadoMissile +405,HallucinationOracle +406,MedivacSpeedBoost +407,ExtendingBridgeNEWide8Out +408,ExtendingBridgeNEWide8 +409,ExtendingBridgeNWWide8Out +410,ExtendingBridgeNWWide8 +411,ExtendingBridgeNEWide10Out +412,ExtendingBridgeNEWide10 +413,ExtendingBridgeNWWide10Out +414,ExtendingBridgeNWWide10 +415,ExtendingBridgeNEWide12Out +416,ExtendingBridgeNEWide12 +417,ExtendingBridgeNWWide12Out +418,ExtendingBridgeNWWide12 +420,CritterFlee +421,OracleRevelation +429,MothershipCorePurifyNexus +430,XelNaga_Caverns_DoorE +431,XelNaga_Caverns_DoorEOpened +432,XelNaga_Caverns_DoorN +433,XelNaga_Caverns_DoorNE +434,XelNaga_Caverns_DoorNEOpened +435,XelNaga_Caverns_DoorNOpened +436,XelNaga_Caverns_DoorNW +437,XelNaga_Caverns_DoorNWOpened +438,XelNaga_Caverns_DoorS +439,XelNaga_Caverns_DoorSE +440,XelNaga_Caverns_DoorSEOpened +441,XelNaga_Caverns_DoorSOpened +442,XelNaga_Caverns_DoorSW +443,XelNaga_Caverns_DoorSWOpened +444,XelNaga_Caverns_DoorW +445,XelNaga_Caverns_DoorWOpened +446,XelNaga_Caverns_Floating_BridgeNE8Out +447,XelNaga_Caverns_Floating_BridgeNE8 +448,XelNaga_Caverns_Floating_BridgeNW8Out +449,XelNaga_Caverns_Floating_BridgeNW8 +450,XelNaga_Caverns_Floating_BridgeNE10Out +451,XelNaga_Caverns_Floating_BridgeNE10 +452,XelNaga_Caverns_Floating_BridgeNW10Out +453,XelNaga_Caverns_Floating_BridgeNW10 +454,XelNaga_Caverns_Floating_BridgeNE12Out +455,XelNaga_Caverns_Floating_BridgeNE12 +456,XelNaga_Caverns_Floating_BridgeNW12Out +457,XelNaga_Caverns_Floating_BridgeNW12 +458,XelNaga_Caverns_Floating_BridgeH8Out +459,XelNaga_Caverns_Floating_BridgeH8 +460,XelNaga_Caverns_Floating_BridgeV8Out +461,XelNaga_Caverns_Floating_BridgeV8 +462,XelNaga_Caverns_Floating_BridgeH10Out +463,XelNaga_Caverns_Floating_BridgeH10 +464,XelNaga_Caverns_Floating_BridgeV10Out +465,XelNaga_Caverns_Floating_BridgeV10 +466,XelNaga_Caverns_Floating_BridgeH12Out +467,XelNaga_Caverns_Floating_BridgeH12 +468,XelNaga_Caverns_Floating_BridgeV12Out +469,XelNaga_Caverns_Floating_BridgeV12 +470,TemporalField +496,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +497,SnowRefinery_Terran_ExtendingBridgeNEShort8 +498,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +499,SnowRefinery_Terran_ExtendingBridgeNWShort8 +521,CausticSpray +524,MorphToLurker +528,PurificationNovaTargeted +530,LockOn +532,LockOnCancel +534,Hyperjump +536,ThorAPMode +539,NydusWormTransport +540,OracleWeapon +546,LocustMPFlyingSwoop +547,HallucinationDisruptor +548,HallucinationAdept +549,VoidRaySwarmDamageBoost +550,SeekerDummyChannel +551,AiurLightBridgeNE8Out +552,AiurLightBridgeNE8 +553,AiurLightBridgeNE10Out +554,AiurLightBridgeNE10 +555,AiurLightBridgeNE12Out +556,AiurLightBridgeNE12 +557,AiurLightBridgeNW8Out +558,AiurLightBridgeNW8 +559,AiurLightBridgeNW10Out +560,AiurLightBridgeNW10 +561,AiurLightBridgeNW12Out +562,AiurLightBridgeNW12 +575,ShakurasLightBridgeNE8Out +576,ShakurasLightBridgeNE8 +577,ShakurasLightBridgeNE10Out +578,ShakurasLightBridgeNE10 +579,ShakurasLightBridgeNE12Out +580,ShakurasLightBridgeNE12 +581,ShakurasLightBridgeNW8Out +582,ShakurasLightBridgeNW8 +583,ShakurasLightBridgeNW10Out +584,ShakurasLightBridgeNW10 +585,ShakurasLightBridgeNW12Out +586,ShakurasLightBridgeNW12 +587,VoidMPImmortalReviveRebuild +589,ArbiterMPStasisField +590,ArbiterMPRecall +591,CorsairMPDisruptionWeb +592,MorphToGuardianMP +593,MorphToDevourerMP +594,DefilerMPConsume +595,DefilerMPDarkSwarm +596,DefilerMPPlague +597,DefilerMPBurrow +598,DefilerMPUnburrow +599,QueenMPEnsnare +600,QueenMPSpawnBroodlings +601,QueenMPInfestCommandCenter +605,OracleBuild +609,ParasiticBomb +610,AdeptPhaseShift +613,LurkerHoldFire +614,LurkerRemoveHoldFire +617,LiberatorAGTarget +618,LiberatorAATarget +620,AiurLightBridgeAbandonedNE8Out +621,AiurLightBridgeAbandonedNE8 +622,AiurLightBridgeAbandonedNE10Out +623,AiurLightBridgeAbandonedNE10 +624,AiurLightBridgeAbandonedNE12Out +625,AiurLightBridgeAbandonedNE12 +626,AiurLightBridgeAbandonedNW8Out +627,AiurLightBridgeAbandonedNW8 +628,AiurLightBridgeAbandonedNW10Out +629,AiurLightBridgeAbandonedNW10 +630,AiurLightBridgeAbandonedNW12Out +631,AiurLightBridgeAbandonedNW12 +632,KD8Charge +635,AdeptPhaseShiftCancel +636,AdeptShadePhaseShiftCancel +637,SlaynElementalGrab +639,PortCity_Bridge_UnitNE8Out +640,PortCity_Bridge_UnitNE8 +641,PortCity_Bridge_UnitSE8Out +642,PortCity_Bridge_UnitSE8 +643,PortCity_Bridge_UnitNW8Out +644,PortCity_Bridge_UnitNW8 +645,PortCity_Bridge_UnitSW8Out +646,PortCity_Bridge_UnitSW8 +647,PortCity_Bridge_UnitNE10Out +648,PortCity_Bridge_UnitNE10 +649,PortCity_Bridge_UnitSE10Out +650,PortCity_Bridge_UnitSE10 +651,PortCity_Bridge_UnitNW10Out +652,PortCity_Bridge_UnitNW10 +653,PortCity_Bridge_UnitSW10Out +654,PortCity_Bridge_UnitSW10 +655,PortCity_Bridge_UnitNE12Out +656,PortCity_Bridge_UnitNE12 +657,PortCity_Bridge_UnitSE12Out +658,PortCity_Bridge_UnitSE12 +659,PortCity_Bridge_UnitNW12Out +660,PortCity_Bridge_UnitNW12 +661,PortCity_Bridge_UnitSW12Out +662,PortCity_Bridge_UnitSW12 +663,PortCity_Bridge_UnitN8Out +664,PortCity_Bridge_UnitN8 +665,PortCity_Bridge_UnitS8Out +666,PortCity_Bridge_UnitS8 +667,PortCity_Bridge_UnitE8Out +668,PortCity_Bridge_UnitE8 +669,PortCity_Bridge_UnitW8Out +670,PortCity_Bridge_UnitW8 +671,PortCity_Bridge_UnitN10Out +672,PortCity_Bridge_UnitN10 +673,PortCity_Bridge_UnitS10Out +674,PortCity_Bridge_UnitS10 +675,PortCity_Bridge_UnitE10Out +676,PortCity_Bridge_UnitE10 +677,PortCity_Bridge_UnitW10Out +678,PortCity_Bridge_UnitW10 +679,PortCity_Bridge_UnitN12Out +680,PortCity_Bridge_UnitN12 +681,PortCity_Bridge_UnitS12Out +682,PortCity_Bridge_UnitS12 +683,PortCity_Bridge_UnitE12Out +684,PortCity_Bridge_UnitE12 +685,PortCity_Bridge_UnitW12Out +686,PortCity_Bridge_UnitW12 +689,DarkTemplarBlink +693,BattlecruiserAttack +695,BattlecruiserMove +697,BattlecruiserStop +698,BatteryOvercharge +700,AmorphousArmorcloud +702,SpawnLocustsTargeted +703,ViperParasiticBombRelay +704,ParasiticBombRelayDodge +705,VoidRaySwarmDamageBoostCancel +709,ChannelSnipe +712,DarkShrineResearch +713,LurkerDenMPResearch +714,ObserverSiegeMorphtoObserver +715,ObserverMorphtoObserverSiege +716,OverseerMorphtoOverseerSiegeMode +717,OverseerSiegeModeMorphtoOverseer +718,RavenScramblerMissile +720,RavenRepairDroneHeal +721,RavenShredderMissile +722,ChronoBoostEnergyCost +723,NexusMassRecall diff --git a/sc2reader/data/LotV/80949_units.csv b/sc2reader/data/LotV/80949_units.csv new file mode 100644 index 00000000..19aa551f --- /dev/null +++ b/sc2reader/data/LotV/80949_units.csv @@ -0,0 +1,1058 @@ +3,System_Snapshot_Dummy +21,Ball +22,StereoscopicOptionsUnit +23,Colossus +24,TechLab +25,Reactor +27,InfestorTerran +28,BanelingCocoon +29,Baneling +30,Mothership +31,PointDefenseDrone +32,Changeling +33,ChangelingZealot +34,ChangelingMarineShield +35,ChangelingMarine +36,ChangelingZerglingWings +37,ChangelingZergling +39,CommandCenter +40,SupplyDepot +41,Refinery +42,Barracks +43,EngineeringBay +44,MissileTurret +45,Bunker +46,RefineryRich +47,SensorTower +48,GhostAcademy +49,Factory +50,Starport +52,Armory +53,FusionCore +54,AutoTurret +55,SiegeTankSieged +56,SiegeTank +57,VikingAssault +58,VikingFighter +59,CommandCenterFlying +60,BarracksTechLab +61,BarracksReactor +62,FactoryTechLab +63,FactoryReactor +64,StarportTechLab +65,StarportReactor +66,FactoryFlying +67,StarportFlying +68,SCV +69,BarracksFlying +70,SupplyDepotLowered +71,Marine +72,Reaper +73,Ghost +74,Marauder +75,Thor +76,Hellion +77,Medivac +78,Banshee +79,Raven +80,Battlecruiser +81,Nuke +82,Nexus +83,Pylon +84,Assimilator +85,Gateway +86,Forge +87,FleetBeacon +88,TwilightCouncil +89,PhotonCannon +90,Stargate +91,TemplarArchive +92,DarkShrine +93,RoboticsBay +94,RoboticsFacility +95,CyberneticsCore +96,Zealot +97,Stalker +98,HighTemplar +99,DarkTemplar +100,Sentry +101,Phoenix +102,Carrier +103,VoidRay +104,WarpPrism +105,Observer +106,Immortal +107,Probe +108,Interceptor +109,Hatchery +110,CreepTumor +111,Extractor +112,SpawningPool +113,EvolutionChamber +114,HydraliskDen +115,Spire +116,UltraliskCavern +117,InfestationPit +118,NydusNetwork +119,BanelingNest +120,RoachWarren +121,SpineCrawler +122,SporeCrawler +123,Lair +124,Hive +125,GreaterSpire +126,Egg +127,Drone +128,Zergling +129,Overlord +130,Hydralisk +131,Mutalisk +132,Ultralisk +133,Roach +134,Infestor +135,Corruptor +136,BroodLordCocoon +137,BroodLord +138,BanelingBurrowed +139,DroneBurrowed +140,HydraliskBurrowed +141,RoachBurrowed +142,ZerglingBurrowed +143,InfestorTerranBurrowed +144,RedstoneLavaCritterBurrowed +145,RedstoneLavaCritterInjuredBurrowed +146,RedstoneLavaCritter +147,RedstoneLavaCritterInjured +148,QueenBurrowed +149,Queen +150,InfestorBurrowed +151,OverlordCocoon +152,Overseer +153,PlanetaryFortress +154,UltraliskBurrowed +155,OrbitalCommand +156,WarpGate +157,OrbitalCommandFlying +158,ForceField +159,WarpPrismPhasing +160,CreepTumorBurrowed +161,CreepTumorQueen +162,SpineCrawlerUprooted +163,SporeCrawlerUprooted +164,Archon +165,NydusCanal +166,BroodlingEscort +167,GhostAlternate +168,GhostNova +169,RichMineralField +170,RichMineralField750 +171,Ursadon +173,LurkerMPBurrowed +174,LurkerMP +175,LurkerDenMP +176,LurkerMPEgg +177,NydusCanalAttacker +178,OverlordTransport +179,Ravager +180,RavagerBurrowed +181,RavagerCocoon +182,TransportOverlordCocoon +183,XelNagaTower +185,Oracle +186,Tempest +188,InfestedTerransEgg +189,Larva +190,OverseerSiegeMode +192,ReaperPlaceholder +193,MarineACGluescreenDummy +194,FirebatACGluescreenDummy +195,MedicACGluescreenDummy +196,MarauderACGluescreenDummy +197,VultureACGluescreenDummy +198,SiegeTankACGluescreenDummy +199,VikingACGluescreenDummy +200,BansheeACGluescreenDummy +201,BattlecruiserACGluescreenDummy +202,OrbitalCommandACGluescreenDummy +203,BunkerACGluescreenDummy +204,BunkerUpgradedACGluescreenDummy +205,MissileTurretACGluescreenDummy +206,HellbatACGluescreenDummy +207,GoliathACGluescreenDummy +208,CycloneACGluescreenDummy +209,WraithACGluescreenDummy +210,ScienceVesselACGluescreenDummy +211,HerculesACGluescreenDummy +212,ThorACGluescreenDummy +213,PerditionTurretACGluescreenDummy +214,FlamingBettyACGluescreenDummy +215,DevastationTurretACGluescreenDummy +216,BlasterBillyACGluescreenDummy +217,SpinningDizzyACGluescreenDummy +218,ZerglingKerriganACGluescreenDummy +219,RaptorACGluescreenDummy +220,QueenCoopACGluescreenDummy +221,HydraliskACGluescreenDummy +222,HydraliskLurkerACGluescreenDummy +223,MutaliskBroodlordACGluescreenDummy +224,BroodLordACGluescreenDummy +225,UltraliskACGluescreenDummy +226,TorrasqueACGluescreenDummy +227,OverseerACGluescreenDummy +228,LurkerACGluescreenDummy +229,SpineCrawlerACGluescreenDummy +230,SporeCrawlerACGluescreenDummy +231,NydusNetworkACGluescreenDummy +232,OmegaNetworkACGluescreenDummy +233,ZerglingZagaraACGluescreenDummy +234,SwarmlingACGluescreenDummy +235,QueenZagaraACGluescreenDummy +236,BanelingACGluescreenDummy +237,SplitterlingACGluescreenDummy +238,AberrationACGluescreenDummy +239,ScourgeACGluescreenDummy +240,CorruptorACGluescreenDummy +241,OverseerZagaraACGluescreenDummy +242,BileLauncherACGluescreenDummy +243,SwarmQueenACGluescreenDummy +244,RoachACGluescreenDummy +245,RoachVileACGluescreenDummy +246,RavagerACGluescreenDummy +247,SwarmHostACGluescreenDummy +248,MutaliskACGluescreenDummy +249,GuardianACGluescreenDummy +250,DevourerACGluescreenDummy +251,ViperACGluescreenDummy +252,BrutaliskACGluescreenDummy +253,LeviathanACGluescreenDummy +254,ZealotACGluescreenDummy +255,ZealotAiurACGluescreenDummy +256,DragoonACGluescreenDummy +257,HighTemplarACGluescreenDummy +258,ArchonACGluescreenDummy +259,ImmortalACGluescreenDummy +260,ObserverACGluescreenDummy +261,PhoenixAiurACGluescreenDummy +262,ReaverACGluescreenDummy +263,TempestACGluescreenDummy +264,PhotonCannonACGluescreenDummy +265,ZealotVorazunACGluescreenDummy +266,ZealotShakurasACGluescreenDummy +267,StalkerShakurasACGluescreenDummy +268,DarkTemplarShakurasACGluescreenDummy +269,CorsairACGluescreenDummy +270,VoidRayACGluescreenDummy +271,VoidRayShakurasACGluescreenDummy +272,OracleACGluescreenDummy +273,DarkArchonACGluescreenDummy +274,DarkPylonACGluescreenDummy +275,ZealotPurifierACGluescreenDummy +276,SentryPurifierACGluescreenDummy +277,ImmortalKaraxACGluescreenDummy +278,ColossusACGluescreenDummy +279,ColossusPurifierACGluescreenDummy +280,PhoenixPurifierACGluescreenDummy +281,CarrierACGluescreenDummy +282,CarrierAiurACGluescreenDummy +283,KhaydarinMonolithACGluescreenDummy +284,ShieldBatteryACGluescreenDummy +285,EliteMarineACGluescreenDummy +286,MarauderCommandoACGluescreenDummy +287,SpecOpsGhostACGluescreenDummy +288,HellbatRangerACGluescreenDummy +289,StrikeGoliathACGluescreenDummy +290,HeavySiegeTankACGluescreenDummy +291,RaidLiberatorACGluescreenDummy +292,RavenTypeIIACGluescreenDummy +293,CovertBansheeACGluescreenDummy +294,RailgunTurretACGluescreenDummy +295,BlackOpsMissileTurretACGluescreenDummy +296,SupplicantACGluescreenDummy +297,StalkerTaldarimACGluescreenDummy +298,SentryTaldarimACGluescreenDummy +299,HighTemplarTaldarimACGluescreenDummy +300,ImmortalTaldarimACGluescreenDummy +301,ColossusTaldarimACGluescreenDummy +302,WarpPrismTaldarimACGluescreenDummy +303,PhotonCannonTaldarimACGluescreenDummy +304,StukovInfestedCivilianACGluescreenDummy +305,StukovInfestedMarineACGluescreenDummy +306,StukovInfestedSiegeTankACGluescreenDummy +307,StukovInfestedDiamondbackACGluescreenDummy +308,StukovInfestedBansheeACGluescreenDummy +309,SILiberatorACGluescreenDummy +310,StukovInfestedBunkerACGluescreenDummy +311,StukovInfestedMissileTurretACGluescreenDummy +312,StukovBroodQueenACGluescreenDummy +313,ZealotFenixACGluescreenDummy +314,SentryFenixACGluescreenDummy +315,AdeptFenixACGluescreenDummy +316,ImmortalFenixACGluescreenDummy +317,ColossusFenixACGluescreenDummy +318,DisruptorACGluescreenDummy +319,ObserverFenixACGluescreenDummy +320,ScoutACGluescreenDummy +321,CarrierFenixACGluescreenDummy +322,PhotonCannonFenixACGluescreenDummy +323,PrimalZerglingACGluescreenDummy +324,RavasaurACGluescreenDummy +325,PrimalRoachACGluescreenDummy +326,FireRoachACGluescreenDummy +327,PrimalGuardianACGluescreenDummy +328,PrimalHydraliskACGluescreenDummy +329,PrimalMutaliskACGluescreenDummy +330,PrimalImpalerACGluescreenDummy +331,PrimalSwarmHostACGluescreenDummy +332,CreeperHostACGluescreenDummy +333,PrimalUltraliskACGluescreenDummy +334,TyrannozorACGluescreenDummy +335,PrimalWurmACGluescreenDummy +336,HHReaperACGluescreenDummy +337,HHWidowMineACGluescreenDummy +338,HHHellionTankACGluescreenDummy +339,HHWraithACGluescreenDummy +340,HHVikingACGluescreenDummy +341,HHBattlecruiserACGluescreenDummy +342,HHRavenACGluescreenDummy +343,HHBomberPlatformACGluescreenDummy +344,HHMercStarportACGluescreenDummy +345,HHMissileTurretACGluescreenDummy +346,TychusReaperACGluescreenDummy +347,TychusFirebatACGluescreenDummy +348,TychusSpectreACGluescreenDummy +349,TychusMedicACGluescreenDummy +350,TychusMarauderACGluescreenDummy +351,TychusWarhoundACGluescreenDummy +352,TychusHERCACGluescreenDummy +353,TychusGhostACGluescreenDummy +354,TychusSCVAutoTurretACGluescreenDummy +355,ZeratulStalkerACGluescreenDummy +356,ZeratulSentryACGluescreenDummy +357,ZeratulDarkTemplarACGluescreenDummy +358,ZeratulImmortalACGluescreenDummy +359,ZeratulObserverACGluescreenDummy +360,ZeratulDisruptorACGluescreenDummy +361,ZeratulWarpPrismACGluescreenDummy +362,ZeratulPhotonCannonACGluescreenDummy +363,MechaZerglingACGluescreenDummy +364,MechaBanelingACGluescreenDummy +365,MechaHydraliskACGluescreenDummy +366,MechaInfestorACGluescreenDummy +367,MechaCorruptorACGluescreenDummy +368,MechaUltraliskACGluescreenDummy +369,MechaOverseerACGluescreenDummy +370,MechaLurkerACGluescreenDummy +371,MechaBattlecarrierLordACGluescreenDummy +372,MechaSpineCrawlerACGluescreenDummy +373,MechaSporeCrawlerACGluescreenDummy +374,TrooperMengskACGluescreenDummy +375,MedivacMengskACGluescreenDummy +376,BlimpMengskACGluescreenDummy +377,MarauderMengskACGluescreenDummy +378,GhostMengskACGluescreenDummy +379,SiegeTankMengskACGluescreenDummy +380,ThorMengskACGluescreenDummy +381,VikingMengskACGluescreenDummy +382,BattlecruiserMengskACGluescreenDummy +383,BunkerDepotMengskACGluescreenDummy +384,MissileTurretMengskACGluescreenDummy +385,ArtilleryMengskACGluescreenDummy +387,RenegadeLongboltMissileWeapon +388,LoadOutSpray@1 +389,LoadOutSpray@2 +390,LoadOutSpray@3 +391,LoadOutSpray@4 +392,LoadOutSpray@5 +393,LoadOutSpray@6 +394,LoadOutSpray@7 +395,LoadOutSpray@8 +396,LoadOutSpray@9 +397,LoadOutSpray@10 +398,LoadOutSpray@11 +399,LoadOutSpray@12 +400,LoadOutSpray@13 +401,LoadOutSpray@14 +402,NeedleSpinesWeapon +403,CorruptionWeapon +404,InfestedTerransWeapon +405,NeuralParasiteWeapon +406,PointDefenseDroneReleaseWeapon +407,HunterSeekerWeapon +408,MULE +410,ThorAAWeapon +411,PunisherGrenadesLMWeapon +412,VikingFighterWeapon +413,ATALaserBatteryLMWeapon +414,ATSLaserBatteryLMWeapon +415,LongboltMissileWeapon +416,D8ChargeWeapon +417,YamatoWeapon +418,IonCannonsWeapon +419,AcidSalivaWeapon +420,SpineCrawlerWeapon +421,SporeCrawlerWeapon +422,GlaiveWurmWeapon +423,GlaiveWurmM2Weapon +424,GlaiveWurmM3Weapon +425,StalkerWeapon +426,EMP2Weapon +427,BacklashRocketsLMWeapon +428,PhotonCannonWeapon +429,ParasiteSporeWeapon +431,Broodling +432,BroodLordBWeapon +435,AutoTurretReleaseWeapon +436,LarvaReleaseMissile +437,AcidSpinesWeapon +438,FrenzyWeapon +439,ContaminateWeapon +451,BeaconArmy +452,BeaconDefend +453,BeaconAttack +454,BeaconHarass +455,BeaconIdle +456,BeaconAuto +457,BeaconDetect +458,BeaconScout +459,BeaconClaim +460,BeaconExpand +461,BeaconRally +462,BeaconCustom1 +463,BeaconCustom2 +464,BeaconCustom3 +465,BeaconCustom4 +470,LiberatorAG +472,PreviewBunkerUpgraded +473,HellionTank +474,Cyclone +475,WidowMine +476,Liberator +478,Adept +479,Disruptor +480,SwarmHostMP +481,Viper +482,ShieldBattery +483,HighTemplarSkinPreview +484,MothershipCore +485,Viking +498,InhibitorZoneSmall +499,InhibitorZoneMedium +500,InhibitorZoneLarge +501,AccelerationZoneSmall +502,AccelerationZoneMedium +503,AccelerationZoneLarge +504,AccelerationZoneFlyingSmall +505,AccelerationZoneFlyingMedium +506,AccelerationZoneFlyingLarge +507,InhibitorZoneFlyingSmall +508,InhibitorZoneFlyingMedium +509,InhibitorZoneFlyingLarge +510,AssimilatorRich +511,RichVespeneGeyser +512,ExtractorRich +513,RavagerCorrosiveBileMissile +514,RavagerWeaponMissile +515,RenegadeMissileTurret +516,Rocks2x2NonConjoined +517,FungalGrowthMissile +518,NeuralParasiteTentacleMissile +519,Beacon_Protoss +520,Beacon_ProtossSmall +521,Beacon_Terran +522,Beacon_TerranSmall +523,Beacon_Zerg +524,Beacon_ZergSmall +525,Lyote +526,CarrionBird +527,KarakMale +528,KarakFemale +529,UrsadakFemaleExotic +530,UrsadakMale +531,UrsadakFemale +532,UrsadakCalf +533,UrsadakMaleExotic +534,UtilityBot +535,CommentatorBot1 +536,CommentatorBot2 +537,CommentatorBot3 +538,CommentatorBot4 +539,Scantipede +540,Dog +541,Sheep +542,Cow +543,InfestedTerransEggPlacement +544,InfestorTerransWeapon +545,MineralField +546,MineralField450 +547,MineralField750 +548,MineralFieldOpaque +549,MineralFieldOpaque900 +550,VespeneGeyser +551,SpacePlatformGeyser +552,DestructibleSearchlight +553,DestructibleBullhornLights +554,DestructibleStreetlight +555,DestructibleSpacePlatformSign +556,DestructibleStoreFrontCityProps +557,DestructibleBillboardTall +558,DestructibleBillboardScrollingText +559,DestructibleSpacePlatformBarrier +560,DestructibleSignsDirectional +561,DestructibleSignsConstruction +562,DestructibleSignsFunny +563,DestructibleSignsIcons +564,DestructibleSignsWarning +565,DestructibleGarage +566,DestructibleGarageLarge +567,DestructibleTrafficSignal +568,TrafficSignal +569,BraxisAlphaDestructible1x1 +570,BraxisAlphaDestructible2x2 +571,DestructibleDebris4x4 +572,DestructibleDebris6x6 +573,DestructibleRock2x4Vertical +574,DestructibleRock2x4Horizontal +575,DestructibleRock2x6Vertical +576,DestructibleRock2x6Horizontal +577,DestructibleRock4x4 +578,DestructibleRock6x6 +579,DestructibleRampDiagonalHugeULBR +580,DestructibleRampDiagonalHugeBLUR +581,DestructibleRampVerticalHuge +582,DestructibleRampHorizontalHuge +583,DestructibleDebrisRampDiagonalHugeULBR +584,DestructibleDebrisRampDiagonalHugeBLUR +585,WarpPrismSkinPreview +586,SiegeTankSkinPreview +587,ThorAP +588,ThorAALance +589,LiberatorSkinPreview +590,OverlordGenerateCreepKeybind +591,MengskStatueAlone +592,MengskStatue +593,WolfStatue +594,GlobeStatue +595,Weapon +596,GlaiveWurmBounceWeapon +597,BroodLordWeapon +598,BroodLordAWeapon +599,CreepBlocker1x1 +600,PermanentCreepBlocker1x1 +601,PathingBlocker1x1 +602,PathingBlocker2x2 +603,AutoTestAttackTargetGround +604,AutoTestAttackTargetAir +605,AutoTestAttacker +606,HelperEmitterSelectionArrow +607,MultiKillObject +608,ShapeGolfball +609,ShapeCone +610,ShapeCube +611,ShapeCylinder +612,ShapeDodecahedron +613,ShapeIcosahedron +614,ShapeOctahedron +615,ShapePyramid +616,ShapeRoundedCube +617,ShapeSphere +618,ShapeTetrahedron +619,ShapeThickTorus +620,ShapeThinTorus +621,ShapeTorus +622,Shape4PointStar +623,Shape5PointStar +624,Shape6PointStar +625,Shape8PointStar +626,ShapeArrowPointer +627,ShapeBowl +628,ShapeBox +629,ShapeCapsule +630,ShapeCrescentMoon +631,ShapeDecahedron +632,ShapeDiamond +633,ShapeFootball +634,ShapeGemstone +635,ShapeHeart +636,ShapeJack +637,ShapePlusSign +638,ShapeShamrock +639,ShapeSpade +640,ShapeTube +641,ShapeEgg +642,ShapeYenSign +643,ShapeX +644,ShapeWatermelon +645,ShapeWonSign +646,ShapeTennisball +647,ShapeStrawberry +648,ShapeSmileyFace +649,ShapeSoccerball +650,ShapeRainbow +651,ShapeSadFace +652,ShapePoundSign +653,ShapePear +654,ShapePineapple +655,ShapeOrange +656,ShapePeanut +657,ShapeO +658,ShapeLemon +659,ShapeMoneyBag +660,ShapeHorseshoe +661,ShapeHockeyStick +662,ShapeHockeyPuck +663,ShapeHand +664,ShapeGolfClub +665,ShapeGrape +666,ShapeEuroSign +667,ShapeDollarSign +668,ShapeBasketball +669,ShapeCarrot +670,ShapeCherry +671,ShapeBaseball +672,ShapeBaseballBat +673,ShapeBanana +674,ShapeApple +675,ShapeCashLarge +676,ShapeCashMedium +677,ShapeCashSmall +678,ShapeFootballColored +679,ShapeLemonSmall +680,ShapeOrangeSmall +681,ShapeTreasureChestOpen +682,ShapeTreasureChestClosed +683,ShapeWatermelonSmall +684,UnbuildableRocksDestructible +685,UnbuildableBricksDestructible +686,UnbuildablePlatesDestructible +687,Debris2x2NonConjoined +688,EnemyPathingBlocker1x1 +689,EnemyPathingBlocker2x2 +690,EnemyPathingBlocker4x4 +691,EnemyPathingBlocker8x8 +692,EnemyPathingBlocker16x16 +693,ScopeTest +694,SentryACGluescreenDummy +695,StukovInfestedTrooperACGluescreenDummy +711,CollapsibleTerranTowerDebris +712,DebrisRampLeft +713,DebrisRampRight +717,LocustMP +718,CollapsibleRockTowerDebris +719,NydusCanalCreeper +720,SwarmHostBurrowedMP +721,WarHound +722,WidowMineBurrowed +723,ExtendingBridgeNEWide8Out +724,ExtendingBridgeNEWide8 +725,ExtendingBridgeNWWide8Out +726,ExtendingBridgeNWWide8 +727,ExtendingBridgeNEWide10Out +728,ExtendingBridgeNEWide10 +729,ExtendingBridgeNWWide10Out +730,ExtendingBridgeNWWide10 +731,ExtendingBridgeNEWide12Out +732,ExtendingBridgeNEWide12 +733,ExtendingBridgeNWWide12Out +734,ExtendingBridgeNWWide12 +736,CollapsibleRockTowerDebrisRampRight +737,CollapsibleRockTowerDebrisRampLeft +738,XelNaga_Caverns_DoorE +739,XelNaga_Caverns_DoorEOpened +740,XelNaga_Caverns_DoorN +741,XelNaga_Caverns_DoorNE +742,XelNaga_Caverns_DoorNEOpened +743,XelNaga_Caverns_DoorNOpened +744,XelNaga_Caverns_DoorNW +745,XelNaga_Caverns_DoorNWOpened +746,XelNaga_Caverns_DoorS +747,XelNaga_Caverns_DoorSE +748,XelNaga_Caverns_DoorSEOpened +749,XelNaga_Caverns_DoorSOpened +750,XelNaga_Caverns_DoorSW +751,XelNaga_Caverns_DoorSWOpened +752,XelNaga_Caverns_DoorW +753,XelNaga_Caverns_DoorWOpened +754,XelNaga_Caverns_Floating_BridgeNE8Out +755,XelNaga_Caverns_Floating_BridgeNE8 +756,XelNaga_Caverns_Floating_BridgeNW8Out +757,XelNaga_Caverns_Floating_BridgeNW8 +758,XelNaga_Caverns_Floating_BridgeNE10Out +759,XelNaga_Caverns_Floating_BridgeNE10 +760,XelNaga_Caverns_Floating_BridgeNW10Out +761,XelNaga_Caverns_Floating_BridgeNW10 +762,XelNaga_Caverns_Floating_BridgeNE12Out +763,XelNaga_Caverns_Floating_BridgeNE12 +764,XelNaga_Caverns_Floating_BridgeNW12Out +765,XelNaga_Caverns_Floating_BridgeNW12 +766,XelNaga_Caverns_Floating_BridgeH8Out +767,XelNaga_Caverns_Floating_BridgeH8 +768,XelNaga_Caverns_Floating_BridgeV8Out +769,XelNaga_Caverns_Floating_BridgeV8 +770,XelNaga_Caverns_Floating_BridgeH10Out +771,XelNaga_Caverns_Floating_BridgeH10 +772,XelNaga_Caverns_Floating_BridgeV10Out +773,XelNaga_Caverns_Floating_BridgeV10 +774,XelNaga_Caverns_Floating_BridgeH12Out +775,XelNaga_Caverns_Floating_BridgeH12 +776,XelNaga_Caverns_Floating_BridgeV12Out +777,XelNaga_Caverns_Floating_BridgeV12 +780,CollapsibleTerranTowerPushUnitRampLeft +781,CollapsibleTerranTowerPushUnitRampRight +784,CollapsibleRockTowerPushUnit +785,CollapsibleTerranTowerPushUnit +786,CollapsibleRockTowerPushUnitRampRight +787,CollapsibleRockTowerPushUnitRampLeft +788,DigesterCreepSprayTargetUnit +789,DigesterCreepSprayUnit +790,NydusCanalAttackerWeapon +791,ViperConsumeStructureWeapon +794,ResourceBlocker +795,TempestWeapon +796,YoinkMissile +800,YoinkVikingAirMissile +802,YoinkVikingGroundMissile +804,YoinkSiegeTankMissile +806,WarHoundWeapon +808,EyeStalkWeapon +811,WidowMineWeapon +812,WidowMineAirWeapon +813,MothershipCoreWeaponWeapon +814,TornadoMissileWeapon +815,TornadoMissileDummyWeapon +816,TalonsMissileWeapon +817,CreepTumorMissile +818,LocustMPEggAMissileWeapon +819,LocustMPEggBMissileWeapon +820,LocustMPWeapon +822,RepulsorCannonWeapon +826,CollapsibleRockTowerDiagonal +827,CollapsibleTerranTowerDiagonal +828,CollapsibleTerranTowerRampLeft +829,CollapsibleTerranTowerRampRight +830,Ice2x2NonConjoined +831,IceProtossCrates +832,ProtossCrates +833,TowerMine +834,PickupPalletGas +835,PickupPalletMinerals +836,PickupScrapSalvage1x1 +837,PickupScrapSalvage2x2 +838,PickupScrapSalvage3x3 +839,RoughTerrain +840,UnbuildableBricksSmallUnit +841,UnbuildablePlatesSmallUnit +842,UnbuildablePlatesUnit +843,UnbuildableRocksSmallUnit +844,XelNagaHealingShrine +845,InvisibleTargetDummy +846,ProtossVespeneGeyser +847,CollapsibleRockTower +848,CollapsibleTerranTower +849,ThornLizard +850,CleaningBot +851,DestructibleRock6x6Weak +852,ProtossSnakeSegmentDemo +853,PhysicsCapsule +854,PhysicsCube +855,PhysicsCylinder +856,PhysicsKnot +857,PhysicsL +858,PhysicsPrimitives +859,PhysicsSphere +860,PhysicsStar +861,CreepBlocker4x4 +862,DestructibleCityDebris2x4Vertical +863,DestructibleCityDebris2x4Horizontal +864,DestructibleCityDebris2x6Vertical +865,DestructibleCityDebris2x6Horizontal +866,DestructibleCityDebris4x4 +867,DestructibleCityDebris6x6 +868,DestructibleCityDebrisHugeDiagonalBLUR +869,DestructibleCityDebrisHugeDiagonalULBR +870,TestZerg +871,PathingBlockerRadius1 +872,DestructibleRockEx12x4Vertical +873,DestructibleRockEx12x4Horizontal +874,DestructibleRockEx12x6Vertical +875,DestructibleRockEx12x6Horizontal +876,DestructibleRockEx14x4 +877,DestructibleRockEx16x6 +878,DestructibleRockEx1DiagonalHugeULBR +879,DestructibleRockEx1DiagonalHugeBLUR +880,DestructibleRockEx1VerticalHuge +881,DestructibleRockEx1HorizontalHuge +882,DestructibleIce2x4Vertical +883,DestructibleIce2x4Horizontal +884,DestructibleIce2x6Vertical +885,DestructibleIce2x6Horizontal +886,DestructibleIce4x4 +887,DestructibleIce6x6 +888,DestructibleIceDiagonalHugeULBR +889,DestructibleIceDiagonalHugeBLUR +890,DestructibleIceVerticalHuge +891,DestructibleIceHorizontalHuge +892,DesertPlanetSearchlight +893,DesertPlanetStreetlight +894,UnbuildableBricksUnit +895,UnbuildableRocksUnit +896,ZerusDestructibleArch +897,Artosilope +898,Anteplott +899,LabBot +900,Crabeetle +901,CollapsibleRockTowerRampRight +902,CollapsibleRockTowerRampLeft +903,LabMineralField +904,LabMineralField750 +919,CollapsibleRockTowerDebrisRampLeftGreen +920,CollapsibleRockTowerDebrisRampRightGreen +921,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +922,SnowRefinery_Terran_ExtendingBridgeNEShort8 +923,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +924,SnowRefinery_Terran_ExtendingBridgeNWShort8 +929,Tarsonis_DoorN +930,Tarsonis_DoorNLowered +931,Tarsonis_DoorNE +932,Tarsonis_DoorNELowered +933,Tarsonis_DoorE +934,Tarsonis_DoorELowered +935,Tarsonis_DoorNW +936,Tarsonis_DoorNWLowered +937,CompoundMansion_DoorN +938,CompoundMansion_DoorNLowered +939,CompoundMansion_DoorNE +940,CompoundMansion_DoorNELowered +941,CompoundMansion_DoorE +942,CompoundMansion_DoorELowered +943,CompoundMansion_DoorNW +944,CompoundMansion_DoorNWLowered +946,LocustMPFlying +947,AiurLightBridgeNE8Out +948,AiurLightBridgeNE8 +949,AiurLightBridgeNE10Out +950,AiurLightBridgeNE10 +951,AiurLightBridgeNE12Out +952,AiurLightBridgeNE12 +953,AiurLightBridgeNW8Out +954,AiurLightBridgeNW8 +955,AiurLightBridgeNW10Out +956,AiurLightBridgeNW10 +957,AiurLightBridgeNW12Out +958,AiurLightBridgeNW12 +959,AiurTempleBridgeNE8Out +961,AiurTempleBridgeNE10Out +963,AiurTempleBridgeNE12Out +965,AiurTempleBridgeNW8Out +967,AiurTempleBridgeNW10Out +969,AiurTempleBridgeNW12Out +971,ShakurasLightBridgeNE8Out +972,ShakurasLightBridgeNE8 +973,ShakurasLightBridgeNE10Out +974,ShakurasLightBridgeNE10 +975,ShakurasLightBridgeNE12Out +976,ShakurasLightBridgeNE12 +977,ShakurasLightBridgeNW8Out +978,ShakurasLightBridgeNW8 +979,ShakurasLightBridgeNW10Out +980,ShakurasLightBridgeNW10 +981,ShakurasLightBridgeNW12Out +982,ShakurasLightBridgeNW12 +983,VoidMPImmortalReviveCorpse +984,GuardianCocoonMP +985,GuardianMP +986,DevourerCocoonMP +987,DevourerMP +988,DefilerMPBurrowed +989,DefilerMP +990,OracleStasisTrap +991,DisruptorPhased +992,AiurLightBridgeAbandonedNE8Out +993,AiurLightBridgeAbandonedNE8 +994,AiurLightBridgeAbandonedNE10Out +995,AiurLightBridgeAbandonedNE10 +996,AiurLightBridgeAbandonedNE12Out +997,AiurLightBridgeAbandonedNE12 +998,AiurLightBridgeAbandonedNW8Out +999,AiurLightBridgeAbandonedNW8 +1000,AiurLightBridgeAbandonedNW10Out +1001,AiurLightBridgeAbandonedNW10 +1002,AiurLightBridgeAbandonedNW12Out +1003,AiurLightBridgeAbandonedNW12 +1004,CollapsiblePurifierTowerDebris +1005,PortCity_Bridge_UnitNE8Out +1006,PortCity_Bridge_UnitNE8 +1007,PortCity_Bridge_UnitSE8Out +1008,PortCity_Bridge_UnitSE8 +1009,PortCity_Bridge_UnitNW8Out +1010,PortCity_Bridge_UnitNW8 +1011,PortCity_Bridge_UnitSW8Out +1012,PortCity_Bridge_UnitSW8 +1013,PortCity_Bridge_UnitNE10Out +1014,PortCity_Bridge_UnitNE10 +1015,PortCity_Bridge_UnitSE10Out +1016,PortCity_Bridge_UnitSE10 +1017,PortCity_Bridge_UnitNW10Out +1018,PortCity_Bridge_UnitNW10 +1019,PortCity_Bridge_UnitSW10Out +1020,PortCity_Bridge_UnitSW10 +1021,PortCity_Bridge_UnitNE12Out +1022,PortCity_Bridge_UnitNE12 +1023,PortCity_Bridge_UnitSE12Out +1024,PortCity_Bridge_UnitSE12 +1025,PortCity_Bridge_UnitNW12Out +1026,PortCity_Bridge_UnitNW12 +1027,PortCity_Bridge_UnitSW12Out +1028,PortCity_Bridge_UnitSW12 +1029,PortCity_Bridge_UnitN8Out +1030,PortCity_Bridge_UnitN8 +1031,PortCity_Bridge_UnitS8Out +1032,PortCity_Bridge_UnitS8 +1033,PortCity_Bridge_UnitE8Out +1034,PortCity_Bridge_UnitE8 +1035,PortCity_Bridge_UnitW8Out +1036,PortCity_Bridge_UnitW8 +1037,PortCity_Bridge_UnitN10Out +1038,PortCity_Bridge_UnitN10 +1039,PortCity_Bridge_UnitS10Out +1040,PortCity_Bridge_UnitS10 +1041,PortCity_Bridge_UnitE10Out +1042,PortCity_Bridge_UnitE10 +1043,PortCity_Bridge_UnitW10Out +1044,PortCity_Bridge_UnitW10 +1045,PortCity_Bridge_UnitN12Out +1046,PortCity_Bridge_UnitN12 +1047,PortCity_Bridge_UnitS12Out +1048,PortCity_Bridge_UnitS12 +1049,PortCity_Bridge_UnitE12Out +1050,PortCity_Bridge_UnitE12 +1051,PortCity_Bridge_UnitW12Out +1052,PortCity_Bridge_UnitW12 +1053,PurifierRichMineralField +1054,PurifierRichMineralField750 +1055,CollapsibleRockTowerPushUnitRampLeftGreen +1056,CollapsibleRockTowerPushUnitRampRightGreen +1071,CollapsiblePurifierTowerPushUnit +1073,LocustMPPrecursor +1074,ReleaseInterceptorsBeacon +1075,AdeptPhaseShift +1076,HydraliskImpaleMissile +1077,CycloneMissileLargeAir +1078,CycloneMissile +1079,CycloneMissileLarge +1080,OracleWeapon +1081,TempestWeaponGround +1082,ScoutMPAirWeaponLeft +1083,ScoutMPAirWeaponRight +1084,ArbiterMPWeaponMissile +1085,GuardianMPWeapon +1086,DevourerMPWeaponMissile +1087,DefilerMPDarkSwarmWeapon +1088,QueenMPEnsnareMissile +1089,QueenMPSpawnBroodlingsMissile +1090,LightningBombWeapon +1091,HERCPlacement +1092,GrappleWeapon +1095,CausticSprayMissile +1096,ParasiticBombMissile +1097,ParasiticBombDummy +1098,AdeptWeapon +1099,AdeptUpgradeWeapon +1100,LiberatorMissile +1101,LiberatorDamageMissile +1102,LiberatorAGMissile +1103,KD8Charge +1104,KD8ChargeWeapon +1106,SlaynElementalGrabWeapon +1107,SlaynElementalGrabAirUnit +1108,SlaynElementalGrabGroundUnit +1109,SlaynElementalWeapon +1114,CollapsibleRockTowerRampLeftGreen +1115,CollapsibleRockTowerRampRightGreen +1116,DestructibleExpeditionGate6x6 +1117,DestructibleZergInfestation3x3 +1118,HERC +1119,Moopy +1120,Replicant +1121,SeekerMissile +1122,AiurTempleBridgeDestructibleNE8Out +1123,AiurTempleBridgeDestructibleNE10Out +1124,AiurTempleBridgeDestructibleNE12Out +1125,AiurTempleBridgeDestructibleNW8Out +1126,AiurTempleBridgeDestructibleNW10Out +1127,AiurTempleBridgeDestructibleNW12Out +1128,AiurTempleBridgeDestructibleSW8Out +1129,AiurTempleBridgeDestructibleSW10Out +1130,AiurTempleBridgeDestructibleSW12Out +1131,AiurTempleBridgeDestructibleSE8Out +1132,AiurTempleBridgeDestructibleSE10Out +1133,AiurTempleBridgeDestructibleSE12Out +1135,FlyoverUnit +1136,CorsairMP +1137,ScoutMP +1139,ArbiterMP +1140,ScourgeMP +1141,DefilerMPPlagueWeapon +1142,QueenMP +1143,XelNagaDestructibleRampBlocker6S +1144,XelNagaDestructibleRampBlocker6SE +1145,XelNagaDestructibleRampBlocker6E +1146,XelNagaDestructibleRampBlocker6NE +1147,XelNagaDestructibleRampBlocker6N +1148,XelNagaDestructibleRampBlocker6NW +1149,XelNagaDestructibleRampBlocker6W +1150,XelNagaDestructibleRampBlocker6SW +1151,XelNagaDestructibleRampBlocker8S +1152,XelNagaDestructibleRampBlocker8SE +1153,XelNagaDestructibleRampBlocker8E +1154,XelNagaDestructibleRampBlocker8NE +1155,XelNagaDestructibleRampBlocker8N +1156,XelNagaDestructibleRampBlocker8NW +1157,XelNagaDestructibleRampBlocker8W +1158,XelNagaDestructibleRampBlocker8SW +1159,XelNagaDestructibleBlocker6S +1160,XelNagaDestructibleBlocker6SE +1161,XelNagaDestructibleBlocker6E +1162,XelNagaDestructibleBlocker6NE +1163,XelNagaDestructibleBlocker6N +1164,XelNagaDestructibleBlocker6NW +1165,XelNagaDestructibleBlocker6W +1166,XelNagaDestructibleBlocker6SW +1167,XelNagaDestructibleBlocker8S +1168,XelNagaDestructibleBlocker8SE +1169,XelNagaDestructibleBlocker8E +1170,XelNagaDestructibleBlocker8NE +1171,XelNagaDestructibleBlocker8N +1172,XelNagaDestructibleBlocker8NW +1173,XelNagaDestructibleBlocker8W +1174,XelNagaDestructibleBlocker8SW +1175,ReptileCrate +1176,SlaynSwarmHostSpawnFlyer +1177,SlaynElemental +1178,PurifierVespeneGeyser +1179,ShakurasVespeneGeyser +1180,CollapsiblePurifierTowerDiagonal +1181,CreepOnlyBlocker4x4 +1182,BattleStationMineralField +1183,BattleStationMineralField750 +1184,PurifierMineralField +1185,PurifierMineralField750 +1186,Beacon_Nova +1187,Beacon_NovaSmall +1188,Ursula +1189,Elsecaro_Colonist_Hut +1190,SnowGlazeStarterMP +1191,PylonOvercharged +1192,ObserverSiegeMode +1193,RavenRepairDrone +1195,ParasiticBombRelayDummy +1196,BypassArmorDrone +1197,AdeptPiercingWeapon +1198,HighTemplarWeaponMissile +1199,CycloneMissileLargeAirAlternative +1200,RavenScramblerMissile +1201,RavenRepairDroneReleaseWeapon +1202,RavenShredderMissileWeapon +1203,InfestedAcidSpinesWeapon +1204,InfestorEnsnareAttackMissile +1205,SNARE_PLACEHOLDER +1208,CorrosiveParasiteWeapon diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index b6ec2d75..0d094b0a 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -480,6 +480,7 @@ def load_build(expansion, version): "70154", "76114", "77379", + "80949", ): lotv_builds[version] = load_build("LotV", version) diff --git a/sc2reader/data/ability_lookup.csv b/sc2reader/data/ability_lookup.csv index 9d0d68fb..831c79ef 100755 --- a/sc2reader/data/ability_lookup.csv +++ b/sc2reader/data/ability_lookup.csv @@ -865,3 +865,4 @@ BattlecruiserStop,Stop BattlecruiserAttack,BattlecruiserAttack BattlecruiserMove,Move,Patrol,HoldPos AmorphousArmorcloud,AmorphousArmorcloud +BatteryOvercharge,BatteryOvercharge diff --git a/sc2reader/data/unit_lookup.csv b/sc2reader/data/unit_lookup.csv index 2912f77d..2284dd18 100755 --- a/sc2reader/data/unit_lookup.csv +++ b/sc2reader/data/unit_lookup.csv @@ -1062,3 +1062,23 @@ ArtilleryMengskACGluescreenDummy,ArtilleryMengskACGluescreenDummy AccelerationZoneSmall,AccelerationZoneSmall AccelerationZoneMedium,AccelerationZoneMedium AccelerationZoneLarge,AccelerationZoneLarge +LoadOutSpray@1,LoadOutSpray@1 +LoadOutSpray@2,LoadOutSpray@2 +LoadOutSpray@3,LoadOutSpray@3 +LoadOutSpray@4,LoadOutSpray@4 +LoadOutSpray@5,LoadOutSpray@5 +LoadOutSpray@6,LoadOutSpray@6 +LoadOutSpray@7,LoadOutSpray@7 +LoadOutSpray@8,LoadOutSpray@8 +LoadOutSpray@9,LoadOutSpray@9 +LoadOutSpray@10,LoadOutSpray@10 +LoadOutSpray@11,LoadOutSpray@11 +LoadOutSpray@12,LoadOutSpray@12 +LoadOutSpray@13,LoadOutSpray@13 +LoadOutSpray@14,LoadOutSpray@14 +AccelerationZoneFlyingSmall,AccelerationZoneFlyingSmall +AccelerationZoneFlyingMedium,AccelerationZoneFlyingMedium +AccelerationZoneFlyingLarge,AccelerationZoneFlyingLarge +InhibitorZoneFlyingSmall,InhibitorZoneFlyingSmall +InhibitorZoneFlyingMedium,InhibitorZoneFlyingMedium +InhibitorZoneFlyingLarge,InhibitorZoneFlyingLarge diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 4196cabb..8d64fa9c 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -870,7 +870,11 @@ def register_default_datapacks(self): ) self.register_datapack( datapacks["LotV"]["77379"], - lambda r: r.expansion == "LotV" and 77379 <= r.build, + lambda r: r.expansion == "LotV" and 77379 <= r.build < 80949, + ) + self.register_datapack( + datapacks["LotV"]["80949"], + lambda r: r.expansion == "LotV" and 80949 <= r.build, ) # Internal Methods From 8ce5c26feda59a67be071c44101784c8ae5cecaa Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Thu, 30 Jul 2020 21:46:02 -0700 Subject: [PATCH 062/136] mark release version 1.6.0 --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a6526d8c..ac15426f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ CHANGELOG ============ +1.6.0 - July 30, 2020 +--------------------- +* Add support for protocol 80949 (StarCraft 5.0) #122 +* Fix toJson script #118 + 1.5.0 - January 18, 2020 ------------------------ * Add support for protocol 77379 #106 #107 diff --git a/setup.py b/setup.py index da473e7e..4cc6db7d 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.5.0", + version="1.6.0", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From d79239d452ec9d3035ff4007a4d130a4a166ccb6 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 28 Sep 2020 11:50:46 -0700 Subject: [PATCH 063/136] update docstring spacing for black --- sc2reader/decoders.py | 100 +++++++++++---- sc2reader/engine/engine.py | 155 ++++++++++++----------- sc2reader/engine/plugins/creeptracker.py | 4 +- sc2reader/engine/plugins/selection.py | 33 ++--- sc2reader/events/game.py | 7 +- sc2reader/events/message.py | 8 +- sc2reader/factories/sc2factory.py | 57 ++++++--- sc2reader/log_utils.py | 8 +- sc2reader/objects.py | 13 +- sc2reader/utils.py | 25 +++- 10 files changed, 254 insertions(+), 156 deletions(-) diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index 8520e259..56674a1c 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -31,7 +31,8 @@ class ByteDecoder(object): _contents = "" def __init__(self, contents, endian): - """ Accepts both strings and files implementing ``read()`` and + """ + Accepts both strings and files implementing ``read()`` and decodes them in the specified endian format. """ if hasattr(contents, "read"): @@ -66,40 +67,58 @@ def __init__(self, contents, endian): self._unpack_bytes = lambda bytes: bytes if self.endian == ">" else bytes[::-1] def done(self): - """ Returns true when all bytes have been decoded """ + """ + Returns true when all bytes have been decoded + """ return self.tell() == self.length def read_range(self, start, end): - """ Returns the raw byte string from the indicated address range """ + """ + Returns the raw byte string from the indicated address range + """ return self._contents[start:end] def peek(self, count): - """ Returns the raw byte string for the next ``count`` bytes """ + """ + Returns the raw byte string for the next ``count`` bytes + """ start = self.tell() return self._contents[start : start + count] def read_uint8(self): - """ Returns the next byte as an unsigned integer """ + """ + Returns the next byte as an unsigned integer + """ return ord(self.read(1)) def read_uint16(self): - """ Returns the next two bytes as an unsigned integer """ + """ + Returns the next two bytes as an unsigned integer + """ return self._unpack_short(self.read(2))[0] def read_uint32(self): - """ Returns the next four bytes as an unsigned integer """ + """ + Returns the next four bytes as an unsigned integer + """ return self._unpack_int(self.read(4))[0] def read_uint64(self): - """ Returns the next eight bytes as an unsigned integer """ + """ + Returns the next eight bytes as an unsigned integer + """ return self._unpack_longlong(self.read(8))[0] def read_bytes(self, count): - """ Returns the next ``count`` bytes as a byte string """ + """ + Returns the next ``count`` bytes as a byte string + """ return self._unpack_bytes(self.read(count)) def read_uint(self, count): - """ Returns the next ``count`` bytes as an unsigned integer """ + """ + Returns the next ``count`` bytes as an unsigned integer + """ unpack = struct.Struct(str(self.endian + "B" * count)).unpack uint = 0 for byte in unpack(self.read(count)): @@ -107,11 +126,15 @@ def read_uint(self, count): return uint def read_string(self, count, encoding="utf8"): - """ Read a string in given encoding (default utf8) that is ``count`` bytes long """ + """ + Read a string in given encoding (default utf8) that is ``count`` bytes long + """ return self.read_bytes(count).decode(encoding) def read_cstring(self, encoding="utf8"): - """ Read a NULL byte terminated character string decoded with given encoding (default utf8). Ignores endian. """ + """ + Read a NULL byte terminated character string decoded with given encoding (default utf8). Ignores endian. + """ cstring = BytesIO() while True: c = self.read(1) @@ -170,16 +193,22 @@ def __init__(self, contents): self.read_bool = functools.partial(self.read_bits, 1) def done(self): - """ Returns true when all bytes in the buffer have been used""" + """ + Returns true when all bytes in the buffer have been used + """ return self.tell() == self.length def byte_align(self): - """ Moves cursor to the beginning of the next byte """ + """ + Moves cursor to the beginning of the next byte + """ self._next_byte = None self._bit_shift = 0 def read_uint8(self): - """ Returns the next 8 bits as an unsigned integer """ + """ + Returns the next 8 bits as an unsigned integer + """ data = ord(self._buffer.read(1)) if self._bit_shift != 0: @@ -192,7 +221,9 @@ def read_uint8(self): return data def read_uint16(self): - """ Returns the next 16 bits as an unsigned integer """ + """ + Returns the next 16 bits as an unsigned integer + """ data = self._buffer.read_uint16() if self._bit_shift != 0: @@ -206,7 +237,9 @@ def read_uint16(self): return data def read_uint32(self): - """ Returns the next 32 bits as an unsigned integer """ + """ + Returns the next 32 bits as an unsigned integer + """ data = self._buffer.read_uint32() if self._bit_shift != 0: @@ -220,7 +253,9 @@ def read_uint32(self): return data def read_uint64(self): - """ Returns the next 64 bits as an unsigned integer """ + """Returns + the next 64 bits as an unsigned integer + """ data = self._buffer.read_uint64() if self._bit_shift != 0: @@ -234,7 +269,9 @@ def read_uint64(self): return data def read_vint(self): - """ Reads a signed integer of variable length """ + """ + Reads a signed integer of variable length + """ byte = ord(self._buffer.read(1)) negative = byte & 0x01 result = (byte & 0x7F) >> 1 @@ -246,17 +283,23 @@ def read_vint(self): return -result if negative else result def read_aligned_bytes(self, count): - """ Skips to the beginning of the next byte and returns the next ``count`` bytes as a byte string """ + """ + Skips to the beginning of the next byte and returns the next ``count`` bytes as a byte string + """ self.byte_align() return self._buffer.read_bytes(count) def read_aligned_string(self, count, encoding="utf8"): - """ Skips to the beginning of the next byte and returns the next ``count`` bytes decoded with encoding (default utf8) """ + """ + Skips to the beginning of the next byte and returns the next ``count`` bytes decoded with encoding (default utf8) + """ self.byte_align() return self._buffer.read_string(count, encoding) def read_bytes(self, count): - """ Returns the next ``count*8`` bits as a byte string """ + """ + Returns the next ``count*8`` bits as a byte string + """ data = self._buffer.read_bytes(count) if self._bit_shift != 0: @@ -276,7 +319,9 @@ def read_bytes(self, count): return data def read_bits(self, count): - """ Returns the next ``count`` bits as an unsigned integer """ + """Returns + the next ``count`` bits as an unsigned integer + """ result = 0 bits = count bit_shift = self._bit_shift @@ -325,7 +370,9 @@ def read_bits(self, count): return result def read_frames(self): - """ Reads a frame count as an unsigned integer """ + """ + Reads a frame count as an unsigned integer + """ byte = self.read_uint8() time, additional_bytes = byte >> 2, byte & 0x03 if additional_bytes == 0: @@ -338,8 +385,9 @@ def read_frames(self): return time << 24 | self.read_uint16() << 8 | self.read_uint8() def read_struct(self, datatype=None): - """ Reads a nested data structure. If the type is not specified the - first byte is used as the type identifier. + """ + Reads a nested data structure. If the type is not specified + the first byte is used as the type identifier. """ self.byte_align() datatype = ord(self._buffer.read(1)) if datatype is None else datatype diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index e091fa3a..479872df 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -7,110 +7,111 @@ class GameEngine(object): - """ GameEngine Specification - -------------------------- + """ + GameEngine Specification + -------------------------- - The game engine runs through all the events for a given replay in - chronological order. For each event, event handlers from registered - plugins are executed in order of plugin registration from most general - to most specific. + The game engine runs through all the events for a given replay in + chronological order. For each event, event handlers from registered + plugins are executed in order of plugin registration from most general + to most specific. - Example Usage:: + Example Usage:: - class Plugin1(): - def handleCommandEvent(self, event, replay): - pass + class Plugin1(): + def handleCommandEvent(self, event, replay): + pass - class Plugin2(): - def handleEvent(self, event, replay): - pass + class Plugin2(): + def handleEvent(self, event, replay): + pass - def handleTargetUnitCommandEvent(self, event, replay): - pass + def handleTargetUnitCommandEvent(self, event, replay): + pass - ... + ... - engine = GameEngine(plugins=[Plugin1(), Plugin2()], **options) - engine.register_plugins(Plugin3(), Plugin(4)) - engine.reigster_plugin(Plugin(5)) - engine.run(replay) + engine = GameEngine(plugins=[Plugin1(), Plugin2()], **options) + engine.register_plugins(Plugin3(), Plugin(4)) + engine.reigster_plugin(Plugin(5)) + engine.run(replay) - Calls functions in the following order for a ``TargetUnitCommandEvent``:: + Calls functions in the following order for a ``TargetUnitCommandEvent``:: - Plugin1.handleCommandEvent(event, replay) - Plugin2.handleEvent(event, replay) - Plugin2.handleTargetUnitCommandEvent(event, replay) + Plugin1.handleCommandEvent(event, replay) + Plugin2.handleEvent(event, replay) + Plugin2.handleTargetUnitCommandEvent(event, replay) - Plugin Specification - ------------------------- + Plugin Specification + ------------------------- - Plugins can opt in to handle events with methods in the format: + Plugins can opt in to handle events with methods in the format: - def handleEventName(self, event, replay) + def handleEventName(self, event, replay) - In addition to handling specific event types, plugins can also - handle events more generally by handling built-in parent classes - from the list below:: + In addition to handling specific event types, plugins can also + handle events more generally by handling built-in parent classes + from the list below:: - * handleEvent - called for every single event of all types - * handleMessageEvent - called for events in replay.message.events - * handleGameEvent - called for events in replay.game.events - * handleTrackerEvent - called for events in replay.tracker.events - * handleCommandEvent - called for all types of command events - * handleControlGroupEvent - called for all player control group events + * handleEvent - called for every single event of all types + * handleMessageEvent - called for events in replay.message.events + * handleGameEvent - called for events in replay.game.events + * handleTrackerEvent - called for events in replay.tracker.events + * handleCommandEvent - called for all types of command events + * handleControlGroupEvent - called for all player control group events - Plugins may also handle optional ``InitGame`` and ``EndGame`` events generated - by the GameEngine before and after processing all the events: + Plugins may also handle optional ``InitGame`` and ``EndGame`` events generated + by the GameEngine before and after processing all the events: - * handleInitGame - is called prior to processing a new replay to provide - an opportunity for the plugin to clear internal state and set up any - replay state necessary. + * handleInitGame - is called prior to processing a new replay to provide + an opportunity for the plugin to clear internal state and set up any + replay state necessary. - * handleEndGame - is called after all events have been processed and - can be used to perform post processing on aggrated data or clean up - intermediate data caches. + * handleEndGame - is called after all events have been processed and + can be used to perform post processing on aggrated data or clean up + intermediate data caches. - Event handlers can choose to ``yield`` additional events which will be injected - into the event stream directly after the event currently being processed. This - feature allows for message passing between plugins. An ExpansionTracker plugin - could notify all other plugins of a new ExpansionEvent that they could opt to - process:: + Event handlers can choose to ``yield`` additional events which will be injected + into the event stream directly after the event currently being processed. This + feature allows for message passing between plugins. An ExpansionTracker plugin + could notify all other plugins of a new ExpansionEvent that they could opt to + process:: - def handleUnitDoneEvent(self, event, replay): - if event.unit.name == 'Nexus': - yield ExpansionEvent(event.frame, event.unit) - .... + def handleUnitDoneEvent(self, event, replay): + if event.unit.name == 'Nexus': + yield ExpansionEvent(event.frame, event.unit) + .... - If a plugin wishes to stop processing a replay it can yield a PluginExit event before returning:: + If a plugin wishes to stop processing a replay it can yield a PluginExit event before returning:: - def handleEvent(self, event, replay): - if len(replay.tracker_events) == 0: - yield PluginExit(self, code=0, details=dict(msg="tracker events required")) - return - ... + def handleEvent(self, event, replay): + if len(replay.tracker_events) == 0: + yield PluginExit(self, code=0, details=dict(msg="tracker events required")) + return + ... - def handleCommandEvent(self, event, replay): - try: - possibly_throwing_error() - catch Error as e: - logger.error(e) - yield PluginExit(self, code=0, details=dict(msg="Unexpected exception")) + def handleCommandEvent(self, event, replay): + try: + possibly_throwing_error() + catch Error as e: + logger.error(e) + yield PluginExit(self, code=0, details=dict(msg="Unexpected exception")) - The GameEngine will intercept this event and remove the plugin from the list of - active plugins for this replay. The exit code and details will be available from the - replay:: + The GameEngine will intercept this event and remove the plugin from the list of + active plugins for this replay. The exit code and details will be available from the + replay:: - code, details = replay.plugins['MyPlugin'] + code, details = replay.plugins['MyPlugin'] - If your plugin depends on another plugin, it is a good idea to implement handlePluginExit - and be alerted if the plugin that you require fails. This way you can exit gracefully. You - can also check to see if the plugin name is in ``replay.plugin_failures``:: + If your plugin depends on another plugin, it is a good idea to implement handlePluginExit + and be alerted if the plugin that you require fails. This way you can exit gracefully. You + can also check to see if the plugin name is in ``replay.plugin_failures``:: - if 'RequiredPlugin' in replay.plugin_failures: - code, details = replay.plugins['RequiredPlugin'] - message = "RequiredPlugin failed with code: {0}. Cannot continue.".format(code) - yield PluginExit(self, code=1, details=dict(msg=message)) + if 'RequiredPlugin' in replay.plugin_failures: + code, details = replay.plugins['RequiredPlugin'] + message = "RequiredPlugin failed with code: {0}. Cannot continue.".format(code) + yield PluginExit(self, code=1, details=dict(msg=message)) """ def __init__(self, plugins=[]): diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index db5e6ac8..8dde77c9 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -86,8 +86,8 @@ def handleEndGame(self, event, replay): for player in replay.players: if player.play_race[0] == "Z": self.creepTracker.reduce_cgu_per_minute(player.pid) - player.creep_spread_by_minute = self.creepTracker.get_creep_spread_area( - player.pid + player.creep_spread_by_minute = ( + self.creepTracker.get_creep_spread_area(player.pid) ) # note that player.max_creep_spread may be a tuple or an int if player.creep_spread_by_minute: diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 59d4ee02..52f50c65 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -3,25 +3,26 @@ class SelectionTracker(object): - """ Tracks a player's active selection as an input into other plugins. + """ + Tracks a player's active selection as an input into other plugins. - In some situations selection tracking isn't perfect. The plugin will - detect these situations and report errors. For a player will a high - level of selection errors, it may be best to ignore the selection - results as they could have been severely compromised. + In some situations selection tracking isn't perfect. The plugin will + detect these situations and report errors. For a player will a high + level of selection errors, it may be best to ignore the selection + results as they could have been severely compromised. - Exposes the following interface, directly integrated into the player: + Exposes the following interface, directly integrated into the player: - for person in replay.entities: - total_errors = person.selection_errors + for person in replay.entities: + total_errors = person.selection_errors - selection = person.selection - control_group_0 = selection[0] - ... - control_group_9 = selection[9] - active_selection = selection[10] + selection = person.selection + control_group_0 = selection[0] + ... + control_group_9 = selection[9] + active_selection = selection[10] - # TODO: list a few error inducing situations + # TODO: list a few error inducing situations """ name = "SelectionTracker" @@ -69,7 +70,9 @@ def _select(self, selection, units): return sorted(set(selection + units)) def _deselect(self, selection, mode, data): - """Returns false if there was a data error when deselecting""" + """ + Returns false if there was a data error when deselecting + """ if mode == "None": return selection, False diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 346adb96..02a054dd 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -685,8 +685,11 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( - self.minerals, self.vespene, self.terrazine, self.custom + return ( + self._str_prefix() + + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( + self.minerals, self.vespene, self.terrazine, self.custom + ) ) diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index 2fdb9cfd..57da5dce 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -9,7 +9,7 @@ @loggable class MessageEvent(Event): """ - Parent class for all message events. + Parent class for all message events. """ def __init__(self, frame, pid): @@ -36,7 +36,7 @@ def __str__(self): @loggable class ChatEvent(MessageEvent): """ - Records in-game chat events. + Records in-game chat events. """ def __init__(self, frame, pid, target, text): @@ -60,7 +60,7 @@ def __init__(self, frame, pid, target, text): @loggable class ProgressEvent(MessageEvent): """ - Sent during the load screen to update load process for other clients. + Sent during the load screen to update load process for other clients. """ def __init__(self, frame, pid, progress): @@ -73,7 +73,7 @@ def __init__(self, frame, pid, progress): @loggable class PingEvent(MessageEvent): """ - Records pings made by players in game. + Records pings made by players in game. """ def __init__(self, frame, pid, target, x, y): diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index 690797a3..1bde3ac1 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -28,7 +28,8 @@ @log_utils.loggable class SC2Factory(object): - """The SC2Factory class acts as a generic loader interface for all + """ + The SC2Factory class acts as a generic loader interface for all available to sc2reader resources. At current time this includes :class:`~sc2reader.resources.Replay` and :class:`~sc2reader.resources.Map` resources. These resources can be loaded in both singular and plural contexts with: @@ -81,57 +82,79 @@ def __init__(self, **options): # Primary Interface def load_replay(self, source, options=None, **new_options): - """Loads a single sc2replay file. Accepts file path, url, or file object.""" + """ + Loads a single sc2replay file. Accepts file path, url, or file object. + """ return self.load(Replay, source, options, **new_options) def load_replays(self, sources, options=None, **new_options): - """Loads a collection of sc2replay files, returns a generator.""" + """ + Loads a collection of sc2replay files, returns a generator. + """ return self.load_all( Replay, sources, options, extension="SC2Replay", **new_options ) def load_localization(self, source, options=None, **new_options): - """Loads a single s2ml file. Accepts file path, url, or file object.""" + """ + Loads a single s2ml file. Accepts file path, url, or file object. + """ return self.load(Localization, source, options, **new_options) def load_localizations(self, sources, options=None, **new_options): - """Loads a collection of s2ml files, returns a generator.""" + """ + Loads a collection of s2ml files, returns a generator. + """ return self.load_all( Localization, sources, options, extension="s2ml", **new_options ) def load_map(self, source, options=None, **new_options): - """Loads a single s2ma file. Accepts file path, url, or file object.""" + """ + Loads a single s2ma file. Accepts file path, url, or file object. + """ return self.load(Map, source, options, **new_options) def load_maps(self, sources, options=None, **new_options): - """Loads a collection of s2ma files, returns a generator.""" + """ + Loads a collection of s2ma files, returns a generator. + """ return self.load_all(Map, sources, options, extension="s2ma", **new_options) def load_game_summary(self, source, options=None, **new_options): - """Loads a single s2gs file. Accepts file path, url, or file object.""" + """ + Loads a single s2gs file. Accepts file path, url, or file object. + """ return self.load(GameSummary, source, options, **new_options) def load_game_summaries(self, sources, options=None, **new_options): - """Loads a collection of s2gs files, returns a generator.""" + """ + Loads a collection of s2gs files, returns a generator. + """ return self.load_all( GameSummary, sources, options, extension="s2gs", **new_options ) def configure(self, cls=None, **options): - """ Configures the factory to use the supplied options. If cls is specified - the options will only be applied when loading that class""" + """ + Configures the factory to use the supplied options. If cls is specified + the options will only be applied when loading that class + """ if isinstance(cls, basestring): cls = self._resource_name_map.get[cls.lower()] cls = cls or Resource self.options[cls].update(options) def reset(self): - "Resets the options to factory defaults" + """ + Resets the options to factory defaults + """ self.options = defaultdict(dict) def register_plugin(self, cls, plugin): - "Registers the given Plugin to be run on classes of the supplied name." + """ + Registers the given Plugin to be run on classes of the supplied name. + """ if isinstance(cls, basestring): cls = self._resource_name_map.get(cls.lower(), Resource) self.plugins.append((cls, plugin)) @@ -170,7 +193,9 @@ def _get_options(self, cls, **new_options): return options def _load_resources(self, resources, options=None, **new_options): - """Collections of resources or a path to a directory""" + """ + Collections of resources or a path to a directory + """ options = options or self._get_options(Resource, **new_options) # Path to a folder, retrieve all relevant files as the collection @@ -190,7 +215,9 @@ def load_local_resource_contents(self, location, **options): return resource_file.read() def _load_resource(self, resource, options=None, **new_options): - """http links, filesystem locations, and file-like objects""" + """ + http links, filesystem locations, and file-like objects + """ options = options or self._get_options(Resource, **new_options) if isinstance(resource, utils.DepotFile): diff --git a/sc2reader/log_utils.py b/sc2reader/log_utils.py index a53656c8..c4288820 100644 --- a/sc2reader/log_utils.py +++ b/sc2reader/log_utils.py @@ -67,12 +67,12 @@ def add_log_handler(handler, level="WARN", format=None, datefmt=None): def get_logger(entity): """ - Retrieves loggers from the enties fully scoped name. + Retrieves loggers from the enties fully scoped name. - get_logger(Replay) -> sc2reader.replay.Replay - get_logger(get_logger) -> sc2reader.utils.get_logger + get_logger(Replay) -> sc2reader.replay.Replay + get_logger(get_logger) -> sc2reader.utils.get_logger - :param entity: The entity for which we want a logger. + :param entity: The entity for which we want a logger. """ try: return logging.getLogger(entity.__module__ + "." + entity.__name__) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 5d37f62a..d9e53035 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -283,14 +283,17 @@ def __init__(self, uid, init_data): @property def url(self): - """The player's formatted Battle.net profile url""" + """ + The player's formatted Battle.net profile url + """ return self.URL_TEMPLATE.format( **self.__dict__ ) # region=self.region, toon_id=self.toon_id, subregion=self.subregion, name=self.name.('utf8')) class Observer(Entity, User): - """ Extends :class:`Entity` and :class:`User`. + """ + Extends :class:`Entity` and :class:`User`. :param integer sid: The entity's unique slot id. :param dict slot_data: The slot data associated with this entity @@ -314,7 +317,8 @@ def __repr__(self): class Computer(Entity, Player): - """ Extends :class:`Entity` and :class:`Player` + """ + Extends :class:`Entity` and :class:`Player` :param integer sid: The entity's unique slot id. :param dict slot_data: The slot data associated with this entity @@ -338,7 +342,8 @@ def __repr__(self): class Participant(Entity, User, Player): - """ Extends :class:`Entity`, :class:`User`, and :class:`Player` + """ + Extends :class:`Entity`, :class:`User`, and :class:`Player` :param integer sid: The entity's unique slot id. :param dict slot_data: The slot data associated with this entity diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 41e4ed09..c1cfd437 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -93,12 +93,16 @@ def __init__(self, name=None, r=0, g=0, b=0, a=255): @property def rgba(self): - """ Returns a tuple containing the color's (r,g,b,a) """ + """ + Returns a tuple containing the color's (r,g,b,a) + """ return (self.r, self.g, self.b, self.a) @property def hex(self): - """The hexadecimal representation of the color""" + """ + The hexadecimal representation of the color + """ return "{0.r:02X}{0.g:02X}{0.b:02X}".format(self) def __str__(self): @@ -197,23 +201,30 @@ def get_files( class Length(timedelta): - """ Extends the builtin timedelta class. See python docs for more info on - what capabilities this gives you. + """ + Extends the builtin timedelta class. See python docs for more info on + what capabilities this gives you. """ @property def hours(self): - """ The number of hours in represented. """ + """ + The number of hours in represented. + """ return self.seconds // 3600 @property def mins(self): - """ The number of minutes in excess of the hours. """ + """ + The number of minutes in excess of the hours. + """ return self.seconds // 60 % 60 @property def secs(self): - """ The number of seconds in excess of the minutes. """ + """ + The number of seconds in excess of the minutes. + """ return self.seconds % 60 def __str__(self): From 15bcece869ddb7a268d9c87cd48b5f4c1916af53 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 28 Sep 2020 11:57:06 -0700 Subject: [PATCH 064/136] add DOI to the readme --- README.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index fa78c025..dcbcb938 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,5 @@ +.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.4007376.svg + :target: https://doi.org/10.5281/zenodo.4007376 What is sc2reader? ==================== @@ -16,7 +18,7 @@ sc2reader is currently powering: * Tools: `The Core`_ * Experiments: `Midi Conversion`_ -If you use sc2reader and you would like your tool, site, project, or implementation listed above, drop us a line on our `mailing list`_ or stop by our #sc2reader IRC channel and say hi! +If you use sc2reader and you would like your tool, site, project, or implementation listed above, drop us a line on our `mailing list`_. .. _gggreplays.com: http://gggreplays.com From b942fe354140409ccc26421a2534ec70971559ee Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 28 Sep 2020 20:51:40 -0700 Subject: [PATCH 065/136] some attributes --- sc2reader/data/attributes.json | 2918 ++++++++++++++++++-------------- 1 file changed, 1649 insertions(+), 1269 deletions(-) diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index 53563b8e..3b9a92a6 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -1,1576 +1,1956 @@ { "attributes": { "0500": [ - "Controller", + "Controller", { - "Clsd": "Closed", - "Comp": "Computer", - "Humn": "User", + "Clsd": "Closed", + "Comp": "Computer", + "Humn": "User", "Open": "Open" } - ], + ], "1000": [ - "Rules", + "Rules", { "Dflt": "Default" } - ], + ], "1001": [ - "Premade Game", + "Premade Game", { - "no": "No", + "no": "No", "yes": "Yes" } - ], + ], "2000": [ - "Teams", - { - "CuTa": "Custom Teams Archon", - "Cust": "Custom Teams", - "FFA": "Free For All", - "FFAT": "Free For All Archon", - "t1": "1 Team", - "t10": "10 Teams", - "t11": "11 Teams", - "t2": "2 Teams", - "t3": "3 Teams", - "t4": "4 Teams", - "t5": "5 Teams", - "t6": "6 Teams", - "t7": "7 Teams", - "t8": "8 Teams", + "Teams", + { + "CuTa": "Custom Teams Archon", + "Cust": "Custom Teams", + "FFA": "Free For All", + "FFAT": "Free For All Archon", + "t1": "1 Team", + "t10": "10 Teams", + "t11": "11 Teams", + "t2": "2 Teams", + "t3": "3 Teams", + "t4": "4 Teams", + "t5": "5 Teams", + "t6": "6 Teams", + "t7": "7 Teams", + "t8": "8 Teams", "t9": "9 Teams" } - ], + ], "2001": [ - "Teams", - { - "1v1": "1v1", - "2v2": "2v2", - "3v3": "3v3", - "4v4": "4v4", - "5v5": "5v5", - "6v6": "6v6", + "Teams", + { + "1v1": "1v1", + "2v2": "2v2", + "3v3": "3v3", + "4v4": "4v4", + "5v5": "5v5", + "6v6": "6v6", "FFA": "FFA" } - ], + ], "2002": [ - "Teams1v1", + "Teams1v1", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2003": [ - "Teams2v2", + "Teams2v2", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2004": [ - "Teams3v3", + "Teams3v3", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2005": [ - "Teams4v4", + "Teams4v4", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2006": [ - "TeamsFFA", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "TeamsFFA", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2007": [ - "Teams5v5", + "Teams5v5", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2008": [ - "Teams6v6", + "Teams6v6", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2010": [ - "Team", + "Team", { "T1": "Team 1" } - ], + ], "2011": [ - "Teams7v7", + "Teams7v7", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2012": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", + "T1": "Team 1", + "T2": "Team 2", "T3": "Team 3" } - ], + ], "2013": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", "T4": "Team 4" } - ], + ], "2014": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", "T5": "Team 5" } - ], + ], "2015": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", "T6": "Team 6" } - ], + ], "2016": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", "T7": "Team 7" } - ], + ], "2017": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2018": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2019": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", "T8": "Team 8" } - ], + ], "2020": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2021": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2022": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2023": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", "T6": "Team 6" } - ], + ], "2024": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", - "T9": "Team 9", - "T10": "Team 10", - "T11": "Team 11" - } - ], + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", + "T9": "Team 9" + } + ], "3000": [ - "Game Speed", + "Game Speed", { - "Fasr": "Faster", - "Fast": "Fast", - "Norm": "Normal", - "Slor": "Slower", + "Fasr": "Faster", + "Fast": "Fast", + "Norm": "Normal", + "Slor": "Slower", "Slow": "Slow" } - ], + ], "3001": [ - "Race", + "Race", { - "Prot": "Protoss", - "RAND": "Random", - "Terr": "Terran", + "Prot": "Protoss", + "RAND": "Random", + "Terr": "Terran", "Zerg": "Zerg" } - ], + ], "3002": [ - "Color", - { - "tc01": "Red", - "tc02": "Blue", - "tc03": "Teal", - "tc04": "Purple", - "tc05": "Yellow", - "tc06": "Orange", - "tc07": "Green", - "tc08": "Light Pink", - "tc09": "Violet", - "tc10": "Light Grey", - "tc11": "Dark Green", - "tc12": "Brown", - "tc13": "Light Green", - "tc14": "Dark Grey", - "tc15": "Pink", + "Color", + { + "tc01": "Red", + "tc02": "Blue", + "tc03": "Teal", + "tc04": "Purple", + "tc05": "Yellow", + "tc06": "Orange", + "tc07": "Green", + "tc08": "Light Pink", + "tc09": "Violet", + "tc10": "Light Grey", + "tc11": "Dark Green", + "tc12": "Brown", + "tc13": "Light Green", + "tc14": "Dark Grey", + "tc15": "Pink", "tc16": "??" } - ], + ], "3003": [ - "Handicap", + "Handicap", { - "100": "100%", - "50": "50%", - "60": "60%", - "70": "70%", - "80": "80%", + "100": "100%", + "50": "50%", + "60": "60%", + "70": "70%", + "80": "80%", "90": "90%" } - ], + ], "3004": [ - "Difficulty", - { - "ChRe": "Cheater 2 (Resources)", - "ChVi": "Cheater 1 (Vision)", - "Easy": "Easy", - "Hard": "Harder", - "HdVH": "Very Hard", - "Insa": "Insane", - "MdHd": "Hard", - "Medi": "Medium", - "VyEy": "Very Easy", + "Difficulty", + { + "ChRe": "Cheater 2 (Resources)", + "ChVi": "Cheater 1 (Vision)", + "Easy": "Easy", + "Hard": "Harder", + "HdVH": "Very Hard", + "Insa": "Insane", + "MdHd": "Hard", + "Medi": "Medium", + "VyEy": "Very Easy", "VyHd": "Elite" } - ], + ], "3006": [ - "Lobby Delay", - { - "10": "10", - "15": "15", - "20": "20", - "25": "25", - "3": "3", - "30": "30", - "5": "5", + "Lobby Delay", + { + "10": "10", + "15": "15", + "20": "20", + "25": "25", + "3": "3", + "30": "30", + "5": "5", "7": "7" } - ], + ], "3007": [ - "Participant Role", + "Participant Role", { - "Part": "Participant", + "Part": "Participant", "Watc": "Observer" } - ], + ], "3008": [ - "Observer Type", + "Observer Type", { - "Obs": "Spectator", + "Obs": "Spectator", "Ref": "Referee" } - ], + ], "3009": [ - "Game Mode", + "Game Mode", { - "": "Single Player", - "Amm": "Ladder", - "Priv": "Private", + "": "Single Player", + "Amm": "Ladder", + "Priv": "Private", "Pub": "Public" } - ], + ], "3010": [ - "Locked Alliances", + "Locked Alliances", { - "no": "No", + "no": "No", "yes": "Yes" } - ], + ], "3011": [ - "Player Logo Index", - { - "0": "0", - "1": "1", - "10": "10", - "100": "100", - "101": "101", - "102": "102", - "103": "103", - "104": "104", - "105": "105", - "106": "106", - "107": "107", - "108": "108", - "109": "109", - "11": "11", - "110": "110", - "111": "111", - "112": "112", - "113": "113", - "114": "114", - "115": "115", - "116": "116", - "117": "117", - "118": "118", - "119": "119", - "12": "12", - "120": "120", - "121": "121", - "122": "122", - "123": "123", - "124": "124", - "125": "125", - "126": "126", - "127": "127", - "128": "128", - "129": "129", - "13": "13", - "130": "130", - "131": "131", - "132": "132", - "133": "133", - "134": "134", - "135": "135", - "136": "136", - "137": "137", - "138": "138", - "139": "139", - "14": "14", - "140": "140", - "141": "141", - "142": "142", - "143": "143", - "144": "144", - "145": "145", - "146": "146", - "147": "147", - "148": "148", - "149": "149", - "15": "15", - "150": "150", - "151": "151", - "152": "152", - "153": "153", - "154": "154", - "155": "155", - "156": "156", - "157": "157", - "158": "158", - "159": "159", - "16": "16", - "160": "160", - "161": "161", - "162": "162", - "163": "163", - "164": "164", - "165": "165", - "166": "166", - "167": "167", - "168": "168", - "169": "169", - "17": "17", - "170": "170", - "171": "171", - "172": "172", - "173": "173", - "174": "174", - "175": "175", - "176": "176", - "177": "177", - "178": "178", - "179": "179", - "18": "18", - "180": "180", - "181": "181", - "182": "182", - "183": "183", - "184": "184", - "185": "185", - "186": "186", - "187": "187", - "188": "188", - "189": "189", - "19": "19", - "190": "190", - "191": "191", - "192": "192", - "193": "193", - "194": "194", - "195": "195", - "196": "196", - "197": "197", - "198": "198", - "199": "199", - "2": "2", - "20": "20", - "200": "200", - "201": "201", - "202": "202", - "203": "203", - "204": "204", - "205": "205", - "206": "206", - "207": "207", - "208": "208", - "209": "209", - "21": "21", - "210": "210", - "211": "211", - "212": "212", - "213": "213", - "214": "214", - "215": "215", - "216": "216", - "217": "217", - "218": "218", - "219": "219", - "22": "22", - "220": "220", - "221": "221", - "222": "222", - "223": "223", - "224": "224", - "225": "225", - "226": "226", - "227": "227", - "228": "228", - "229": "229", - "23": "23", - "230": "230", - "231": "231", - "232": "232", - "233": "233", - "234": "234", - "235": "235", - "236": "236", - "237": "237", - "238": "238", - "239": "239", - "24": "24", - "240": "240", - "241": "241", - "242": "242", - "243": "243", - "244": "244", - "245": "245", - "246": "246", - "247": "247", - "248": "248", - "249": "249", - "25": "25", - "250": "250", - "251": "251", - "252": "252", - "253": "253", - "254": "254", - "255": "255", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "31": "31", - "32": "32", - "33": "33", - "34": "34", - "35": "35", - "36": "36", - "37": "37", - "38": "38", - "39": "39", - "4": "4", - "40": "40", - "41": "41", - "42": "42", - "43": "43", - "44": "44", - "45": "45", - "46": "46", - "47": "47", - "48": "48", - "49": "49", - "5": "5", - "50": "50", - "51": "51", - "52": "52", - "53": "53", - "54": "54", - "55": "55", - "56": "56", - "57": "57", - "58": "58", - "59": "59", - "6": "6", - "60": "60", - "61": "61", - "62": "62", - "63": "63", - "64": "64", - "65": "65", - "66": "66", - "67": "67", - "68": "68", - "69": "69", - "7": "7", - "70": "70", - "71": "71", - "72": "72", - "73": "73", - "74": "74", - "75": "75", - "76": "76", - "77": "77", - "78": "78", - "79": "79", - "8": "8", - "80": "80", - "81": "81", - "82": "82", - "83": "83", - "84": "84", - "85": "85", - "86": "86", - "87": "87", - "88": "88", - "89": "89", - "9": "9", - "90": "90", - "91": "91", - "92": "92", - "93": "93", - "94": "94", - "95": "95", - "96": "96", - "97": "97", - "98": "98", + "Player Logo Index", + { + "0": "0", + "1": "1", + "10": "10", + "100": "100", + "101": "101", + "102": "102", + "103": "103", + "104": "104", + "105": "105", + "106": "106", + "107": "107", + "108": "108", + "109": "109", + "11": "11", + "110": "110", + "111": "111", + "112": "112", + "113": "113", + "114": "114", + "115": "115", + "116": "116", + "117": "117", + "118": "118", + "119": "119", + "12": "12", + "120": "120", + "121": "121", + "122": "122", + "123": "123", + "124": "124", + "125": "125", + "126": "126", + "127": "127", + "128": "128", + "129": "129", + "13": "13", + "130": "130", + "131": "131", + "132": "132", + "133": "133", + "134": "134", + "135": "135", + "136": "136", + "137": "137", + "138": "138", + "139": "139", + "14": "14", + "140": "140", + "141": "141", + "142": "142", + "143": "143", + "144": "144", + "145": "145", + "146": "146", + "147": "147", + "148": "148", + "149": "149", + "15": "15", + "150": "150", + "151": "151", + "152": "152", + "153": "153", + "154": "154", + "155": "155", + "156": "156", + "157": "157", + "158": "158", + "159": "159", + "16": "16", + "160": "160", + "161": "161", + "162": "162", + "163": "163", + "164": "164", + "165": "165", + "166": "166", + "167": "167", + "168": "168", + "169": "169", + "17": "17", + "170": "170", + "171": "171", + "172": "172", + "173": "173", + "174": "174", + "175": "175", + "176": "176", + "177": "177", + "178": "178", + "179": "179", + "18": "18", + "180": "180", + "181": "181", + "182": "182", + "183": "183", + "184": "184", + "185": "185", + "186": "186", + "187": "187", + "188": "188", + "189": "189", + "19": "19", + "190": "190", + "191": "191", + "192": "192", + "193": "193", + "194": "194", + "195": "195", + "196": "196", + "197": "197", + "198": "198", + "199": "199", + "2": "2", + "20": "20", + "200": "200", + "201": "201", + "202": "202", + "203": "203", + "204": "204", + "205": "205", + "206": "206", + "207": "207", + "208": "208", + "209": "209", + "21": "21", + "210": "210", + "211": "211", + "212": "212", + "213": "213", + "214": "214", + "215": "215", + "216": "216", + "217": "217", + "218": "218", + "219": "219", + "22": "22", + "220": "220", + "221": "221", + "222": "222", + "223": "223", + "224": "224", + "225": "225", + "226": "226", + "227": "227", + "228": "228", + "229": "229", + "23": "23", + "230": "230", + "231": "231", + "232": "232", + "233": "233", + "234": "234", + "235": "235", + "236": "236", + "237": "237", + "238": "238", + "239": "239", + "24": "24", + "240": "240", + "241": "241", + "242": "242", + "243": "243", + "244": "244", + "245": "245", + "246": "246", + "247": "247", + "248": "248", + "249": "249", + "25": "25", + "250": "250", + "251": "251", + "252": "252", + "253": "253", + "254": "254", + "255": "255", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "31": "31", + "32": "32", + "33": "33", + "34": "34", + "35": "35", + "36": "36", + "37": "37", + "38": "38", + "39": "39", + "4": "4", + "40": "40", + "41": "41", + "42": "42", + "43": "43", + "44": "44", + "45": "45", + "46": "46", + "47": "47", + "48": "48", + "49": "49", + "5": "5", + "50": "50", + "51": "51", + "52": "52", + "53": "53", + "54": "54", + "55": "55", + "56": "56", + "57": "57", + "58": "58", + "59": "59", + "6": "6", + "60": "60", + "61": "61", + "62": "62", + "63": "63", + "64": "64", + "65": "65", + "66": "66", + "67": "67", + "68": "68", + "69": "69", + "7": "7", + "70": "70", + "71": "71", + "72": "72", + "73": "73", + "74": "74", + "75": "75", + "76": "76", + "77": "77", + "78": "78", + "79": "79", + "8": "8", + "80": "80", + "81": "81", + "82": "82", + "83": "83", + "84": "84", + "85": "85", + "86": "86", + "87": "87", + "88": "88", + "89": "89", + "9": "9", + "90": "90", + "91": "91", + "92": "92", + "93": "93", + "94": "94", + "95": "95", + "96": "96", + "97": "97", + "98": "98", "99": "99" } - ], + ], "3012": [ - "Tandem Leader Slot", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "2": "2", - "22": "None", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Tandem Leader Slot", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "2": "2", + "22": "None", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "3013": [ - "Commander", - { - "": "Pick Commander", - "Abat": "Abathur", - "Alar": "Alarak", - "Arta": "Artanis", - "Deha": "Dehaka", - "Feni": "Fenix", - "Horn": "Horner", - "Kara": "Karax", - "Kerr": "Kerrigan", - "Meng": "Mengsk", - "Nova": "Nova", - "Rayn": "Raynor", - "Stet": "Stetmann", - "Stuk": "Stukov", - "Swan": "Swann", - "Tych": "Tychus", - "Vora": "Vorazun", - "Zaga": "Zagara", + "Commander", + { + "": "Pick Commander", + "Abat": "Abathur", + "Alar": "Alarak", + "Arta": "Artanis", + "Deha": "Dehaka", + "Feni": "Fenix", + "Horn": "Han & Horner", + "Kara": "Karax", + "Kerr": "Kerrigan", + "Meng": "Mengsk", + "Nova": "Nova", + "Rayn": "Raynor", + "Stet": "Stetmann", + "Stuk": "Stukov", + "Swan": "Swann", + "Tych": "Tychus", + "Vora": "Vorazun", + "Zaga": "Zagara", "Zera": "Zeratul" } - ], + ], "3014": [ - "Commander Level", - { - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Level", + { + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "3015": [ - "Game Duration", - { - "0": "Infinite", - "120": "120 Minutes", - "15": "15 Minutes", - "25": "25 Minutes", - "30": "30 Minutes", - "45": "45 Minutes", - "5": "5 Minutes", - "60": "60 Minutes", + "Game Duration", + { + "0": "Infinite", + "120": "120 Minutes", + "15": "15 Minutes", + "25": "25 Minutes", + "30": "30 Minutes", + "45": "45 Minutes", + "5": "5 Minutes", + "60": "60 Minutes", "90": "90 Minutes" } - ], + ], + "3016": [ + "Commander Mastery Level", + { + "0": "0", + "1": "1", + "10": "10", + "100": "100", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "31": "31", + "32": "32", + "33": "33", + "34": "34", + "35": "35", + "36": "36", + "37": "37", + "38": "38", + "39": "39", + "4": "4", + "40": "40", + "41": "41", + "42": "42", + "43": "43", + "44": "44", + "45": "45", + "46": "46", + "47": "47", + "48": "48", + "49": "49", + "5": "5", + "50": "50", + "51": "51", + "52": "52", + "53": "53", + "54": "54", + "55": "55", + "56": "56", + "57": "57", + "58": "58", + "59": "59", + "6": "6", + "60": "60", + "61": "61", + "62": "62", + "63": "63", + "64": "64", + "65": "65", + "66": "66", + "67": "67", + "68": "68", + "69": "69", + "7": "7", + "70": "70", + "71": "71", + "72": "72", + "73": "73", + "74": "74", + "75": "75", + "76": "76", + "77": "77", + "78": "78", + "79": "79", + "8": "8", + "80": "80", + "81": "81", + "82": "82", + "83": "83", + "84": "84", + "85": "85", + "86": "86", + "87": "87", + "88": "88", + "89": "89", + "9": "9", + "90": "90", + "91": "91", + "92": "92", + "93": "93", + "94": "94", + "95": "95", + "96": "96", + "97": "97", + "98": "98", + "99": "99" + } + ], + "3017": [ + "Commander Mastery Tier", + { + "0": "0", + "1": "1", + "10": "10", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "3019": [ + "Random Commander", + { + "no": "Not Random", + "yes": "Random" + } + ], + "3020": [ + "Commander Is Trial", + { + "no": "Not Trial", + "yes": "Trial" + } + ], "3102": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3103": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3104": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3105": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3106": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3107": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3108": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3109": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3110": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3111": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3134": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3135": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3136": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3137": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3138": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3139": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3140": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3141": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3142": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3143": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3166": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3167": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3168": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3169": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3170": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3171": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3172": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3173": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3174": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3175": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3198": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3199": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3200": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3201": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3202": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3203": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3204": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3205": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3206": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3207": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "4000": [ - "Game Privacy", + "Game Privacy", { - "NoBO": "No Build Order", - "NoMH": "No Match History", + "NoBO": "No Build Order", + "NoMH": "No Match History", "Norm": "Normal" } - ], + ], "4001": [ - "Using Custom Observer UI", + "Using Custom Observer UI", { - "no": "Not Using Custom Observer UI", + "no": "Not Using Custom Observer UI", "yes": "Using Custom Observer UI" } - ], + ], "4005": [ - "Ready", + "Ready", { - "no": "Not Ready", + "no": "Not Ready", "yes": "Ready" } + ], + "5000": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5001": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5002": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5003": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5004": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5005": [ + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", + "9": "9" + } + ], + "5100": [ + "Brutal Plus Level", + { + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6" + } + ], + "5200": [ + "Brutal Plus Is Retry", + { + "no": "No", + "yes": "Yes" + } + ], + "5300": [ + "Commander Prestige Level", + { + "0": "0", + "1": "1", + "2": "2", + "3": "3" + } ] - }, - "decisions": "(dp0\nc__builtin__\nfrozenset\np1\n((lp2\nS'Hard'\np3\naVHarder\np4\na(I3004\nS'Hard'\np5\ntp6\natp7\nRp8\ng4\nsg1\n((lp9\n(I2001\nS'1v1'\np10\ntp11\naS'1 v 1'\np12\naV1v1\np13\natp14\nRp15\ng13\nsg1\n((lp16\n(I3104\nS'AB04'\np17\ntp18\naS'Agressive Push'\np19\naVAggressive Push\np20\natp21\nRp22\ng20\nsg1\n((lp23\nS'Agressive Push'\np24\naVAggressive Push\np25\na(I3199\nS'AB04'\np26\ntp27\natp28\nRp29\ng25\nsg1\n((lp30\nV6v6\np31\naS'6 v 6'\np32\na(I2001\nS'6v6'\np33\ntp34\natp35\nRp36\ng31\nsg1\n((lp37\nS'Agressive Push'\np38\na(I3102\nS'AB04'\np39\ntp40\naVAggressive Push\np41\natp42\nRp43\ng41\nsg1\n((lp44\nI2003\naVTeams2v2\np45\naS'Team'\np46\natp47\nRp48\ng45\nsg1\n((lp49\nVLadder\np50\naS'Automated Match Making'\np51\na(I3009\nS'Amm'\np52\ntp53\natp54\nRp55\ng50\nsg1\n((lp56\n(I2001\nS'5v5'\np57\ntp58\naS'5 v 5'\np59\naV5v5\np60\natp61\nRp62\ng60\nsg1\n((lp63\nVFree For All Teams\np64\naS'Free For All Archon'\np65\na(I2000\nVFFAT\np66\ntp67\natp68\nRp69\ng65\nsg1\n((lp70\nI3141\naVAI Build (Terran)\np71\naS'AI Build'\np72\natp73\nRp74\ng71\nsg1\n((lp75\n(I2001\nS'3v3'\np76\ntp77\naS'3 v 3'\np78\naV3v3\np79\natp80\nRp81\ng79\nsg1\n((lp82\n(I3168\nS'AB04'\np83\ntp84\naS'Agressive Push'\np85\naVAggressive Push\np86\natp87\nRp88\ng86\nsg1\n((lp89\n(I3200\nS'AB04'\np90\ntp91\naS'Agressive Push'\np92\naVAggressive Push\np93\natp94\nRp95\ng93\nsg1\n((lp96\nVAI Build (Protoss)\np97\naI3174\naS'AI Build'\np98\natp99\nRp100\ng97\nsg1\n((lp101\nS'Very Hard'\np102\naVElite\np103\na(I3004\nS'VyHd'\np104\ntp105\natp106\nRp107\ng103\nsg1\n((lp108\nS'Agressive Push'\np109\naVAggressive Push\np110\na(I3167\nS'AB04'\np111\ntp112\natp113\nRp114\ng110\nsg1\n((lp115\nI3204\naS'AI Build'\np116\naVAI Build (Zerg)\np117\natp118\nRp119\ng117\nsg1\n((lp120\nVInsane\np121\naS'Cheater 3 (Insane)'\np122\na(I3004\nS'Insa'\np123\ntp124\natp125\nRp126\ng121\nsg1\n((lp127\n(I3007\nS'Watc'\np128\ntp129\naS'Observer'\np130\naS'Watcher'\np131\natp132\nRp133\ng130\nsg1\n((lp134\nI3205\naVAI Build (Zerg)\np135\naS'AI Build'\np136\natp137\nRp138\ng135\nsg1\n((lp139\nVTeams5v5\np140\naS'Team'\np141\naI2007\natp142\nRp143\ng140\nsg1\n((lp144\nI3171\naVAI Build (Protoss)\np145\naS'AI Build'\np146\natp147\nRp148\ng145\nsg1\n((lp149\nS'Unknown'\np150\naI2012\naS'Team'\np151\natp152\nRp153\ng151\nsg1\n((lp154\nI3173\naS'AI Build'\np155\naVAI Build (Protoss)\np156\natp157\nRp158\ng156\nsg1\n((lp159\nVAI Build (Terran)\np160\naI3142\naS'AI Build'\np161\natp162\nRp163\ng160\nsg1\n((lp164\nI3172\naVAI Build (Protoss)\np165\naS'AI Build'\np166\natp167\nRp168\ng165\nsg1\n((lp169\nS'Level 1 (Very Easy)'\np170\na(I3004\nS'VyEy'\np171\ntp172\naVVery Easy\np173\natp174\nRp175\ng173\nsg1\n((lp176\nS'Agressive Push'\np177\naVAggressive Push\np178\na(I3135\nS'AB04'\np179\ntp180\natp181\nRp182\ng178\nsg1\n((lp183\nV2v2\np184\naS'2 v 2'\np185\na(I2001\nS'2v2'\np186\ntp187\natp188\nRp189\ng184\nsg1\n((lp190\nS'Agressive Push'\np191\na(I3166\nS'AB04'\np192\ntp193\naVAggressive Push\np194\natp195\nRp196\ng194\nsg1\n((lp197\nVTeamsFFA\np198\naI2006\naS'Team'\np199\natp200\nRp201\ng198\nsg1\n((lp202\nS'AI Build'\np203\naVAI Build (Terran)\np204\naI3143\natp205\nRp206\ng204\nsg1\n((lp207\nVTeams7v7\np208\naI2011\naS'Team'\np209\natp210\nRp211\ng208\nsg1\n((lp212\nVMedium\np213\naS'Level 3 (Medium)'\np214\na(I3004\nS'Medi'\np215\ntp216\natp217\nRp218\ng213\nsg1\n((lp219\nI3140\naS'AI Build'\np220\naVAI Build (Terran)\np221\natp222\nRp223\ng221\nsg1\n((lp224\nVTeams4v4\np225\naI2005\naS'Team'\np226\natp227\nRp228\ng225\nsg1\n((lp229\nS'Agressive Push'\np230\na(I3198\nS'AB04'\np231\ntp232\naVAggressive Push\np233\natp234\nRp235\ng233\nsg1\n((lp236\n(I3136\nS'AB04'\np237\ntp238\naS'Agressive Push'\np239\naVAggressive Push\np240\natp241\nRp242\ng240\nsg1\n((lp243\nI2008\naVTeams6v6\np244\naS'Team'\np245\natp246\nRp247\ng244\nsg1\n((lp248\nS'Agressive Push'\np249\naVAggressive Push\np250\na(I3103\nS'AB04'\np251\ntp252\natp253\nRp254\ng250\nsg1\n((lp255\nV4v4\np256\naS'4 v 4'\np257\na(I2001\nS'4v4'\np258\ntp259\natp260\nRp261\ng256\nsg1\n((lp262\nS'Agressive Push'\np263\na(I3134\nS'AB04'\np264\ntp265\naVAggressive Push\np266\natp267\nRp268\ng266\nsg1\n((lp269\nVTeams1v1\np270\naI2002\naS'Team'\np271\natp272\nRp273\ng270\nsg1\n((lp274\nI3139\naS'AI Build'\np275\naVAI Build (Terran)\np276\natp277\nRp278\ng276\nsg1\n((lp279\nS'AI Build'\np280\naVAI Build (Zerg)\np281\naI3207\natp282\nRp283\ng281\nsg1\n((lp284\n(I2001\nS'FFA'\np285\ntp286\naS'Free For All'\np287\naVFFA\np288\natp289\nRp290\ng288\nsg1\n((lp291\nVAI Build (Zerg)\np292\naI3206\naS'AI Build'\np293\natp294\nRp295\ng292\nsg1\n((lp296\nVTeams3v3\np297\naI2004\naS'Team'\np298\natp299\nRp300\ng297\nsg1\n((lp301\nVAI Build (Protoss)\np302\naS'AI Build'\np303\naI3175\natp304\nRp305\ng302\nsg1\n((lp306\nS'Level 2 (Easy)'\np307\na(I3004\nS'Easy'\np308\ntp309\naVEasy\np310\natp311\nRp312\ng310\nsg1\n((lp313\nI3203\naS'AI Build'\np314\naVAI Build (Zerg)\np315\natp316\nRp317\ng315\ns." -} + }, + "decisions": "(dp0\nc__builtin__\nfrozenset\np1\n((lp2\nS'Hard'\np3\naVHarder\np4\na(I3004\nS'Hard'\np5\ntp6\natp7\nRp8\ng4\nsg1\n((lp9\n(I2001\nS'1v1'\np10\ntp11\naS'1 v 1'\np12\naV1v1\np13\natp14\nRp15\ng13\nsg1\n((lp16\n(I3104\nS'AB04'\np17\ntp18\naS'Agressive Push'\np19\naVAggressive Push\np20\natp21\nRp22\ng20\nsg1\n((lp23\nS'Agressive Push'\np24\naVAggressive Push\np25\na(I3199\nS'AB04'\np26\ntp27\natp28\nRp29\ng25\nsg1\n((lp30\nV6v6\np31\naS'6 v 6'\np32\na(I2001\nS'6v6'\np33\ntp34\natp35\nRp36\ng31\nsg1\n((lp37\nS'Agressive Push'\np38\na(I3102\nS'AB04'\np39\ntp40\naVAggressive Push\np41\natp42\nRp43\ng41\nsg1\n((lp44\nI2003\naVTeams2v2\np45\naS'Team'\np46\natp47\nRp48\ng45\nsg1\n((lp49\nVLadder\np50\naS'Automated Match Making'\np51\na(I3009\nS'Amm'\np52\ntp53\natp54\nRp55\ng50\nsg1\n((lp56\n(I2001\nS'5v5'\np57\ntp58\naS'5 v 5'\np59\naV5v5\np60\natp61\nRp62\ng60\nsg1\n((lp63\nVFree For All Teams\np64\naS'Free For All Archon'\np65\na(I2000\nVFFAT\np66\ntp67\natp68\nRp69\ng65\nsg1\n((lp70\nVHorner\np71\na(I3013\nVHorn\np72\ntp73\naS'Han & Horner'\np74\natp75\nRp76\ng74\nsg1\n((lp77\nI3141\naVAI Build (Terran)\np78\naS'AI Build'\np79\natp80\nRp81\ng78\nsg1\n((lp82\n(I2001\nS'3v3'\np83\ntp84\naS'3 v 3'\np85\naV3v3\np86\natp87\nRp88\ng86\nsg1\n((lp89\n(I3168\nS'AB04'\np90\ntp91\naS'Agressive Push'\np92\naVAggressive Push\np93\natp94\nRp95\ng93\nsg1\n((lp96\n(I3200\nS'AB04'\np97\ntp98\naS'Agressive Push'\np99\naVAggressive Push\np100\natp101\nRp102\ng100\nsg1\n((lp103\nVAI Build (Protoss)\np104\naI3174\naS'AI Build'\np105\natp106\nRp107\ng104\nsg1\n((lp108\nS'Very Hard'\np109\naVElite\np110\na(I3004\nS'VyHd'\np111\ntp112\natp113\nRp114\ng110\nsg1\n((lp115\nS'Agressive Push'\np116\naVAggressive Push\np117\na(I3167\nS'AB04'\np118\ntp119\natp120\nRp121\ng117\nsg1\n((lp122\nI3204\naS'AI Build'\np123\naVAI Build (Zerg)\np124\natp125\nRp126\ng124\nsg1\n((lp127\nVInsane\np128\naS'Cheater 3 (Insane)'\np129\na(I3004\nS'Insa'\np130\ntp131\natp132\nRp133\ng128\nsg1\n((lp134\n(I3007\nS'Watc'\np135\ntp136\naS'Observer'\np137\naS'Watcher'\np138\natp139\nRp140\ng137\nsg1\n((lp141\nI3205\naVAI Build (Zerg)\np142\naS'AI Build'\np143\natp144\nRp145\ng142\nsg1\n((lp146\nVTeams5v5\np147\naS'Team'\np148\naI2007\natp149\nRp150\ng147\nsg1\n((lp151\nI3171\naVAI Build (Protoss)\np152\naS'AI Build'\np153\natp154\nRp155\ng152\nsg1\n((lp156\nS'Unknown'\np157\naI2012\naS'Team'\np158\natp159\nRp160\ng158\nsg1\n((lp161\nI3173\naS'AI Build'\np162\naVAI Build (Protoss)\np163\natp164\nRp165\ng163\nsg1\n((lp166\nVAI Build (Terran)\np167\naI3142\naS'AI Build'\np168\natp169\nRp170\ng167\nsg1\n((lp171\nI3172\naVAI Build (Protoss)\np172\naS'AI Build'\np173\natp174\nRp175\ng172\nsg1\n((lp176\nS'Level 1 (Very Easy)'\np177\na(I3004\nS'VyEy'\np178\ntp179\naVVery Easy\np180\natp181\nRp182\ng180\nsg1\n((lp183\nS'Agressive Push'\np184\naVAggressive Push\np185\na(I3135\nS'AB04'\np186\ntp187\natp188\nRp189\ng185\nsg1\n((lp190\nV2v2\np191\naS'2 v 2'\np192\na(I2001\nS'2v2'\np193\ntp194\natp195\nRp196\ng191\nsg1\n((lp197\nS'Agressive Push'\np198\na(I3166\nS'AB04'\np199\ntp200\naVAggressive Push\np201\natp202\nRp203\ng201\nsg1\n((lp204\nVTeamsFFA\np205\naI2006\naS'Team'\np206\natp207\nRp208\ng205\nsg1\n((lp209\nS'AI Build'\np210\naVAI Build (Terran)\np211\naI3143\natp212\nRp213\ng211\nsg1\n((lp214\nVTeams7v7\np215\naI2011\naS'Team'\np216\natp217\nRp218\ng215\nsg1\n((lp219\nVMedium\np220\naS'Level 3 (Medium)'\np221\na(I3004\nS'Medi'\np222\ntp223\natp224\nRp225\ng220\nsg1\n((lp226\nI3140\naS'AI Build'\np227\naVAI Build (Terran)\np228\natp229\nRp230\ng228\nsg1\n((lp231\nVTeams4v4\np232\naI2005\naS'Team'\np233\natp234\nRp235\ng232\nsg1\n((lp236\nS'Agressive Push'\np237\na(I3198\nS'AB04'\np238\ntp239\naVAggressive Push\np240\natp241\nRp242\ng240\nsg1\n((lp243\n(I3136\nS'AB04'\np244\ntp245\naS'Agressive Push'\np246\naVAggressive Push\np247\natp248\nRp249\ng247\nsg1\n((lp250\nI2008\naVTeams6v6\np251\naS'Team'\np252\natp253\nRp254\ng251\nsg1\n((lp255\nS'Agressive Push'\np256\naVAggressive Push\np257\na(I3103\nS'AB04'\np258\ntp259\natp260\nRp261\ng257\nsg1\n((lp262\nV4v4\np263\naS'4 v 4'\np264\na(I2001\nS'4v4'\np265\ntp266\natp267\nRp268\ng263\nsg1\n((lp269\nS'Agressive Push'\np270\na(I3134\nS'AB04'\np271\ntp272\naVAggressive Push\np273\natp274\nRp275\ng273\nsg1\n((lp276\nVTeams1v1\np277\naI2002\naS'Team'\np278\natp279\nRp280\ng277\nsg1\n((lp281\nI3139\naS'AI Build'\np282\naVAI Build (Terran)\np283\natp284\nRp285\ng283\nsg1\n((lp286\nS'AI Build'\np287\naVAI Build (Zerg)\np288\naI3207\natp289\nRp290\ng288\nsg1\n((lp291\n(I2001\nS'FFA'\np292\ntp293\naS'Free For All'\np294\naVFFA\np295\natp296\nRp297\ng295\nsg1\n((lp298\nVAI Build (Zerg)\np299\naI3206\naS'AI Build'\np300\natp301\nRp302\ng299\nsg1\n((lp303\nVTeams3v3\np304\naI2004\naS'Team'\np305\natp306\nRp307\ng304\nsg1\n((lp308\nVAI Build (Protoss)\np309\naS'AI Build'\np310\naI3175\natp311\nRp312\ng309\nsg1\n((lp313\nS'Level 2 (Easy)'\np314\na(I3004\nS'Easy'\np315\ntp316\naVEasy\np317\natp318\nRp319\ng317\nsg1\n((lp320\nI3203\naS'AI Build'\np321\naVAI Build (Zerg)\np322\natp323\nRp324\ng322\ns." +} \ No newline at end of file From c9338cf6125ad4b545287a06ccaecd9998ef8065 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 28 Sep 2020 22:01:37 -0700 Subject: [PATCH 066/136] handle missing value --- sc2reader/objects.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index d9e53035..3867896d 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -77,7 +77,11 @@ def __init__(self, header, attr_id, player, value): self.value = None else: self.name, lookup = LOBBY_PROPERTIES[self.id] - self.value = lookup[value.strip("\x00 ")[::-1]] + try: + self.value = lookup[value.strip("\x00 ")[::-1]] + except KeyError: + self.logger.info("Missing attribute value: {0}".format(value)) + self.value = None def __repr__(self): return str(self) From 2f4c89a642cb584a919f8a826c6b4037c85f52a6 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Tue, 29 Sep 2020 08:27:58 -0700 Subject: [PATCH 067/136] more co-op data --- sc2reader/data/attributes.json | 3214 ++++++++++++++++---------------- sc2reader/resources.py | 16 +- 2 files changed, 1621 insertions(+), 1609 deletions(-) diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index 3b9a92a6..7922c1cc 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -1,1956 +1,1960 @@ { "attributes": { "0500": [ - "Controller", + "Controller", { - "Clsd": "Closed", - "Comp": "Computer", - "Humn": "User", + "Clsd": "Closed", + "Comp": "Computer", + "Humn": "User", "Open": "Open" } - ], + ], "1000": [ - "Rules", + "Rules", { "Dflt": "Default" } - ], + ], "1001": [ - "Premade Game", + "Premade Game", { - "no": "No", + "no": "No", "yes": "Yes" } - ], + ], "2000": [ - "Teams", - { - "CuTa": "Custom Teams Archon", - "Cust": "Custom Teams", - "FFA": "Free For All", - "FFAT": "Free For All Archon", - "t1": "1 Team", - "t10": "10 Teams", - "t11": "11 Teams", - "t2": "2 Teams", - "t3": "3 Teams", - "t4": "4 Teams", - "t5": "5 Teams", - "t6": "6 Teams", - "t7": "7 Teams", - "t8": "8 Teams", + "Teams", + { + "CuTa": "Custom Teams Archon", + "Cust": "Custom Teams", + "FFA": "Free For All", + "FFAT": "Free For All Archon", + "t1": "1 Team", + "t10": "10 Teams", + "t11": "11 Teams", + "t2": "2 Teams", + "t3": "3 Teams", + "t4": "4 Teams", + "t5": "5 Teams", + "t6": "6 Teams", + "t7": "7 Teams", + "t8": "8 Teams", "t9": "9 Teams" } - ], + ], "2001": [ - "Teams", - { - "1v1": "1v1", - "2v2": "2v2", - "3v3": "3v3", - "4v4": "4v4", - "5v5": "5v5", - "6v6": "6v6", + "Teams", + { + "1v1": "1v1", + "2v2": "2v2", + "3v3": "3v3", + "4v4": "4v4", + "5v5": "5v5", + "6v6": "6v6", "FFA": "FFA" } - ], + ], "2002": [ - "Teams1v1", + "Teams1v1", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2003": [ - "Teams2v2", + "Teams2v2", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2004": [ - "Teams3v3", + "Teams3v3", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2005": [ - "Teams4v4", + "Teams4v4", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2006": [ - "TeamsFFA", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "TeamsFFA", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2007": [ - "Teams5v5", + "Teams5v5", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2008": [ - "Teams6v6", + "Teams6v6", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2010": [ - "Team", + "Team", { "T1": "Team 1" } - ], + ], "2011": [ - "Teams7v7", + "Teams7v7", { - "T1": "Team 1", + "T1": "Team 1", "T2": "Team 2" } - ], + ], "2012": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", + "T1": "Team 1", + "T2": "Team 2", "T3": "Team 3" } - ], + ], "2013": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", "T4": "Team 4" } - ], + ], "2014": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", "T5": "Team 5" } - ], + ], "2015": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", "T6": "Team 6" } - ], + ], "2016": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", "T7": "Team 7" } - ], + ], "2017": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2018": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T12": "Team 12", - "T13": "Team 13", - "T14": "Team 14", - "T15": "Team 15", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T12": "Team 12", + "T13": "Team 13", + "T14": "Team 14", + "T15": "Team 15", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2019": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", "T8": "Team 8" } - ], + ], "2020": [ - "Team", - { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2021": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2022": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "2023": [ - "Team", + "Team", { - "T1": "Team 1", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", + "T1": "Team 1", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", "T6": "Team 6" } - ], + ], "2024": [ - "Team", - { - "T1": "Team 1", - "T10": "Team 10", - "T11": "Team 11", - "T2": "Team 2", - "T3": "Team 3", - "T4": "Team 4", - "T5": "Team 5", - "T6": "Team 6", - "T7": "Team 7", - "T8": "Team 8", + "Team", + { + "T1": "Team 1", + "T10": "Team 10", + "T11": "Team 11", + "T2": "Team 2", + "T3": "Team 3", + "T4": "Team 4", + "T5": "Team 5", + "T6": "Team 6", + "T7": "Team 7", + "T8": "Team 8", "T9": "Team 9" } - ], + ], "3000": [ - "Game Speed", + "Game Speed", { - "Fasr": "Faster", - "Fast": "Fast", - "Norm": "Normal", - "Slor": "Slower", + "Fasr": "Faster", + "Fast": "Fast", + "Norm": "Normal", + "Slor": "Slower", "Slow": "Slow" } - ], + ], "3001": [ - "Race", - { - "Prot": "Protoss", - "RAND": "Random", - "Terr": "Terran", + "Race", + { + "InfT": "Infested Terran", + "PZrg": "Primal Zerg", + "Prot": "Protoss", + "RAND": "Random", + "TerH": "Terran Horner", + "TerT": "Terran Tychus", + "Terr": "Terran", "Zerg": "Zerg" } - ], + ], "3002": [ - "Color", - { - "tc01": "Red", - "tc02": "Blue", - "tc03": "Teal", - "tc04": "Purple", - "tc05": "Yellow", - "tc06": "Orange", - "tc07": "Green", - "tc08": "Light Pink", - "tc09": "Violet", - "tc10": "Light Grey", - "tc11": "Dark Green", - "tc12": "Brown", - "tc13": "Light Green", - "tc14": "Dark Grey", - "tc15": "Pink", + "Color", + { + "tc01": "Red", + "tc02": "Blue", + "tc03": "Teal", + "tc04": "Purple", + "tc05": "Yellow", + "tc06": "Orange", + "tc07": "Green", + "tc08": "Light Pink", + "tc09": "Violet", + "tc10": "Light Grey", + "tc11": "Dark Green", + "tc12": "Brown", + "tc13": "Light Green", + "tc14": "Dark Grey", + "tc15": "Pink", "tc16": "??" } - ], + ], "3003": [ - "Handicap", + "Handicap", { - "100": "100%", - "50": "50%", - "60": "60%", - "70": "70%", - "80": "80%", + "100": "100%", + "50": "50%", + "60": "60%", + "70": "70%", + "80": "80%", "90": "90%" } - ], + ], "3004": [ - "Difficulty", - { - "ChRe": "Cheater 2 (Resources)", - "ChVi": "Cheater 1 (Vision)", - "Easy": "Easy", - "Hard": "Harder", - "HdVH": "Very Hard", - "Insa": "Insane", - "MdHd": "Hard", - "Medi": "Medium", - "VyEy": "Very Easy", + "Difficulty", + { + "ChRe": "Cheater 2 (Resources)", + "ChVi": "Cheater 1 (Vision)", + "Easy": "Easy", + "Hard": "Harder", + "HdVH": "Very Hard", + "Insa": "Insane", + "MdHd": "Hard", + "Medi": "Medium", + "VyEy": "Very Easy", "VyHd": "Elite" } - ], + ], "3006": [ - "Lobby Delay", - { - "10": "10", - "15": "15", - "20": "20", - "25": "25", - "3": "3", - "30": "30", - "5": "5", + "Lobby Delay", + { + "10": "10", + "15": "15", + "20": "20", + "25": "25", + "3": "3", + "30": "30", + "5": "5", "7": "7" } - ], + ], "3007": [ - "Participant Role", + "Participant Role", { - "Part": "Participant", + "Part": "Participant", "Watc": "Observer" } - ], + ], "3008": [ - "Observer Type", + "Observer Type", { - "Obs": "Spectator", + "Obs": "Spectator", "Ref": "Referee" } - ], + ], "3009": [ - "Game Mode", + "Game Mode", { - "": "Single Player", - "Amm": "Ladder", - "Priv": "Private", + "": "Single Player", + "Amm": "Ladder", + "Priv": "Private", "Pub": "Public" } - ], + ], "3010": [ - "Locked Alliances", + "Locked Alliances", { - "no": "No", + "no": "No", "yes": "Yes" } - ], + ], "3011": [ - "Player Logo Index", - { - "0": "0", - "1": "1", - "10": "10", - "100": "100", - "101": "101", - "102": "102", - "103": "103", - "104": "104", - "105": "105", - "106": "106", - "107": "107", - "108": "108", - "109": "109", - "11": "11", - "110": "110", - "111": "111", - "112": "112", - "113": "113", - "114": "114", - "115": "115", - "116": "116", - "117": "117", - "118": "118", - "119": "119", - "12": "12", - "120": "120", - "121": "121", - "122": "122", - "123": "123", - "124": "124", - "125": "125", - "126": "126", - "127": "127", - "128": "128", - "129": "129", - "13": "13", - "130": "130", - "131": "131", - "132": "132", - "133": "133", - "134": "134", - "135": "135", - "136": "136", - "137": "137", - "138": "138", - "139": "139", - "14": "14", - "140": "140", - "141": "141", - "142": "142", - "143": "143", - "144": "144", - "145": "145", - "146": "146", - "147": "147", - "148": "148", - "149": "149", - "15": "15", - "150": "150", - "151": "151", - "152": "152", - "153": "153", - "154": "154", - "155": "155", - "156": "156", - "157": "157", - "158": "158", - "159": "159", - "16": "16", - "160": "160", - "161": "161", - "162": "162", - "163": "163", - "164": "164", - "165": "165", - "166": "166", - "167": "167", - "168": "168", - "169": "169", - "17": "17", - "170": "170", - "171": "171", - "172": "172", - "173": "173", - "174": "174", - "175": "175", - "176": "176", - "177": "177", - "178": "178", - "179": "179", - "18": "18", - "180": "180", - "181": "181", - "182": "182", - "183": "183", - "184": "184", - "185": "185", - "186": "186", - "187": "187", - "188": "188", - "189": "189", - "19": "19", - "190": "190", - "191": "191", - "192": "192", - "193": "193", - "194": "194", - "195": "195", - "196": "196", - "197": "197", - "198": "198", - "199": "199", - "2": "2", - "20": "20", - "200": "200", - "201": "201", - "202": "202", - "203": "203", - "204": "204", - "205": "205", - "206": "206", - "207": "207", - "208": "208", - "209": "209", - "21": "21", - "210": "210", - "211": "211", - "212": "212", - "213": "213", - "214": "214", - "215": "215", - "216": "216", - "217": "217", - "218": "218", - "219": "219", - "22": "22", - "220": "220", - "221": "221", - "222": "222", - "223": "223", - "224": "224", - "225": "225", - "226": "226", - "227": "227", - "228": "228", - "229": "229", - "23": "23", - "230": "230", - "231": "231", - "232": "232", - "233": "233", - "234": "234", - "235": "235", - "236": "236", - "237": "237", - "238": "238", - "239": "239", - "24": "24", - "240": "240", - "241": "241", - "242": "242", - "243": "243", - "244": "244", - "245": "245", - "246": "246", - "247": "247", - "248": "248", - "249": "249", - "25": "25", - "250": "250", - "251": "251", - "252": "252", - "253": "253", - "254": "254", - "255": "255", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "31": "31", - "32": "32", - "33": "33", - "34": "34", - "35": "35", - "36": "36", - "37": "37", - "38": "38", - "39": "39", - "4": "4", - "40": "40", - "41": "41", - "42": "42", - "43": "43", - "44": "44", - "45": "45", - "46": "46", - "47": "47", - "48": "48", - "49": "49", - "5": "5", - "50": "50", - "51": "51", - "52": "52", - "53": "53", - "54": "54", - "55": "55", - "56": "56", - "57": "57", - "58": "58", - "59": "59", - "6": "6", - "60": "60", - "61": "61", - "62": "62", - "63": "63", - "64": "64", - "65": "65", - "66": "66", - "67": "67", - "68": "68", - "69": "69", - "7": "7", - "70": "70", - "71": "71", - "72": "72", - "73": "73", - "74": "74", - "75": "75", - "76": "76", - "77": "77", - "78": "78", - "79": "79", - "8": "8", - "80": "80", - "81": "81", - "82": "82", - "83": "83", - "84": "84", - "85": "85", - "86": "86", - "87": "87", - "88": "88", - "89": "89", - "9": "9", - "90": "90", - "91": "91", - "92": "92", - "93": "93", - "94": "94", - "95": "95", - "96": "96", - "97": "97", - "98": "98", + "Player Logo Index", + { + "0": "0", + "1": "1", + "10": "10", + "100": "100", + "101": "101", + "102": "102", + "103": "103", + "104": "104", + "105": "105", + "106": "106", + "107": "107", + "108": "108", + "109": "109", + "11": "11", + "110": "110", + "111": "111", + "112": "112", + "113": "113", + "114": "114", + "115": "115", + "116": "116", + "117": "117", + "118": "118", + "119": "119", + "12": "12", + "120": "120", + "121": "121", + "122": "122", + "123": "123", + "124": "124", + "125": "125", + "126": "126", + "127": "127", + "128": "128", + "129": "129", + "13": "13", + "130": "130", + "131": "131", + "132": "132", + "133": "133", + "134": "134", + "135": "135", + "136": "136", + "137": "137", + "138": "138", + "139": "139", + "14": "14", + "140": "140", + "141": "141", + "142": "142", + "143": "143", + "144": "144", + "145": "145", + "146": "146", + "147": "147", + "148": "148", + "149": "149", + "15": "15", + "150": "150", + "151": "151", + "152": "152", + "153": "153", + "154": "154", + "155": "155", + "156": "156", + "157": "157", + "158": "158", + "159": "159", + "16": "16", + "160": "160", + "161": "161", + "162": "162", + "163": "163", + "164": "164", + "165": "165", + "166": "166", + "167": "167", + "168": "168", + "169": "169", + "17": "17", + "170": "170", + "171": "171", + "172": "172", + "173": "173", + "174": "174", + "175": "175", + "176": "176", + "177": "177", + "178": "178", + "179": "179", + "18": "18", + "180": "180", + "181": "181", + "182": "182", + "183": "183", + "184": "184", + "185": "185", + "186": "186", + "187": "187", + "188": "188", + "189": "189", + "19": "19", + "190": "190", + "191": "191", + "192": "192", + "193": "193", + "194": "194", + "195": "195", + "196": "196", + "197": "197", + "198": "198", + "199": "199", + "2": "2", + "20": "20", + "200": "200", + "201": "201", + "202": "202", + "203": "203", + "204": "204", + "205": "205", + "206": "206", + "207": "207", + "208": "208", + "209": "209", + "21": "21", + "210": "210", + "211": "211", + "212": "212", + "213": "213", + "214": "214", + "215": "215", + "216": "216", + "217": "217", + "218": "218", + "219": "219", + "22": "22", + "220": "220", + "221": "221", + "222": "222", + "223": "223", + "224": "224", + "225": "225", + "226": "226", + "227": "227", + "228": "228", + "229": "229", + "23": "23", + "230": "230", + "231": "231", + "232": "232", + "233": "233", + "234": "234", + "235": "235", + "236": "236", + "237": "237", + "238": "238", + "239": "239", + "24": "24", + "240": "240", + "241": "241", + "242": "242", + "243": "243", + "244": "244", + "245": "245", + "246": "246", + "247": "247", + "248": "248", + "249": "249", + "25": "25", + "250": "250", + "251": "251", + "252": "252", + "253": "253", + "254": "254", + "255": "255", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "31": "31", + "32": "32", + "33": "33", + "34": "34", + "35": "35", + "36": "36", + "37": "37", + "38": "38", + "39": "39", + "4": "4", + "40": "40", + "41": "41", + "42": "42", + "43": "43", + "44": "44", + "45": "45", + "46": "46", + "47": "47", + "48": "48", + "49": "49", + "5": "5", + "50": "50", + "51": "51", + "52": "52", + "53": "53", + "54": "54", + "55": "55", + "56": "56", + "57": "57", + "58": "58", + "59": "59", + "6": "6", + "60": "60", + "61": "61", + "62": "62", + "63": "63", + "64": "64", + "65": "65", + "66": "66", + "67": "67", + "68": "68", + "69": "69", + "7": "7", + "70": "70", + "71": "71", + "72": "72", + "73": "73", + "74": "74", + "75": "75", + "76": "76", + "77": "77", + "78": "78", + "79": "79", + "8": "8", + "80": "80", + "81": "81", + "82": "82", + "83": "83", + "84": "84", + "85": "85", + "86": "86", + "87": "87", + "88": "88", + "89": "89", + "9": "9", + "90": "90", + "91": "91", + "92": "92", + "93": "93", + "94": "94", + "95": "95", + "96": "96", + "97": "97", + "98": "98", "99": "99" } - ], + ], "3012": [ - "Tandem Leader Slot", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "2": "2", - "22": "None", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Tandem Leader Slot", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "2": "2", + "22": "None", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "3013": [ - "Commander", - { - "": "Pick Commander", - "Abat": "Abathur", - "Alar": "Alarak", - "Arta": "Artanis", - "Deha": "Dehaka", - "Feni": "Fenix", - "Horn": "Han & Horner", - "Kara": "Karax", - "Kerr": "Kerrigan", - "Meng": "Mengsk", - "Nova": "Nova", - "Rayn": "Raynor", - "Stet": "Stetmann", - "Stuk": "Stukov", - "Swan": "Swann", - "Tych": "Tychus", - "Vora": "Vorazun", - "Zaga": "Zagara", + "Commander", + { + "": "Pick Commander", + "Abat": "Abathur", + "Alar": "Alarak", + "Arta": "Artanis", + "Deha": "Dehaka", + "Feni": "Fenix", + "Horn": "Han & Horner", + "Kara": "Karax", + "Kerr": "Kerrigan", + "Meng": "Mengsk", + "Nova": "Nova", + "Rayn": "Raynor", + "Stet": "Stetmann", + "Stuk": "Stukov", + "Swan": "Swann", + "Tych": "Tychus", + "Vora": "Vorazun", + "Zaga": "Zagara", "Zera": "Zeratul" } - ], + ], "3014": [ - "Commander Level", - { - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Level", + { + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "3015": [ - "Game Duration", - { - "0": "Infinite", - "120": "120 Minutes", - "15": "15 Minutes", - "25": "25 Minutes", - "30": "30 Minutes", - "45": "45 Minutes", - "5": "5 Minutes", - "60": "60 Minutes", + "Game Duration", + { + "0": "Infinite", + "120": "120 Minutes", + "15": "15 Minutes", + "25": "25 Minutes", + "30": "30 Minutes", + "45": "45 Minutes", + "5": "5 Minutes", + "60": "60 Minutes", "90": "90 Minutes" } - ], + ], "3016": [ - "Commander Mastery Level", - { - "0": "0", - "1": "1", - "10": "10", - "100": "100", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "31": "31", - "32": "32", - "33": "33", - "34": "34", - "35": "35", - "36": "36", - "37": "37", - "38": "38", - "39": "39", - "4": "4", - "40": "40", - "41": "41", - "42": "42", - "43": "43", - "44": "44", - "45": "45", - "46": "46", - "47": "47", - "48": "48", - "49": "49", - "5": "5", - "50": "50", - "51": "51", - "52": "52", - "53": "53", - "54": "54", - "55": "55", - "56": "56", - "57": "57", - "58": "58", - "59": "59", - "6": "6", - "60": "60", - "61": "61", - "62": "62", - "63": "63", - "64": "64", - "65": "65", - "66": "66", - "67": "67", - "68": "68", - "69": "69", - "7": "7", - "70": "70", - "71": "71", - "72": "72", - "73": "73", - "74": "74", - "75": "75", - "76": "76", - "77": "77", - "78": "78", - "79": "79", - "8": "8", - "80": "80", - "81": "81", - "82": "82", - "83": "83", - "84": "84", - "85": "85", - "86": "86", - "87": "87", - "88": "88", - "89": "89", - "9": "9", - "90": "90", - "91": "91", - "92": "92", - "93": "93", - "94": "94", - "95": "95", - "96": "96", - "97": "97", - "98": "98", + "Commander Mastery Level", + { + "0": "0", + "1": "1", + "10": "10", + "100": "100", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "31": "31", + "32": "32", + "33": "33", + "34": "34", + "35": "35", + "36": "36", + "37": "37", + "38": "38", + "39": "39", + "4": "4", + "40": "40", + "41": "41", + "42": "42", + "43": "43", + "44": "44", + "45": "45", + "46": "46", + "47": "47", + "48": "48", + "49": "49", + "5": "5", + "50": "50", + "51": "51", + "52": "52", + "53": "53", + "54": "54", + "55": "55", + "56": "56", + "57": "57", + "58": "58", + "59": "59", + "6": "6", + "60": "60", + "61": "61", + "62": "62", + "63": "63", + "64": "64", + "65": "65", + "66": "66", + "67": "67", + "68": "68", + "69": "69", + "7": "7", + "70": "70", + "71": "71", + "72": "72", + "73": "73", + "74": "74", + "75": "75", + "76": "76", + "77": "77", + "78": "78", + "79": "79", + "8": "8", + "80": "80", + "81": "81", + "82": "82", + "83": "83", + "84": "84", + "85": "85", + "86": "86", + "87": "87", + "88": "88", + "89": "89", + "9": "9", + "90": "90", + "91": "91", + "92": "92", + "93": "93", + "94": "94", + "95": "95", + "96": "96", + "97": "97", + "98": "98", "99": "99" } - ], + ], "3017": [ - "Commander Mastery Tier", - { - "0": "0", - "1": "1", - "10": "10", - "2": "2", - "3": "3", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Tier", + { + "0": "0", + "1": "1", + "10": "10", + "2": "2", + "3": "3", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "3019": [ - "Random Commander", + "Random Commander", { - "no": "Not Random", + "no": "Not Random", "yes": "Random" } - ], + ], "3020": [ - "Commander Is Trial", + "Commander Is Trial", { - "no": "Not Trial", + "no": "Not Trial", "yes": "Trial" } - ], + ], "3102": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3103": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3104": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3105": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3106": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3107": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3108": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3109": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3110": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3111": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3134": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3135": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3136": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3137": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3138": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3139": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3140": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3141": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3142": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3143": [ - "AI Build (Terran)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB43": "MarineStim (Timing)", - "AB44": "MarauderHellion (Timing)", - "AB45": "MarineSiege (Timing)", - "AB46": "CloakBanshee (Timing)", - "AB48": "MMM (Aggressive)", - "AB49": "MarineSiege (Aggressive)", - "AB50": "SiegeBanshee (Aggressive)", - "AB51": "HellionSiege (Aggressive)", - "AB52": "SiegeThor (Aggressive)", - "AB54": "BioMMM (Economic)", - "AB55": "Mech (Economic)", - "AB56": "ThorBC (Economic)", - "T070": "MarineStim (Timing)", - "T071": "MarauderHellion (Timing)", - "T072": "MarineSiege (Timing)", - "T073": "CloakBanshee (Timing)", - "T080": "MMM (Aggressive)", - "T081": "MarineSiege (Aggressive)", - "T082": "SiegeBanshee (Aggressive)", - "T083": "HellionSiege (Aggressive)", - "T084": "SiegeThor (Aggressive)", - "T090": "BioMMM (Economic)", - "T091": "Mech (Economic)", + "AI Build (Terran)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB43": "MarineStim (Timing)", + "AB44": "MarauderHellion (Timing)", + "AB45": "MarineSiege (Timing)", + "AB46": "CloakBanshee (Timing)", + "AB48": "MMM (Aggressive)", + "AB49": "MarineSiege (Aggressive)", + "AB50": "SiegeBanshee (Aggressive)", + "AB51": "HellionSiege (Aggressive)", + "AB52": "SiegeThor (Aggressive)", + "AB54": "BioMMM (Economic)", + "AB55": "Mech (Economic)", + "AB56": "ThorBC (Economic)", + "T070": "MarineStim (Timing)", + "T071": "MarauderHellion (Timing)", + "T072": "MarineSiege (Timing)", + "T073": "CloakBanshee (Timing)", + "T080": "MMM (Aggressive)", + "T081": "MarineSiege (Aggressive)", + "T082": "SiegeBanshee (Aggressive)", + "T083": "HellionSiege (Aggressive)", + "T084": "SiegeThor (Aggressive)", + "T090": "BioMMM (Economic)", + "T091": "Mech (Economic)", "T092": "ThorBC (Economic)" } - ], + ], "3166": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3167": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3168": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3169": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3170": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3171": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3172": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3173": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3174": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3175": [ - "AI Build (Protoss)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB64": "FourGate (Timing)", - "AB65": "StalkerRobo (Timing)", - "AB66": "Blink Stalker (Timing)", - "AB67": "Dark Templar Rush (Timing)", - "AB69": "SevenGate (Aggressive)", - "AB70": "ArchonImmortal (Aggressive)", - "AB71": "Colossi (Aggressive)", - "AB72": "GatewayAir (Aggressive)", - "AB73": "VoidPhoenix (Aggressive)", - "AB75": "GateImmortal (Economic)", - "AB76": "Colossi (Economic)", - "AB77": "GatewayAir (Economic)", - "P110": "WarpGate (Timing)", - "P112": "StalkerRobo (Timing)", - "P113": "Blink Stalker (Timing)", - "P114": "Dark Templar Rush (Timing)", - "P120": "SevenGate (Aggressive)", - "P121": "GateImmortal (Aggressive)", - "P122": "Colossi (Aggressive)", - "P123": "GatewayAir (Aggressive)", - "P124": "VoidPhoenix (Aggressive)", - "P130": "GateImmortal (Economic)", - "P131": "Colossi (Economic)", + "AI Build (Protoss)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB64": "FourGate (Timing)", + "AB65": "StalkerRobo (Timing)", + "AB66": "Blink Stalker (Timing)", + "AB67": "Dark Templar Rush (Timing)", + "AB69": "SevenGate (Aggressive)", + "AB70": "ArchonImmortal (Aggressive)", + "AB71": "Colossi (Aggressive)", + "AB72": "GatewayAir (Aggressive)", + "AB73": "VoidPhoenix (Aggressive)", + "AB75": "GateImmortal (Economic)", + "AB76": "Colossi (Economic)", + "AB77": "GatewayAir (Economic)", + "P110": "WarpGate (Timing)", + "P112": "StalkerRobo (Timing)", + "P113": "Blink Stalker (Timing)", + "P114": "Dark Templar Rush (Timing)", + "P120": "SevenGate (Aggressive)", + "P121": "GateImmortal (Aggressive)", + "P122": "Colossi (Aggressive)", + "P123": "GatewayAir (Aggressive)", + "P124": "VoidPhoenix (Aggressive)", + "P130": "GateImmortal (Economic)", + "P131": "Colossi (Economic)", "P132": "GatewayAir (Economic)" } - ], + ], "3198": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3199": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3200": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3201": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3202": [ - "AI Build", + "AI Build", { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", "AB06": "Straight to Air" } - ], + ], "3203": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3204": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3205": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3206": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "3207": [ - "AI Build (Zerg)", - { - "AB00": "Any Build", - "AB02": "Full Rush", - "AB03": "Timing Attack", - "AB04": "Aggressive Push", - "AB05": "Economic Focus", - "AB06": "Straight to Air", - "AB23": "BaneLing Bust (Timing)", - "AB24": "Roach Rush (Timing)", - "AB25": "LingRoach (Timing)", - "AB27": "Pure Mutalisk (Aggressive)", - "AB28": "MutaLing (Aggressive)", - "AB29": "RoachAttack (Aggressive)", - "AB30": "RoachInfestor (Aggressive)", - "AB31": "RoachHydra (Aggressive)", - "AB34": "Infestor (Economic)", - "AB35": "Ultralisk (Economic)", - "AB36": "Brood Lord (Economic)", - "Z030": "BaneLing Bust (Timing)", - "Z031": "Roach Rush (Timing)", - "Z032": "LingRoach (Timing)", - "Z040": "Mutalisk (Aggressive)", - "Z041": "MutaLing (Aggressive)", - "Z042": "RoachAttack (Aggressive)", - "Z043": "RoachInfestor (Aggressive)", - "Z044": "RoachHydra (Aggressive)", - "Z050": "Infestor (Economic)", - "Z052": "Ultralisk (Economic)", + "AI Build (Zerg)", + { + "AB00": "Any Build", + "AB02": "Full Rush", + "AB03": "Timing Attack", + "AB04": "Aggressive Push", + "AB05": "Economic Focus", + "AB06": "Straight to Air", + "AB23": "BaneLing Bust (Timing)", + "AB24": "Roach Rush (Timing)", + "AB25": "LingRoach (Timing)", + "AB27": "Pure Mutalisk (Aggressive)", + "AB28": "MutaLing (Aggressive)", + "AB29": "RoachAttack (Aggressive)", + "AB30": "RoachInfestor (Aggressive)", + "AB31": "RoachHydra (Aggressive)", + "AB34": "Infestor (Economic)", + "AB35": "Ultralisk (Economic)", + "AB36": "Brood Lord (Economic)", + "Z030": "BaneLing Bust (Timing)", + "Z031": "Roach Rush (Timing)", + "Z032": "LingRoach (Timing)", + "Z040": "Mutalisk (Aggressive)", + "Z041": "MutaLing (Aggressive)", + "Z042": "RoachAttack (Aggressive)", + "Z043": "RoachInfestor (Aggressive)", + "Z044": "RoachHydra (Aggressive)", + "Z050": "Infestor (Economic)", + "Z052": "Ultralisk (Economic)", "Z053": "Brood Lord (Economic)" } - ], + ], "4000": [ - "Game Privacy", + "Game Privacy", { - "NoBO": "No Build Order", - "NoMH": "No Match History", + "NoBO": "No Build Order", + "NoMH": "No Match History", "Norm": "Normal" } - ], + ], "4001": [ - "Using Custom Observer UI", + "Using Custom Observer UI", { - "no": "Not Using Custom Observer UI", + "no": "Not Using Custom Observer UI", "yes": "Using Custom Observer UI" } - ], + ], "4005": [ - "Ready", + "Ready", { - "no": "Not Ready", + "no": "Not Ready", "yes": "Ready" } - ], + ], "5000": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5001": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5002": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5003": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5004": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5005": [ - "Commander Mastery Talent", - { - "0": "0", - "1": "1", - "10": "10", - "11": "11", - "12": "12", - "13": "13", - "14": "14", - "15": "15", - "16": "16", - "17": "17", - "18": "18", - "19": "19", - "2": "2", - "20": "20", - "21": "21", - "22": "22", - "23": "23", - "24": "24", - "25": "25", - "26": "26", - "27": "27", - "28": "28", - "29": "29", - "3": "3", - "30": "30", - "4": "4", - "5": "5", - "6": "6", - "7": "7", - "8": "8", + "Commander Mastery Talent", + { + "0": "0", + "1": "1", + "10": "10", + "11": "11", + "12": "12", + "13": "13", + "14": "14", + "15": "15", + "16": "16", + "17": "17", + "18": "18", + "19": "19", + "2": "2", + "20": "20", + "21": "21", + "22": "22", + "23": "23", + "24": "24", + "25": "25", + "26": "26", + "27": "27", + "28": "28", + "29": "29", + "3": "3", + "30": "30", + "4": "4", + "5": "5", + "6": "6", + "7": "7", + "8": "8", "9": "9" } - ], + ], "5100": [ - "Brutal Plus Level", - { - "0": "0", - "1": "1", - "2": "2", - "3": "3", - "4": "4", - "5": "5", + "Brutal Plus Level", + { + "0": "0", + "1": "1", + "2": "2", + "3": "3", + "4": "4", + "5": "5", "6": "6" } - ], + ], "5200": [ - "Brutal Plus Is Retry", + "Brutal Plus Is Retry", { - "no": "No", + "no": "No", "yes": "Yes" } - ], + ], "5300": [ - "Commander Prestige Level", + "Commander Prestige Level", { - "0": "0", - "1": "1", - "2": "2", + "0": "0", + "1": "1", + "2": "2", "3": "3" } ] - }, - "decisions": "(dp0\nc__builtin__\nfrozenset\np1\n((lp2\nS'Hard'\np3\naVHarder\np4\na(I3004\nS'Hard'\np5\ntp6\natp7\nRp8\ng4\nsg1\n((lp9\n(I2001\nS'1v1'\np10\ntp11\naS'1 v 1'\np12\naV1v1\np13\natp14\nRp15\ng13\nsg1\n((lp16\n(I3104\nS'AB04'\np17\ntp18\naS'Agressive Push'\np19\naVAggressive Push\np20\natp21\nRp22\ng20\nsg1\n((lp23\nS'Agressive Push'\np24\naVAggressive Push\np25\na(I3199\nS'AB04'\np26\ntp27\natp28\nRp29\ng25\nsg1\n((lp30\nV6v6\np31\naS'6 v 6'\np32\na(I2001\nS'6v6'\np33\ntp34\natp35\nRp36\ng31\nsg1\n((lp37\nS'Agressive Push'\np38\na(I3102\nS'AB04'\np39\ntp40\naVAggressive Push\np41\natp42\nRp43\ng41\nsg1\n((lp44\nI2003\naVTeams2v2\np45\naS'Team'\np46\natp47\nRp48\ng45\nsg1\n((lp49\nVLadder\np50\naS'Automated Match Making'\np51\na(I3009\nS'Amm'\np52\ntp53\natp54\nRp55\ng50\nsg1\n((lp56\n(I2001\nS'5v5'\np57\ntp58\naS'5 v 5'\np59\naV5v5\np60\natp61\nRp62\ng60\nsg1\n((lp63\nVFree For All Teams\np64\naS'Free For All Archon'\np65\na(I2000\nVFFAT\np66\ntp67\natp68\nRp69\ng65\nsg1\n((lp70\nVHorner\np71\na(I3013\nVHorn\np72\ntp73\naS'Han & Horner'\np74\natp75\nRp76\ng74\nsg1\n((lp77\nI3141\naVAI Build (Terran)\np78\naS'AI Build'\np79\natp80\nRp81\ng78\nsg1\n((lp82\n(I2001\nS'3v3'\np83\ntp84\naS'3 v 3'\np85\naV3v3\np86\natp87\nRp88\ng86\nsg1\n((lp89\n(I3168\nS'AB04'\np90\ntp91\naS'Agressive Push'\np92\naVAggressive Push\np93\natp94\nRp95\ng93\nsg1\n((lp96\n(I3200\nS'AB04'\np97\ntp98\naS'Agressive Push'\np99\naVAggressive Push\np100\natp101\nRp102\ng100\nsg1\n((lp103\nVAI Build (Protoss)\np104\naI3174\naS'AI Build'\np105\natp106\nRp107\ng104\nsg1\n((lp108\nS'Very Hard'\np109\naVElite\np110\na(I3004\nS'VyHd'\np111\ntp112\natp113\nRp114\ng110\nsg1\n((lp115\nS'Agressive Push'\np116\naVAggressive Push\np117\na(I3167\nS'AB04'\np118\ntp119\natp120\nRp121\ng117\nsg1\n((lp122\nI3204\naS'AI Build'\np123\naVAI Build (Zerg)\np124\natp125\nRp126\ng124\nsg1\n((lp127\nVInsane\np128\naS'Cheater 3 (Insane)'\np129\na(I3004\nS'Insa'\np130\ntp131\natp132\nRp133\ng128\nsg1\n((lp134\n(I3007\nS'Watc'\np135\ntp136\naS'Observer'\np137\naS'Watcher'\np138\natp139\nRp140\ng137\nsg1\n((lp141\nI3205\naVAI Build (Zerg)\np142\naS'AI Build'\np143\natp144\nRp145\ng142\nsg1\n((lp146\nVTeams5v5\np147\naS'Team'\np148\naI2007\natp149\nRp150\ng147\nsg1\n((lp151\nI3171\naVAI Build (Protoss)\np152\naS'AI Build'\np153\natp154\nRp155\ng152\nsg1\n((lp156\nS'Unknown'\np157\naI2012\naS'Team'\np158\natp159\nRp160\ng158\nsg1\n((lp161\nI3173\naS'AI Build'\np162\naVAI Build (Protoss)\np163\natp164\nRp165\ng163\nsg1\n((lp166\nVAI Build (Terran)\np167\naI3142\naS'AI Build'\np168\natp169\nRp170\ng167\nsg1\n((lp171\nI3172\naVAI Build (Protoss)\np172\naS'AI Build'\np173\natp174\nRp175\ng172\nsg1\n((lp176\nS'Level 1 (Very Easy)'\np177\na(I3004\nS'VyEy'\np178\ntp179\naVVery Easy\np180\natp181\nRp182\ng180\nsg1\n((lp183\nS'Agressive Push'\np184\naVAggressive Push\np185\na(I3135\nS'AB04'\np186\ntp187\natp188\nRp189\ng185\nsg1\n((lp190\nV2v2\np191\naS'2 v 2'\np192\na(I2001\nS'2v2'\np193\ntp194\natp195\nRp196\ng191\nsg1\n((lp197\nS'Agressive Push'\np198\na(I3166\nS'AB04'\np199\ntp200\naVAggressive Push\np201\natp202\nRp203\ng201\nsg1\n((lp204\nVTeamsFFA\np205\naI2006\naS'Team'\np206\natp207\nRp208\ng205\nsg1\n((lp209\nS'AI Build'\np210\naVAI Build (Terran)\np211\naI3143\natp212\nRp213\ng211\nsg1\n((lp214\nVTeams7v7\np215\naI2011\naS'Team'\np216\natp217\nRp218\ng215\nsg1\n((lp219\nVMedium\np220\naS'Level 3 (Medium)'\np221\na(I3004\nS'Medi'\np222\ntp223\natp224\nRp225\ng220\nsg1\n((lp226\nI3140\naS'AI Build'\np227\naVAI Build (Terran)\np228\natp229\nRp230\ng228\nsg1\n((lp231\nVTeams4v4\np232\naI2005\naS'Team'\np233\natp234\nRp235\ng232\nsg1\n((lp236\nS'Agressive Push'\np237\na(I3198\nS'AB04'\np238\ntp239\naVAggressive Push\np240\natp241\nRp242\ng240\nsg1\n((lp243\n(I3136\nS'AB04'\np244\ntp245\naS'Agressive Push'\np246\naVAggressive Push\np247\natp248\nRp249\ng247\nsg1\n((lp250\nI2008\naVTeams6v6\np251\naS'Team'\np252\natp253\nRp254\ng251\nsg1\n((lp255\nS'Agressive Push'\np256\naVAggressive Push\np257\na(I3103\nS'AB04'\np258\ntp259\natp260\nRp261\ng257\nsg1\n((lp262\nV4v4\np263\naS'4 v 4'\np264\na(I2001\nS'4v4'\np265\ntp266\natp267\nRp268\ng263\nsg1\n((lp269\nS'Agressive Push'\np270\na(I3134\nS'AB04'\np271\ntp272\naVAggressive Push\np273\natp274\nRp275\ng273\nsg1\n((lp276\nVTeams1v1\np277\naI2002\naS'Team'\np278\natp279\nRp280\ng277\nsg1\n((lp281\nI3139\naS'AI Build'\np282\naVAI Build (Terran)\np283\natp284\nRp285\ng283\nsg1\n((lp286\nS'AI Build'\np287\naVAI Build (Zerg)\np288\naI3207\natp289\nRp290\ng288\nsg1\n((lp291\n(I2001\nS'FFA'\np292\ntp293\naS'Free For All'\np294\naVFFA\np295\natp296\nRp297\ng295\nsg1\n((lp298\nVAI Build (Zerg)\np299\naI3206\naS'AI Build'\np300\natp301\nRp302\ng299\nsg1\n((lp303\nVTeams3v3\np304\naI2004\naS'Team'\np305\natp306\nRp307\ng304\nsg1\n((lp308\nVAI Build (Protoss)\np309\naS'AI Build'\np310\naI3175\natp311\nRp312\ng309\nsg1\n((lp313\nS'Level 2 (Easy)'\np314\na(I3004\nS'Easy'\np315\ntp316\naVEasy\np317\natp318\nRp319\ng317\nsg1\n((lp320\nI3203\naS'AI Build'\np321\naVAI Build (Zerg)\np322\natp323\nRp324\ng322\ns." -} \ No newline at end of file + }, + "decisions": "(dp0\nc__builtin__\nfrozenset\np1\n((lp2\nS'Hard'\np3\naVHarder\np4\na(I3004\nS'Hard'\np5\ntp6\natp7\nRp8\ng4\nsg1\n((lp9\n(I2001\nS'1v1'\np10\ntp11\naS'1 v 1'\np12\naV1v1\np13\natp14\nRp15\ng13\nsg1\n((lp16\nVUnknown\np17\naI2003\naVTeams2v2\np18\natp19\nRp20\ng18\nsg1\n((lp21\nI3009\naVGame Mode\np22\nag17\natp23\nRp24\ng22\nsg1\n((lp25\n(I3104\nS'AB04'\np26\ntp27\naS'Agressive Push'\np28\naVAggressive Push\np29\natp30\nRp31\ng29\nsg1\n((lp32\nI2001\nag17\naVTeams\np33\natp34\nRp35\ng33\nsg1\n((lp36\ng17\naVTeams4v4\np37\naI2005\natp38\nRp39\ng37\nsg1\n((lp40\nS'Agressive Push'\np41\naVAggressive Push\np42\na(I3199\nS'AB04'\np43\ntp44\natp45\nRp46\ng42\nsg1\n((lp47\nV6v6\np48\naS'6 v 6'\np49\na(I2001\nS'6v6'\np50\ntp51\natp52\nRp53\ng48\nsg1\n((lp54\nS'Agressive Push'\np55\na(I3102\nS'AB04'\np56\ntp57\naVAggressive Push\np58\natp59\nRp60\ng58\nsg1\n((lp61\nI4001\nag17\naVUsing Custom Observer UI\np62\natp63\nRp64\ng62\nsg1\n((lp65\nI2003\naVTeams2v2\np66\naS'Team'\np67\natp68\nRp69\ng66\nsg1\n((lp70\nVLadder\np71\naS'Automated Match Making'\np72\na(I3009\nS'Amm'\np73\ntp74\natp75\nRp76\ng71\nsg1\n((lp77\n(I2001\nS'5v5'\np78\ntp79\naS'5 v 5'\np80\naV5v5\np81\natp82\nRp83\ng81\nsg1\n((lp84\nVFree For All Teams\np85\naS'Free For All Archon'\np86\na(I2000\nVFFAT\np87\ntp88\natp89\nRp90\ng86\nsg1\n((lp91\ng17\naVGame Duration\np92\naI3015\natp93\nRp94\ng92\nsg1\n((lp95\nI2008\nag17\naVTeams6v6\np96\natp97\nRp98\ng96\nsg1\n((lp99\nVHorner\np100\na(I3013\nVHorn\np101\ntp102\naS'Han & Horner'\np103\natp104\nRp105\ng103\nsg1\n((lp106\nI3141\naVAI Build (Terran)\np107\naS'AI Build'\np108\natp109\nRp110\ng107\nsg1\n((lp111\nI3008\nag17\naVObserver Type\np112\natp113\nRp114\ng112\nsg1\n((lp115\n(I2001\nS'3v3'\np116\ntp117\naS'3 v 3'\np118\naV3v3\np119\natp120\nRp121\ng119\nsg1\n((lp122\n(I3168\nS'AB04'\np123\ntp124\naS'Agressive Push'\np125\naVAggressive Push\np126\natp127\nRp128\ng126\nsg1\n((lp129\n(I3200\nS'AB04'\np130\ntp131\naS'Agressive Push'\np132\naVAggressive Push\np133\natp134\nRp135\ng133\nsg1\n((lp136\nVAI Build (Protoss)\np137\naI3174\naS'AI Build'\np138\natp139\nRp140\ng137\nsg1\n((lp141\nS'Very Hard'\np142\naVElite\np143\na(I3004\nS'VyHd'\np144\ntp145\natp146\nRp147\ng143\nsg1\n((lp148\nS'Agressive Push'\np149\naVAggressive Push\np150\na(I3167\nS'AB04'\np151\ntp152\natp153\nRp154\ng150\nsg1\n((lp155\nVTeams5v5\np156\nag17\naI2007\natp157\nRp158\ng156\nsg1\n((lp159\nI3204\naS'AI Build'\np160\naVAI Build (Zerg)\np161\natp162\nRp163\ng161\nsg1\n((lp164\nVInsane\np165\naS'Cheater 3 (Insane)'\np166\na(I3004\nS'Insa'\np167\ntp168\natp169\nRp170\ng165\nsg1\n((lp171\n(I3007\nS'Watc'\np172\ntp173\naS'Observer'\np174\naS'Watcher'\np175\natp176\nRp177\ng174\nsg1\n((lp178\nI3205\naVAI Build (Zerg)\np179\naS'AI Build'\np180\natp181\nRp182\ng179\nsg1\n((lp183\nVTeams5v5\np184\naS'Team'\np185\naI2007\natp186\nRp187\ng184\nsg1\n((lp188\nI3171\naVAI Build (Protoss)\np189\naS'AI Build'\np190\natp191\nRp192\ng189\nsg1\n((lp193\nI1000\naVRules\np194\nag17\natp195\nRp196\ng194\nsg1\n((lp197\nS'Unknown'\np198\naI2012\naS'Team'\np199\natp200\nRp201\ng199\nsg1\n((lp202\ng17\naVTandem Leader Slot\np203\naI3012\natp204\nRp205\ng203\nsg1\n((lp206\ng17\naI3011\naVPlayer Logo Index\np207\natp208\nRp209\ng207\nsg1\n((lp210\nI1001\naVPremade Game\np211\nag17\natp212\nRp213\ng211\nsg1\n((lp214\nI3173\naS'AI Build'\np215\naVAI Build (Protoss)\np216\natp217\nRp218\ng216\nsg1\n((lp219\nVAI Build (Terran)\np220\naI3142\naS'AI Build'\np221\natp222\nRp223\ng220\nsg1\n((lp224\nI3172\naVAI Build (Protoss)\np225\naS'AI Build'\np226\natp227\nRp228\ng225\nsg1\n((lp229\nS'Level 1 (Very Easy)'\np230\na(I3004\nS'VyEy'\np231\ntp232\naVVery Easy\np233\natp234\nRp235\ng233\nsg1\n((lp236\nS'Agressive Push'\np237\naVAggressive Push\np238\na(I3135\nS'AB04'\np239\ntp240\natp241\nRp242\ng238\nsg1\n((lp243\nV2v2\np244\naS'2 v 2'\np245\na(I2001\nS'2v2'\np246\ntp247\natp248\nRp249\ng244\nsg1\n((lp250\nS'Agressive Push'\np251\na(I3166\nS'AB04'\np252\ntp253\naVAggressive Push\np254\natp255\nRp256\ng254\nsg1\n((lp257\nVTeamsFFA\np258\naI2006\naS'Team'\np259\natp260\nRp261\ng258\nsg1\n((lp262\nS'AI Build'\np263\naVAI Build (Terran)\np264\naI3143\natp265\nRp266\ng264\nsg1\n((lp267\nVTeams7v7\np268\naI2011\naS'Team'\np269\natp270\nRp271\ng268\nsg1\n((lp272\nVTeams3v3\np273\nag17\naI2004\natp274\nRp275\ng273\nsg1\n((lp276\nI3000\naVGame Speed\np277\nag17\natp278\nRp279\ng277\nsg1\n((lp280\nVMedium\np281\naS'Level 3 (Medium)'\np282\na(I3004\nS'Medi'\np283\ntp284\natp285\nRp286\ng281\nsg1\n((lp287\nI3140\naS'AI Build'\np288\naVAI Build (Terran)\np289\natp290\nRp291\ng289\nsg1\n((lp292\nVTeams4v4\np293\naI2005\naS'Team'\np294\natp295\nRp296\ng293\nsg1\n((lp297\nS'Agressive Push'\np298\na(I3198\nS'AB04'\np299\ntp300\naVAggressive Push\np301\natp302\nRp303\ng301\nsg1\n((lp304\nVLocked Alliances\np305\nag17\naI3010\natp306\nRp307\ng305\nsg1\n((lp308\nVReady\np309\nag17\naI4005\natp310\nRp311\ng309\nsg1\n((lp312\ng17\naVLobby Delay\np313\naI3006\natp314\nRp315\ng313\nsg1\n((lp316\n(I3136\nS'AB04'\np317\ntp318\naS'Agressive Push'\np319\naVAggressive Push\np320\natp321\nRp322\ng320\nsg1\n((lp323\nI2008\naVTeams6v6\np324\naS'Team'\np325\natp326\nRp327\ng324\nsg1\n((lp328\nS'Agressive Push'\np329\naVAggressive Push\np330\na(I3103\nS'AB04'\np331\ntp332\natp333\nRp334\ng330\nsg1\n((lp335\nV4v4\np336\naS'4 v 4'\np337\na(I2001\nS'4v4'\np338\ntp339\natp340\nRp341\ng336\nsg1\n((lp342\nS'Agressive Push'\np343\na(I3134\nS'AB04'\np344\ntp345\naVAggressive Push\np346\natp347\nRp348\ng346\nsg1\n((lp349\nVTeams1v1\np350\naI2002\naS'Team'\np351\natp352\nRp353\ng350\nsg1\n((lp354\nVParticipant Role\np355\nag17\naI3007\natp356\nRp357\ng355\nsg1\n((lp358\nI3139\naS'AI Build'\np359\naVAI Build (Terran)\np360\natp361\nRp362\ng360\nsg1\n((lp363\nI4000\nag17\naVGame Privacy\np364\natp365\nRp366\ng364\nsg1\n((lp367\nS'AI Build'\np368\naVAI Build (Zerg)\np369\naI3207\natp370\nRp371\ng369\nsg1\n((lp372\n(I2001\nS'FFA'\np373\ntp374\naS'Free For All'\np375\naVFFA\np376\natp377\nRp378\ng376\nsg1\n((lp379\nVAI Build (Zerg)\np380\naI3206\naS'AI Build'\np381\natp382\nRp383\ng380\nsg1\n((lp384\nVTeams3v3\np385\naI2004\naS'Team'\np386\natp387\nRp388\ng385\nsg1\n((lp389\nVAI Build (Protoss)\np390\naS'AI Build'\np391\naI3175\natp392\nRp393\ng390\nsg1\n((lp394\nS'Level 2 (Easy)'\np395\na(I3004\nS'Easy'\np396\ntp397\naVEasy\np398\natp399\nRp400\ng398\nsg1\n((lp401\nVTeams1v1\np402\nag17\naI2002\natp403\nRp404\ng402\nsg1\n((lp405\nI3203\naS'AI Build'\np406\naVAI Build (Zerg)\np407\natp408\nRp409\ng407\ns." +} diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 8d64fa9c..8a755f8a 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1014,6 +1014,12 @@ def __init__(self, s2ml_file, **options): class GameSummary(Resource): + """ + Data extracted from the post-game Game Summary with units killed, + etc. This code does not work as reliably for Co-op games, which + have a completely different format for the report, which means + that the data is not necessarily in the places we expect. + """ url_template = "http://{0}.depot.battle.net:1119/{1}.s2gs" @@ -1110,7 +1116,9 @@ def __init__(self, summary_file, filename=None, lang="enUS", **options): self.load_player_stats() self.load_players() - self.game_type = self.settings["Teams"].replace(" ", "") + # the game type is probably co-op because it uses a different + # game summary format than other games + self.game_type = self.settings.get("Teams", 'Unknown').replace(" ", "") self.real_type = utils.get_real_type(self.teams) # The s2gs file also keeps reference to a series of s2mv files @@ -1149,8 +1157,8 @@ def load_translations(self): self.id_map[uid] = (sheet, entry) for value in item[1]: - sheet = value[1][0][1] - entry = value[1][0][2] + sheet = value[1][0][1] if value[1][0] else None + entry = value[1][0][2] if value[1][0] else None self.id_map[(uid, value[0])] = (sheet, entry) # Each localization is a pairing of a language id, e.g. enUS @@ -1188,7 +1196,7 @@ def load_translations(self): translation = dict() for uid, (sheet, item) in self.id_map.items(): - if sheet < len(sheets) and item in sheets[sheet]: + if sheet and sheet < len(sheets) and item in sheets[sheet]: translation[uid] = sheets[sheet][item] elif self.opt["debug"]: msg = "No {0} translation for sheet {1}, item {2}" From 7b08c1e814488ef9d3a8f7509eb3f3fbb272de72 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Tue, 29 Sep 2020 08:29:06 -0700 Subject: [PATCH 068/136] fix black --- sc2reader/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 8a755f8a..0be9b736 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1118,7 +1118,7 @@ def __init__(self, summary_file, filename=None, lang="enUS", **options): # the game type is probably co-op because it uses a different # game summary format than other games - self.game_type = self.settings.get("Teams", 'Unknown').replace(" ", "") + self.game_type = self.settings.get("Teams", "Unknown").replace(" ", "") self.real_type = utils.get_real_type(self.teams) # The s2gs file also keeps reference to a series of s2mv files From ba6817d9ff2f9637a28397886683af9052aafefb Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Tue, 29 Sep 2020 08:36:21 -0700 Subject: [PATCH 069/136] fix tests --- sc2reader/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 0be9b736..ad80695e 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1196,7 +1196,7 @@ def load_translations(self): translation = dict() for uid, (sheet, item) in self.id_map.items(): - if sheet and sheet < len(sheets) and item in sheets[sheet]: + if sheet is not None and sheet < len(sheets) and item in sheets[sheet]: translation[uid] = sheets[sheet][item] elif self.opt["debug"]: msg = "No {0} translation for sheet {1}, item {2}" From 4d1f9c513eeb6676b16100946cf7f2e0e8a22749 Mon Sep 17 00:00:00 2001 From: georgh Date: Fri, 23 Oct 2020 23:13:09 +0200 Subject: [PATCH 070/136] Bump version --- sc2reader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 68c429f1..11c8a336 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -20,7 +20,7 @@ """ from __future__ import absolute_import, print_function, unicode_literals, division -__version__ = "0.8.0" +__version__ = "1.6.0" import os import sys From 99a44faccc4e75de56a1c5ad35223366d6f481ec Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sat, 24 Oct 2020 05:46:21 +0200 Subject: [PATCH 071/136] CircleCI: Upgrade to Python 3.9 --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 546b7a7b..667b49fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ build_and_test: &build_and_test_steps jobs: StyleCheck: docker: - - image: circleci/python:3.8 + - image: circleci/python:3.9 steps: - checkout - run: sudo pip install black codespell flake8 @@ -27,12 +27,12 @@ jobs: Python2: docker: - - image: circleci/python:2.7.15 + - image: circleci/python:2.7.18 steps: *build_and_test_steps Python3: docker: - - image: circleci/python:3.8 + - image: circleci/python:3.9 steps: *build_and_test_steps From 4c577d6ab9022fe2872e700b49f03fa6ebac46f9 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 26 Oct 2020 13:42:30 -0700 Subject: [PATCH 072/136] Skip updating the owner on an event with no unit --- sc2reader/engine/plugins/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index dd846634..74e7c548 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -250,7 +250,7 @@ def handleUnitOwnerChangeEvent(self, event, replay): ) ) - if event.unit_upkeeper: + if event.unit_upkeeper and event.unit: if event.unit.owner: event.unit.owner.units.remove(event.unit) event.unit.owner = event.unit_upkeeper From 86c576241655063ec9c2e69ab1de620517bcb084 Mon Sep 17 00:00:00 2001 From: Chazzz <270857+Chazzz@users.noreply.github.com> Date: Thu, 10 Dec 2020 16:28:58 -0800 Subject: [PATCH 073/136] integrated ResourceTradeEvent and added test --- sc2reader/engine/plugins/context.py | 2 +- sc2reader/events/game.py | 11 ++++------- sc2reader/readers.py | 5 +++-- test_replays/test_replays.py | 11 +++++++++++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index dd846634..40a7e965 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -151,7 +151,7 @@ def handleSelectionEvent(self, event, replay): def handleResourceTradeEvent(self, event, replay): event.sender = event.player - event.recipient = replay.players[event.recipient_id] + event.recipient = replay.players[event.recipient_id-1] def handleHijackReplayGameEvent(self, event, replay): replay.resume_from_replay = True diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 02a054dd..ed3b48aa 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -650,14 +650,14 @@ def __init__(self, frame, pid, data): self.vespene = self.resources[1] if len(self.resources) >= 2 else None #: Amount terrazine sent - self.terrazon = self.resources[2] if len(self.resources) >= 3 else None + self.terrazine = self.resources[2] if len(self.resources) >= 3 else None #: Amount custom resource sent self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format( - self.minerals, self.vespene, self.terrazine, self.custom, self.recipient + self.minerals, self.vespene, self.terrazine, self.custom_resource, self.recipient ) @@ -685,11 +685,8 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return ( - self._str_prefix() - + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( - self.minerals, self.vespene, self.terrazine, self.custom - ) + return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( + self.minerals, self.vespene, self.terrazine, self.custom ) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 97e18074..382f7c04 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -421,7 +421,7 @@ def __init__(self): 28: (SelectionEvent, self.selection_delta_event), 29: (create_control_group_event, self.control_group_update_event), 30: (None, self.selection_sync_check_event), - 31: (None, self.resource_trade_event), + 31: (ResourceTradeEvent, self.resource_trade_event), 32: (None, self.trigger_chat_message_event), 33: (None, self.ai_communicate_event), 34: (None, self.set_absolute_game_speed_event), @@ -737,12 +737,13 @@ def selection_sync_check_event(self, data): ) def resource_trade_event(self, data): - return dict( + d = dict( recipient_id=data.read_bits(4), resources=[ data.read_uint32() - 2147483648 for i in range(data.read_bits(3)) ], ) + return d def trigger_chat_message_event(self, data): return dict(message=data.read_aligned_string(data.read_bits(10))) diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 2d0693ee..c9b34f5b 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -305,6 +305,17 @@ def test_send_resources(self): replay = sc2reader.load_replay( "test_replays/2.0.4.24944/Backwater Complex (15).SC2Replay" ) + trade_events = [ + event + for event in replay.events + if event.name == "ResourceTradeEvent" + ] + self.assertEqual(len(trade_events), 5) + self.assertEqual(trade_events[0].sender.name, "Guardian") + self.assertEqual(trade_events[0].recipient.name, "Sturmkind") + self.assertEqual(trade_events[0].recipient_id, 2) + self.assertEqual(trade_events[0].minerals, 0) + self.assertEqual(trade_events[0].vespene, 750) def test_cn_replays(self): replay = sc2reader.load_replay("test_replays/2.0.5.25092/cn1.SC2Replay") From 0866ea65ca5feab0aea18de4fa42f4eaeb665d31 Mon Sep 17 00:00:00 2001 From: Chazzz <270857+Chazzz@users.noreply.github.com> Date: Thu, 10 Dec 2020 16:31:20 -0800 Subject: [PATCH 074/136] revert return statement change --- sc2reader/readers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 382f7c04..496f66e5 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -737,13 +737,12 @@ def selection_sync_check_event(self, data): ) def resource_trade_event(self, data): - d = dict( + return dict( recipient_id=data.read_bits(4), resources=[ data.read_uint32() - 2147483648 for i in range(data.read_bits(3)) ], ) - return d def trigger_chat_message_event(self, data): return dict(message=data.read_aligned_string(data.read_bits(10))) From 563f83a98d90fb83ba73c41f7912fc917eef6c16 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 11 Dec 2020 08:32:44 +0100 Subject: [PATCH 075/136] setup.py: Add Python 3.8 and 3.9 to supported list Our CircleCI has been testing our code on Python 3.9 for some time now. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 4cc6db7d..5f912321 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,8 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Games/Entertainment", From dc75e7c998a2383b66deecf7b16f87778dda22e0 Mon Sep 17 00:00:00 2001 From: Chazzz <270857+Chazzz@users.noreply.github.com> Date: Fri, 11 Dec 2020 07:47:28 -0800 Subject: [PATCH 076/136] lookup on self.player instead of self.players and fix custom -> custom_resource --- sc2reader/engine/plugins/context.py | 2 +- sc2reader/events/game.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 40a7e965..3c0950af 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -151,7 +151,7 @@ def handleSelectionEvent(self, event, replay): def handleResourceTradeEvent(self, event, replay): event.sender = event.player - event.recipient = replay.players[event.recipient_id-1] + event.recipient = replay.player[event.recipient_id] def handleHijackReplayGameEvent(self, event, replay): replay.resume_from_replay = True diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index ed3b48aa..aaff75f4 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -686,7 +686,7 @@ def __init__(self, frame, pid, data): def __str__(self): return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( - self.minerals, self.vespene, self.terrazine, self.custom + self.minerals, self.vespene, self.terrazine, self.custom_resource ) From 3d1c3bac9984b69f6a3fa3d7db75ab934928081b Mon Sep 17 00:00:00 2001 From: Chazzz <270857+Chazzz@users.noreply.github.com> Date: Fri, 11 Dec 2020 08:22:06 -0800 Subject: [PATCH 077/136] Autoformatting provided by black --- sc2reader/events/game.py | 13 ++++++++++--- test_replays/test_replays.py | 6 ++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index aaff75f4..65eacc7e 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -657,7 +657,11 @@ def __init__(self, frame, pid, data): def __str__(self): return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format( - self.minerals, self.vespene, self.terrazine, self.custom_resource, self.recipient + self.minerals, + self.vespene, + self.terrazine, + self.custom_resource, + self.recipient, ) @@ -685,8 +689,11 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( - self.minerals, self.vespene, self.terrazine, self.custom_resource + return ( + self._str_prefix() + + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( + self.minerals, self.vespene, self.terrazine, self.custom_resource + ) ) diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index c9b34f5b..5e162327 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -306,10 +306,8 @@ def test_send_resources(self): "test_replays/2.0.4.24944/Backwater Complex (15).SC2Replay" ) trade_events = [ - event - for event in replay.events - if event.name == "ResourceTradeEvent" - ] + event for event in replay.events if event.name == "ResourceTradeEvent" + ] self.assertEqual(len(trade_events), 5) self.assertEqual(trade_events[0].sender.name, "Guardian") self.assertEqual(trade_events[0].recipient.name, "Sturmkind") From 98eb2d90630fec4667bd63b22cc4559265767b98 Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 13 Jan 2021 19:31:44 -0500 Subject: [PATCH 078/136] Changed depot url template Blizzard changed the depot url format. Updated with new format --- sc2reader/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index c1cfd437..0c08808f 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,7 +20,7 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = "http://{0}.depot.battle.net:1119/{1}.{2}" + url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}"" def __init__(self, bytes): #: The server the file is hosted on From c9719c64d4a3cd101775d9b1936b77ae6235a451 Mon Sep 17 00:00:00 2001 From: Robert Date: Thu, 14 Jan 2021 17:32:10 -0500 Subject: [PATCH 079/136] removed extra quote --- sc2reader/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 0c08808f..ed3314c0 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,7 +20,7 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}"" + url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}" def __init__(self, bytes): #: The server the file is hosted on From 6f9f6da095f496c92008f8f17d16eb48ea543f8b Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 17 May 2021 21:02:17 -0700 Subject: [PATCH 080/136] mark release version 1.7.0 --- CHANGELOG.rst | 9 +++++++++ sc2reader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ac15426f..bb518cc3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ CHANGELOG ============ +1.7.0 - May 17, 2021 +-------------------- +* Add DOI to the README #128 +* Add various missing attributes for co-op replays #129 +* Add support for python 3.8, 3.9 #132 #136 +* Fix owner on an event with no unit #133 +* Add support for ResourceTradeEvent #135 +* Fix depot URL template #139 + 1.6.0 - July 30, 2020 --------------------- * Add support for protocol 80949 (StarCraft 5.0) #122 diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 11c8a336..4d601ca8 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -20,7 +20,7 @@ """ from __future__ import absolute_import, print_function, unicode_literals, division -__version__ = "1.6.0" +__version__ = "1.7.0" import os import sys diff --git a/setup.py b/setup.py index 5f912321..0ce5ce32 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.6.0", + version="1.7.0", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From b4ae14d61ed1d462cf59021af154c9dd751171c7 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 17 May 2021 21:09:23 -0700 Subject: [PATCH 081/136] black updates --- sc2reader/data/__init__.py | 20 ++++++++++---------- sc2reader/events/tracker.py | 2 +- sc2reader/factories/plugins/utils.py | 6 +++--- sc2reader/objects.py | 2 +- sc2reader/scripts/sc2printer.py | 2 +- sc2reader/utils.py | 2 +- test_replays/test_replays.py | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 0d094b0a..6cbb9425 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -151,42 +151,42 @@ def title(self): @property def type(self): - """ The internal type id of the current unit type of this unit. None if no type is assigned""" + """The internal type id of the current unit type of this unit. None if no type is assigned""" return self._type_class.id if self._type_class else None @property def race(self): - """ The race of this unit. One of Terran, Protoss, Zerg, Neutral, or None""" + """The race of this unit. One of Terran, Protoss, Zerg, Neutral, or None""" return self._type_class.race if self._type_class else None @property def minerals(self): - """ The mineral cost of the unit. None if no type is assigned""" + """The mineral cost of the unit. None if no type is assigned""" return self._type_class.minerals if self._type_class else None @property def vespene(self): - """ The vespene cost of the unit. None if no type is assigned""" + """The vespene cost of the unit. None if no type is assigned""" return self._type_class.vespene if self._type_class else None @property def supply(self): - """ The supply used by this unit. Negative for supply providers. None if no type is assigned """ + """The supply used by this unit. Negative for supply providers. None if no type is assigned""" return self._type_class.supply if self._type_class else None @property def is_worker(self): - """ Boolean flagging units as worker units. SCV, MULE, Drone, Probe """ + """Boolean flagging units as worker units. SCV, MULE, Drone, Probe""" return self._type_class.is_worker if self._type_class else False @property def is_building(self): - """ Boolean flagging units as buildings. """ + """Boolean flagging units as buildings.""" return self._type_class.is_building if self._type_class else False @property def is_army(self): - """ Boolean flagging units as army units. """ + """Boolean flagging units as army units.""" return self._type_class.is_army if self._type_class else False def __str__(self): @@ -221,7 +221,7 @@ def __repr__(self): class UnitType(object): - """ Represents an in game unit type """ + """Represents an in game unit type""" def __init__( self, @@ -272,7 +272,7 @@ def __init__( class Ability(object): - """ Represents an in-game ability """ + """Represents an in-game ability""" def __init__( self, id, name=None, title=None, is_build=False, build_time=0, build_unit=None diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 47ebaf09..a3e3935e 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -36,7 +36,7 @@ def __str__(self): class PlayerSetupEvent(TrackerEvent): - """ Sent during game setup to help us organize players better """ + """Sent during game setup to help us organize players better""" def __init__(self, frames, data, build): super(PlayerSetupEvent, self).__init__(frames) diff --git a/sc2reader/factories/plugins/utils.py b/sc2reader/factories/plugins/utils.py index e472b86e..7eef123a 100644 --- a/sc2reader/factories/plugins/utils.py +++ b/sc2reader/factories/plugins/utils.py @@ -89,7 +89,7 @@ def deselect(self, mode, data): return True elif mode == "Mask": - """ Deselect objects according to deselect mask """ + """Deselect objects according to deselect mask""" mask = data if len(mask) < size: # pad to the right @@ -105,7 +105,7 @@ def deselect(self, mode, data): return len(mask) <= size elif mode == "OneIndices": - """ Deselect objects according to indexes """ + """Deselect objects according to indexes""" clean_data = list(filter(lambda i: i < size, data)) self.objects = [ self.objects[i] for i in range(len(self.objects)) if i not in clean_data @@ -113,7 +113,7 @@ def deselect(self, mode, data): return len(clean_data) == len(data) elif mode == "ZeroIndices": - """ Deselect objects according to indexes """ + """Deselect objects according to indexes""" clean_data = list(filter(lambda i: i < size, data)) self.objects = [self.objects[i] for i in clean_data] return len(clean_data) == len(data) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 3867896d..2ca2552b 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -473,7 +473,7 @@ def __init__(self, x, y, xy_list=None): self.values = y def as_points(self): - """ Get the graph as a list of (x, y) tuples """ + """Get the graph as a list of (x, y) tuples""" return list(zip(self.times, self.values)) def __str__(self): diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 92cfe686..f27fda49 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -11,7 +11,7 @@ def printReplay(filepath, arguments): - """ Prints summary information about SC2 replay file """ + """Prints summary information about SC2 replay file""" try: replay = sc2reader.load_replay(filepath, debug=True) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index ed3314c0..c35f91f3 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -38,7 +38,7 @@ def __init__(self, bytes): @property def url(self): - """ Returns url of the depot file. """ + """Returns url of the depot file.""" return self.url_template.format(self.server, self.hash, self.type) def __hash__(self): diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 5e162327..810cad06 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -321,7 +321,7 @@ def test_cn_replays(self): self.assertEqual(replay.expansion, "WoL") def test_unit_types(self): - """ sc2reader#136 regression test """ + """sc2reader#136 regression test""" replay = sc2reader.load_replay("test_replays/2.0.8.25604/issue136.SC2Replay") hellion_times = [ u.started_at for u in replay.players[0].units if u.name == "Hellion" From 44f4226d623660123de400c02b7dd11fb7488883 Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Wed, 30 Jun 2021 22:03:34 +1000 Subject: [PATCH 082/136] docs: fix a few simple typos There are small typos in: - docs/source/articles/creatingagameengineplugin.rst - docs/source/articles/whatsinareplay.rst - docs/source/tutorials/prettyprinter.rst - examples/sc2autosave.py - sc2reader/engine/engine.py - sc2reader/events/game.py - sc2reader/objects.py - sc2reader/resources.py Fixes: - Should read `aggregated` rather than `aggrated`. - Should read `account` rather than `acount`. - Should read `versatile` rather than `versitile`. - Should read `unnecessary` rather than `unncessary`. - Should read `targeting` rather than `targetting`. - Should read `substitution` rather than `substition`. - Should read `requirements` rather than `requirments`. Closes #145 --- docs/source/articles/creatingagameengineplugin.rst | 2 +- docs/source/articles/whatsinareplay.rst | 2 +- docs/source/tutorials/prettyprinter.rst | 2 +- examples/sc2autosave.py | 2 +- sc2reader/engine/engine.py | 2 +- sc2reader/events/game.py | 2 +- sc2reader/objects.py | 4 ++-- sc2reader/resources.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/articles/creatingagameengineplugin.rst b/docs/source/articles/creatingagameengineplugin.rst index 1d371aeb..c37ad079 100644 --- a/docs/source/articles/creatingagameengineplugin.rst +++ b/docs/source/articles/creatingagameengineplugin.rst @@ -52,7 +52,7 @@ Plugins may also handle special ``InitGame`` and ``EndGame`` events. These handl replay state necessary. * handleEndGame - is called after all events have been processed and - can be used to perform post processing on aggrated data or clean up + can be used to perform post processing on aggregated data or clean up intermediate data caches. Message Passing diff --git a/docs/source/articles/whatsinareplay.rst b/docs/source/articles/whatsinareplay.rst index a9b690c6..b1aa1643 100644 --- a/docs/source/articles/whatsinareplay.rst +++ b/docs/source/articles/whatsinareplay.rst @@ -35,7 +35,7 @@ The last file provides a record of important events from the game. * replay.tracker.events - Records important game events and game state updates. -This file was introduced in 2.0.4 and is unncessary for the Starcraft II to reproduce the game. Instead, it records interesting game events and game state for community developers to use when analyzing replays. +This file was introduced in 2.0.4 and is unnecessary for the Starcraft II to reproduce the game. Instead, it records interesting game events and game state for community developers to use when analyzing replays. What isn't in a replay? diff --git a/docs/source/tutorials/prettyprinter.rst b/docs/source/tutorials/prettyprinter.rst index a8584620..8e27a478 100644 --- a/docs/source/tutorials/prettyprinter.rst +++ b/docs/source/tutorials/prettyprinter.rst @@ -182,7 +182,7 @@ So lets put it all together into the final script, ``prettyPrinter.py``: Making Improvements --------------------------- -So our script works fine for single files, but what if you want to handle multiple files or directories? sc2reader provides two functions for loading replays: :meth:`~sc2reader.factories.SC2Factory.load_replay` and :meth:`~sc2reader.factories.SC2Factory.load_replays` which return a single replay and a list respectively. :meth:`~sc2reader.factories.SC2Factory.load_replay` was used above for convenience but :meth:`~sc2reader.factories.SC2Factory.load_replays` is much more versitile. Here's the difference: +So our script works fine for single files, but what if you want to handle multiple files or directories? sc2reader provides two functions for loading replays: :meth:`~sc2reader.factories.SC2Factory.load_replay` and :meth:`~sc2reader.factories.SC2Factory.load_replays` which return a single replay and a list respectively. :meth:`~sc2reader.factories.SC2Factory.load_replay` was used above for convenience but :meth:`~sc2reader.factories.SC2Factory.load_replays` is much more versatile. Here's the difference: * :meth:`~sc2reader.factories.SC2Factory.load_replay`: accepts a file path or an opened file object. * :meth:`~sc2reader.factories.SC2Factory.load_replays`: accepts a collection of opened file objects or file paths. Can also accept a single path to a directory; files will be pulled from the directory using :func:`~sc2reader.utils.get_files` and the given options. diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index fc40bb98..9facac16 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -37,7 +37,7 @@ -------------------- The --rename option allows you to specify a renaming format string. The string -is constructed the pythonic (3.0) way with {:field} indicating the substition +is constructed the pythonic (3.0) way with {:field} indicating the substitution of a field. The forward slash (/) is a special character here which terminates a folder name and allows for organization into subdirectories. All other string characters form the template into which the fields are inserted. diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index d527db36..986d1065 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -68,7 +68,7 @@ def handleEventName(self, event, replay) replay state necessary. * handleEndGame - is called after all events have been processed and - can be used to perform post processing on aggrated data or clean up + can be used to perform post processing on aggregated data or clean up intermediate data caches. Event handlers can choose to ``yield`` additional events which will be injected diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 03d69bba..8a890db4 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -351,7 +351,7 @@ class SelectionEvent(GameEvent): player is updated. Unlike other game events, these events can also be generated by non-player actions like unit deaths or transformations. - Starting in Starcraft 2.0.0, selection events targetting control group + Starting in Starcraft 2.0.0, selection events targeting control group buffers are also generated when control group selections are modified by non-player actions. When a player action updates a control group a :class:`ControlGroupEvent` is generated. diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 711203a6..c46d437b 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -129,7 +129,7 @@ def __init__(self, sid, slot_data): #: The Battle.net subregion the entity is registered to self.subregion = int(parts[2]) - #: The Battle.net acount identifier. Used to construct the + #: The Battle.net account identifier. Used to construct the #: bnet profile url. This value can be zero for games #: played offline when a user was not logged in to battle.net. self.toon_id = int(parts[3]) @@ -200,7 +200,7 @@ def __init__(self, pid, detail_data, attribute_data): #: The Battle.net subregion the entity is registered to self.subregion = detail_data['bnet']['subregion'] - #: The Battle.net acount identifier. Used to construct the + #: The Battle.net account identifier. Used to construct the #: bnet profile url. This value can be zero for games #: played offline when a user was not logged in to battle.net. self.toon_id = detail_data['bnet']['uid'] diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 0aedcffd..688a3268 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -923,7 +923,7 @@ def use_property(prop, player=None): # Lobby properties can require on player properties. # How does this work? I assume that one player satisfying the - # property requirments is sufficient + # property requirements is sufficient if requirement.is_lobby: values = [setting] else: From 0795bfae9c3ef50a452d7af60387c08b2b17b19d Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 15 Aug 2021 20:39:36 -0400 Subject: [PATCH 083/136] url_template updates load_map=True should now work on CN and NA out of the box. Updated the other url_templates to use the new blizzard CDN, although I neglected to include CN in them as adding it would be a bit more effort/it seems those were rarely used anyway since no one complained they were broken. --- sc2reader/resources.py | 10 +++++----- sc2reader/utils.py | 9 ++++++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index ad80695e..3b15ad5c 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -914,7 +914,7 @@ def __getstate__(self): class Map(Resource): - url_template = "http://{0}.depot.battle.net:1119/{1}.s2ma" + url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2ma" def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): super(Map, self).__init__(map_file, filename, **options) @@ -1021,7 +1021,7 @@ class GameSummary(Resource): that the data is not necessarily in the places we expect. """ - url_template = "http://{0}.depot.battle.net:1119/{1}.s2gs" + url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2gs" #: Game speed game_speed = str() @@ -1450,9 +1450,9 @@ def __str__(self): class MapHeader(Resource): """**Experimental**""" - base_url_template = "http://{0}.depot.battle.net:1119/{1}.{2}" - url_template = "http://{0}.depot.battle.net:1119/{1}.s2mh" - image_url_template = "http://{0}.depot.battle.net:1119/{1}.s2mv" + base_url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}" + url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2mh" + image_url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2mv" #: The name of the map name = str() diff --git a/sc2reader/utils.py b/sc2reader/utils.py index c35f91f3..0b4000cb 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,15 +20,18 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}" + url_template = "https://{0}-s2-depot.classic.blizzard.com{1}/{2}.{3}" def __init__(self, bytes): #: The server the file is hosted on self.server = bytes[4:8].decode("utf-8").strip("\x00 ") - + #: Used to make it possible to load maps from CN. This isn't needed for any other region and so is blank by default. + self.urlEnd = "" # There is no SEA depot, use US instead if self.server == "SEA": self.server = "US" + elif self.server == "CN": + self.urlEnd = ".cn" #: The unique content based hash of the file self.hash = binascii.b2a_hex(bytes[8:]).decode("utf8") @@ -39,7 +42,7 @@ def __init__(self, bytes): @property def url(self): """Returns url of the depot file.""" - return self.url_template.format(self.server, self.hash, self.type) + return self.url_template.format(self.server, self.urlEnd, self.hash, self.type) def __hash__(self): return hash(self.url) From 2c0b37771a5962faf34b5c4ab172e854243744ca Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 16 Aug 2021 08:45:07 -0400 Subject: [PATCH 084/136] Cleaned URL templates --- sc2reader/resources.py | 10 +++++----- sc2reader/utils.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 3b15ad5c..50c54f9f 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -914,7 +914,7 @@ def __getstate__(self): class Map(Resource): - url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2ma" + url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2ma" def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): super(Map, self).__init__(map_file, filename, **options) @@ -1021,7 +1021,7 @@ class GameSummary(Resource): that the data is not necessarily in the places we expect. """ - url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2gs" + url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2gs" #: Game speed game_speed = str() @@ -1450,9 +1450,9 @@ def __str__(self): class MapHeader(Resource): """**Experimental**""" - base_url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.{2}" - url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2mh" - image_url_template = "https://{0}-s2-depot.classic.blizzard.com/{1}.s2mv" + base_url_template = "https://{}-s2-depot.classic.blizzard.com/{}.{}" + url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2mh" + image_url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2mv" #: The name of the map name = str() diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 0b4000cb..a4564af3 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,7 +20,7 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = "https://{0}-s2-depot.classic.blizzard.com{1}/{2}.{3}" + url_template = "https://{}-s2-depot.classic.blizzard.com{}/{}.{}" def __init__(self, bytes): #: The server the file is hosted on From d96eb510838c7d63f02c32ca92fab45a697b032c Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 16 Aug 2021 08:46:55 -0400 Subject: [PATCH 085/136] Rename variable Renamed urlEnd to url_suffix for naming rules/better naming --- sc2reader/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index a4564af3..8bc1c054 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -26,12 +26,12 @@ def __init__(self, bytes): #: The server the file is hosted on self.server = bytes[4:8].decode("utf-8").strip("\x00 ") #: Used to make it possible to load maps from CN. This isn't needed for any other region and so is blank by default. - self.urlEnd = "" + self.url_suffix = "" # There is no SEA depot, use US instead if self.server == "SEA": self.server = "US" elif self.server == "CN": - self.urlEnd = ".cn" + self.url_suffix = ".cn" #: The unique content based hash of the file self.hash = binascii.b2a_hex(bytes[8:]).decode("utf8") @@ -42,7 +42,7 @@ def __init__(self, bytes): @property def url(self): """Returns url of the depot file.""" - return self.url_template.format(self.server, self.urlEnd, self.hash, self.type) + return self.url_template.format(self.server, self.url_suffix, self.hash, self.type) def __hash__(self): return hash(self.url) From 64041c27e1295aa99ea2e0c3c19a55676c8095b4 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 16 Aug 2021 13:20:34 -0400 Subject: [PATCH 086/136] Formatting Fixes --- sc2reader/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 8bc1c054..c0cdd62c 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -25,8 +25,11 @@ class DepotFile(object): def __init__(self, bytes): #: The server the file is hosted on self.server = bytes[4:8].decode("utf-8").strip("\x00 ") - #: Used to make it possible to load maps from CN. This isn't needed for any other region and so is blank by default. + + # Used to make it possible to load maps from CN. + # This isn't needed for any other region and so is blank by default. self.url_suffix = "" + # There is no SEA depot, use US instead if self.server == "SEA": self.server = "US" @@ -42,7 +45,9 @@ def __init__(self, bytes): @property def url(self): """Returns url of the depot file.""" - return self.url_template.format(self.server, self.url_suffix, self.hash, self.type) + return self.url_template.format( + self.server, self.url_suffix, self.hash, self.type + ) def __hash__(self): return hash(self.url) From b68cc48a8b08da15ff90d4f950d3d3cbca4b89b1 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 16 Aug 2021 13:39:57 -0400 Subject: [PATCH 087/136] remove trailing spaces --- sc2reader/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index c0cdd62c..c4bbcd77 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -26,9 +26,9 @@ def __init__(self, bytes): #: The server the file is hosted on self.server = bytes[4:8].decode("utf-8").strip("\x00 ") - # Used to make it possible to load maps from CN. + # Used to make it possible to load maps from CN. # This isn't needed for any other region and so is blank by default. - self.url_suffix = "" + self.url_suffix = "" # There is no SEA depot, use US instead if self.server == "SEA": From 7bc551e40671e27e7d0ead58b9dc5ae1577d3186 Mon Sep 17 00:00:00 2001 From: Bean Date: Tue, 7 Sep 2021 11:43:59 +0800 Subject: [PATCH 088/136] fix for CN --- sc2reader/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index c4bbcd77..b195bc7b 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -20,7 +20,7 @@ class DepotFile(object): """ #: The url template for all DepotFiles - url_template = "https://{}-s2-depot.classic.blizzard.com{}/{}.{}" + url_template = "{}://{}-s2-depot.{}/{}.{}" def __init__(self, bytes): #: The server the file is hosted on @@ -28,13 +28,15 @@ def __init__(self, bytes): # Used to make it possible to load maps from CN. # This isn't needed for any other region and so is blank by default. - self.url_suffix = "" + self.scheme = "https" + self.domain = "classic.blizzard.com" # There is no SEA depot, use US instead if self.server == "SEA": self.server = "US" elif self.server == "CN": - self.url_suffix = ".cn" + self.scheme = "http" + self.domain = "battlenet.com.cn" #: The unique content based hash of the file self.hash = binascii.b2a_hex(bytes[8:]).decode("utf8") @@ -46,7 +48,7 @@ def __init__(self, bytes): def url(self): """Returns url of the depot file.""" return self.url_template.format( - self.server, self.url_suffix, self.hash, self.type + self.scheme, self.server, self.domain, self.hash, self.type ) def __hash__(self): From c50e91a0061b691f4333d87362c8435ef12233d0 Mon Sep 17 00:00:00 2001 From: Bean Date: Tue, 7 Sep 2021 14:07:38 +0800 Subject: [PATCH 089/136] Add utils.get_resource_url to get resource url --- sc2reader/resources.py | 20 +++++--------------- sc2reader/utils.py | 34 +++++++++++++++------------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 50c54f9f..9b4e95b9 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -914,8 +914,6 @@ def __getstate__(self): class Map(Resource): - url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2ma" - def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): super(Map, self).__init__(map_file, filename, **options) @@ -998,9 +996,7 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option def get_url(cls, region, map_hash): """Builds a download URL for the map from its components.""" if region and map_hash: - # it seems like sea maps are stored on us depots. - region = "us" if region == "sea" else region - return cls.url_template.format(region, map_hash) + return utils.get_resource_url(region, hash, "s2ma") else: return None @@ -1021,8 +1017,6 @@ class GameSummary(Resource): that the data is not necessarily in the places we expect. """ - url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2gs" - #: Game speed game_speed = str() @@ -1450,10 +1444,6 @@ def __str__(self): class MapHeader(Resource): """**Experimental**""" - base_url_template = "https://{}-s2-depot.classic.blizzard.com/{}.{}" - url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2mh" - image_url_template = "https://{}-s2-depot.classic.blizzard.com/{}.s2mv" - #: The name of the map name = str() @@ -1488,14 +1478,14 @@ def __init__(self, header_file, filename=None, **options): # Parse image hash parsed_hash = utils.parse_hash(self.data[0][1]) self.image_hash = parsed_hash["hash"] - self.image_url = self.image_url_template.format( - parsed_hash["server"], parsed_hash["hash"] + self.image_url = utils.get_resource_url( + parsed_hash["server"], parsed_hash["hash"], "s2mv" ) # Parse map hash parsed_hash = utils.parse_hash(self.data[0][2]) self.map_hash = parsed_hash["hash"] - self.map_url = self.base_url_template.format( + self.map_url = utils.get_resource_url( parsed_hash["server"], parsed_hash["hash"], parsed_hash["type"] ) @@ -1503,6 +1493,6 @@ def __init__(self, header_file, filename=None, **options): l18n_struct = self.data[0][4][8] for l in l18n_struct: parsed_hash = utils.parse_hash(l[1][0]) - self.localization_urls[l[0]] = self.base_url_template.format( + self.localization_urls[l[0]] = utils.get_resource_url( parsed_hash["server"], parsed_hash["hash"], parsed_hash["type"] ) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index b195bc7b..4c67992f 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -19,24 +19,9 @@ class DepotFile(object): and assembles them into a URL so that the dependency can be fetched. """ - #: The url template for all DepotFiles - url_template = "{}://{}-s2-depot.{}/{}.{}" - def __init__(self, bytes): #: The server the file is hosted on - self.server = bytes[4:8].decode("utf-8").strip("\x00 ") - - # Used to make it possible to load maps from CN. - # This isn't needed for any other region and so is blank by default. - self.scheme = "https" - self.domain = "classic.blizzard.com" - - # There is no SEA depot, use US instead - if self.server == "SEA": - self.server = "US" - elif self.server == "CN": - self.scheme = "http" - self.domain = "battlenet.com.cn" + self.server = bytes[4:8].decode("utf-8").strip("\x00 ").lower() #: The unique content based hash of the file self.hash = binascii.b2a_hex(bytes[8:]).decode("utf8") @@ -47,9 +32,7 @@ def __init__(self, bytes): @property def url(self): """Returns url of the depot file.""" - return self.url_template.format( - self.scheme, self.server, self.domain, self.hash, self.type - ) + return get_resource_url(self.server, self.hash, self.type) def __hash__(self): return hash(self.url) @@ -210,6 +193,19 @@ def get_files( depth -= 1 +def get_resource_url(region, hash, type): + url_template = "{}://{}-s2-depot.{}/{}.{}" + scheme = "https" + domain = "classic.blizzard.com" + + if region == "sea": + region = "us" + elif region == "cn": + scheme = "http" + domain = "battlenet.com.cn" + return url_template.format(scheme, region, domain, hash, type) + + class Length(timedelta): """ Extends the builtin timedelta class. See python docs for more info on From 2d47a9047426872afa5b7fc8603a9395bb47b29b Mon Sep 17 00:00:00 2001 From: Bean Date: Wed, 1 Dec 2021 10:07:14 +0800 Subject: [PATCH 090/136] Update new Battle.net profile url --- sc2reader/objects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 2ca2552b..11ef8eb7 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -241,6 +241,9 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[detail_data["bnet"]["region"]] + #: The Battle.net region id the entity is registered to + self.region_id = detail_data["bnet"]["region"] + #: The Battle.net subregion the entity is registered to self.subregion = detail_data["bnet"]["subregion"] @@ -258,7 +261,7 @@ class User(object): #: The Battle.net profile url template URL_TEMPLATE = ( - "http://{region}.battle.net/sc2/en/profile/{toon_id}/{subregion}/{name}/" + "https://starcraft2.com/en-us/profile/{region_id}/{subregion}/{toon_id}" ) def __init__(self, uid, init_data): From 628a70615fb2a6c4805938a4a33e2fa2d4d51a73 Mon Sep 17 00:00:00 2001 From: Bean Date: Wed, 1 Dec 2021 10:14:09 +0800 Subject: [PATCH 091/136] Update new profile urls for tests and docs --- docs/source/tutorials/prettyprinter.rst | 2 +- test_replays/test_replays.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/tutorials/prettyprinter.rst b/docs/source/tutorials/prettyprinter.rst index 578b4ed5..f72005a0 100644 --- a/docs/source/tutorials/prettyprinter.rst +++ b/docs/source/tutorials/prettyprinter.rst @@ -89,7 +89,7 @@ Many of the replay attributes are nested data structures which are generally all >>> replay.teams[0].players[0].color.hex 'B4141E' >>> replay.player.name('Remedy').url - 'http://us.battle.net/sc2/en/profile/2198663/1/Remedy/' + 'https://starcraft2.com/en-us/profile/1/1/2198663' Each of these nested structures can be found either on its own reference page or lumped together with the other minor structures on the Misc Structures page. diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 810cad06..9600c33e 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -80,10 +80,10 @@ def test_standard_1v1(self): self.assertEqual(boom.result, "Loss") self.assertEqual( - emperor.url, "http://eu.battle.net/sc2/en/profile/520049/1/Emperor/" + emperor.url, "https://starcraft2.com/en-us/profile/2/1/520049" ) self.assertEqual( - boom.url, "http://eu.battle.net/sc2/en/profile/1694745/1/Boom/" + boom.url, "https://starcraft2.com/en-us/profile/2/1/1694745" ) self.assertEqual(len(replay.messages), 12) @@ -163,10 +163,10 @@ def test_us_realm(self): reddawn = [player for player in replay.players if player.name == "reddawn"][0] self.assertEqual( shadesofgray.url, - "http://us.battle.net/sc2/en/profile/2358439/1/ShadesofGray/", + "https://starcraft2.com/en-us/profile/1/1/2358439", ) self.assertEqual( - reddawn.url, "http://us.battle.net/sc2/en/profile/2198663/1/reddawn/" + reddawn.url, "https://starcraft2.com/en-us/profile/1/1/2198663" ) def test_kr_realm_and_tampered_messages(self): @@ -179,10 +179,10 @@ def test_kr_realm_and_tampered_messages(self): first = [player for player in replay.players if player.name == "명지대학교"][0] second = [player for player in replay.players if player.name == "티에스엘사기수"][0] self.assertEqual( - first.url, "http://kr.battle.net/sc2/en/profile/258945/1/명지대학교/" + first.url, "https://starcraft2.com/en-us/profile/3/1/258945" ) self.assertEqual( - second.url, "http://kr.battle.net/sc2/en/profile/102472/1/티에스엘사기수/" + second.url, "https://starcraft2.com/en-us/profile/3/1/102472" ) self.assertEqual(replay.messages[0].text, "sc2.replays.net") self.assertEqual(replay.messages[5].text, "sc2.replays.net") From 6bb984dbe85f46a6684680dd0e56c09d7188214b Mon Sep 17 00:00:00 2001 From: Bean Date: Wed, 1 Dec 2021 10:17:42 +0800 Subject: [PATCH 092/136] Format for StyleCheck --- test_replays/test_replays.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 9600c33e..2b1d1492 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -79,12 +79,8 @@ def test_standard_1v1(self): self.assertEqual(emperor.result, "Win") self.assertEqual(boom.result, "Loss") - self.assertEqual( - emperor.url, "https://starcraft2.com/en-us/profile/2/1/520049" - ) - self.assertEqual( - boom.url, "https://starcraft2.com/en-us/profile/2/1/1694745" - ) + self.assertEqual(emperor.url, "https://starcraft2.com/en-us/profile/2/1/520049") + self.assertEqual(boom.url, "https://starcraft2.com/en-us/profile/2/1/1694745") self.assertEqual(len(replay.messages), 12) self.assertEqual(replay.messages[0].text, "hf") @@ -178,12 +174,8 @@ def test_kr_realm_and_tampered_messages(self): self.assertEqual(replay.expansion, "WoL") first = [player for player in replay.players if player.name == "명지대학교"][0] second = [player for player in replay.players if player.name == "티에스엘사기수"][0] - self.assertEqual( - first.url, "https://starcraft2.com/en-us/profile/3/1/258945" - ) - self.assertEqual( - second.url, "https://starcraft2.com/en-us/profile/3/1/102472" - ) + self.assertEqual(first.url, "https://starcraft2.com/en-us/profile/3/1/258945") + self.assertEqual(second.url, "https://starcraft2.com/en-us/profile/3/1/102472") self.assertEqual(replay.messages[0].text, "sc2.replays.net") self.assertEqual(replay.messages[5].text, "sc2.replays.net") From 0bb34d092d717cded4f0458eed89c63e9c72c523 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 10 Jan 2022 21:24:09 +0100 Subject: [PATCH 093/136] docs: sphinx.ext.pngmath --> sphinx.ext.imgmath Fixes #157 --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 15623ce9..2d9a7214 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,7 +28,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.pngmath", "sphinx.ext.viewcode"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.imgmath", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] From 5b44de8e33001493345d0eb1767b08745777bef2 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 31 Jan 2022 20:47:03 -0800 Subject: [PATCH 094/136] fix black formatting from StyleCheck 625 (#160) * fix black formatting from StyleCheck 625 * style guide documentation updates * more black updates * reference f-strings --- STYLE_GUIDE.rst | 19 ++++++++----------- docs/source/conf.py | 8 ++++---- sc2reader/engine/plugins/creeptracker.py | 2 +- sc2reader/events/tracker.py | 2 +- sc2reader/readers.py | 2 +- sc2reader/resources.py | 6 +++--- sc2reader/scripts/sc2replayer.py | 1 - sc2reader/utils.py | 2 +- 8 files changed, 19 insertions(+), 23 deletions(-) diff --git a/STYLE_GUIDE.rst b/STYLE_GUIDE.rst index 26b0bb8f..14f91df5 100644 --- a/STYLE_GUIDE.rst +++ b/STYLE_GUIDE.rst @@ -1,11 +1,15 @@ STYLE GUIDE ============== -As a rough style guide, please lint your code with pep8:: +As a rough style guide, please lint your code with black, codespell, and flake8:: - pip install pep8 - pep8 --ignore E501,E226,E241 sc2reader + pip install black codespell flake8 + codespell -L queenland,uint + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + black . --check +More up-to-date checks may be detailed in `.circleci/config.yml`. All files should start with the following:: @@ -17,11 +21,4 @@ All files should start with the following:: All imports should be absolute. - -All string formatting sound be done in the following style:: - - "my {0} formatted {1} string {2}".format("super", "python", "example") - "the {x} style of {y} is also {z}".format(x="dict", y="arguments", z="acceptable") - -The format argument index numbers are important for 2.6 support. ``%`` formatting is not allowed for 3.x support - +All string formatting should be done with f-strings. See https://docs.python.org/3/reference/lexical_analysis.html#f-strings diff --git a/docs/source/conf.py b/docs/source/conf.py index 2d9a7214..17164624 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -43,8 +43,8 @@ master_doc = "index" # General information about the project. -project = u"sc2reader" -copyright = u"2011-2013" +project = "sc2reader" +copyright = "2011-2013" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -182,7 +182,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ("index", "sc2reader.tex", u"sc2reader Documentation", u"Graylin Kim", "manual") + ("index", "sc2reader.tex", "sc2reader Documentation", "Graylin Kim", "manual") ] # The name of an image file (relative to this directory) to place at the top of @@ -213,4 +213,4 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("index", "sc2reader", u"sc2reader Documentation", [u"Graylin Kim"], 1)] +man_pages = [("index", "sc2reader", "sc2reader Documentation", ["Graylin Kim"], 1)] diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 8dde77c9..5a56b4f2 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -173,7 +173,7 @@ def radius_to_map_positions(self, radius): # Sample a square area using the radius for x in range(-radius, radius): for y in range(-radius, radius): - if (x ** 2 + y ** 2) <= (radius * radius): + if (x**2 + y**2) <= (radius * radius): output_coordinates.append((x, y)) return output_coordinates diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index a3e3935e..6220d5fe 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -17,7 +17,7 @@ class TrackerEvent(Event): def __init__(self, frames): #: The frame of the game this event was applied #: Ignore all but the lowest 32 bits of the frame - self.frame = frames % 2 ** 32 + self.frame = frames % 2**32 #: The second of the game (game time not real time) this event was applied self.second = self.frame >> 4 diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 496f66e5..83c3a2ea 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -561,7 +561,7 @@ def __call__(self, data, replay): ) # Don't want to do this more than once - SINGLE_BIT_MASKS = [0x1 << i for i in range(2 ** 9)] + SINGLE_BIT_MASKS = [0x1 << i for i in range(2**9)] def read_selection_bitmask(self, data, mask_length): bits_left = mask_length diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 9b4e95b9..428e9cc9 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -421,11 +421,11 @@ def load_details(self): # the value required to get the adjusted timestamp. We know the upper # limit for any adjustment number so use that to distinguish between # the two cases. - if details["utc_adjustment"] < 10 ** 7 * 60 * 60 * 24: - self.time_zone = details["utc_adjustment"] / (10 ** 7 * 60 * 60) + if details["utc_adjustment"] < 10**7 * 60 * 60 * 24: + self.time_zone = details["utc_adjustment"] / (10**7 * 60 * 60) else: self.time_zone = (details["utc_adjustment"] - details["file_time"]) / ( - 10 ** 7 * 60 * 60 + 10**7 * 60 * 60 ) self.game_length = self.length diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index 011cffc6..3d8212ca 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -28,7 +28,6 @@ def getch(): termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm) fcntl.fcntl(fd, fcntl.F_SETFL, oldflags) - except ImportError as e: try: # Oops, we might be on windows, try this one diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 4c67992f..93787212 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -45,7 +45,7 @@ def windows_to_unix(windows_time): # This windows timestamp measures the number of 100 nanosecond periods since # January 1st, 1601. First we subtract the number of nanosecond periods from # 1601-1970, then we divide by 10^7 to bring it back to seconds. - return int((windows_time - 116444735995904000) / 10 ** 7) + return int((windows_time - 116444735995904000) / 10**7) @loggable From f7cac8ea3fd7365a7dc8a442a8b59b1ec9ea125b Mon Sep 17 00:00:00 2001 From: Dan Fulton Date: Sat, 19 Feb 2022 14:02:45 -0800 Subject: [PATCH 095/136] Fixed Ravager and RavagerCocoon unit info. (#161) --- sc2reader/data/unit_info.json | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sc2reader/data/unit_info.json b/sc2reader/data/unit_info.json index 81cf8669..15d02ff6 100644 --- a/sc2reader/data/unit_info.json +++ b/sc2reader/data/unit_info.json @@ -224,9 +224,15 @@ }, "ravager": { "is_army": true, - "minerals": 75, - "vespene": 25, - "supply": 2 + "minerals": 100, + "vespene": 100, + "supply": 3 + }, + "ravagercocoon": { + "is_army": true, + "minerals": 100, + "vespene": 100, + "supply": 3 }, "roach": { "is_army": true, From 6b562fa2a90887f2bb84b9eac60c148276d02e7e Mon Sep 17 00:00:00 2001 From: Daniel Fulton Date: Mon, 21 Feb 2022 21:47:42 -0800 Subject: [PATCH 096/136] Added class CommandManagerStateEvent. --- sc2reader/events/game.py | 20 ++++++++++++++++++++ sc2reader/readers.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 65eacc7e..82232011 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -399,6 +399,26 @@ def __init__(self, frame, pid, data): self.target_data = self.ability_type_data.get("data", None) +@loggable +class CommandManagerStateEvent(GameEvent): + """ + These events indicated that the last :class:`CommandEvent` called has been + called again. For example, if you add three SCVs to an empty queue on a + Command Center, the first add will be generate a :class:`BasicCommandEvent` + and the two subsequent adds will each generate a + :class:`CommandManagerStateEvent`. + """ + + def __init__(self, frame, pid, data): + super(CommandManagerStateEvent, self).__init__(frame, pid) + + #: Always 1? + self.state = data["state"] + + #: An index identifying how many events of this type have been called + self.sequence = data["sequence"] + + @loggable class SelectionEvent(GameEvent): """ diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 83c3a2ea..89ba67fc 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1745,7 +1745,7 @@ def __init__(self): self.command_manager_reset_event, ), # Re-using this old number 61: (None, self.trigger_hotkey_pressed_event), - 103: (None, self.command_manager_state_event), + 103: (CommandManagerStateEvent, self.command_manager_state_event), 104: ( UpdateTargetPointCommandEvent, self.command_update_target_point_event, From f5e663bf52a4426171a9d9c602d5d7199e500bdd Mon Sep 17 00:00:00 2001 From: Daniel Fulton Date: Sat, 11 Sep 2021 14:33:47 -0700 Subject: [PATCH 097/136] Updated .gitignore to ignore Jupyter files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 6643915a..22d56881 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *~ *.pyc +*.ipynb +*-checkpoint* dist build sc2reader.egg-info From f023de417c9d589b91fb48ef3aac0fb70bc3b87a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 22 Feb 2022 07:39:26 +0100 Subject: [PATCH 098/136] CircleCI: Upgrade to Python 3.10 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 667b49fe..ff87ea20 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ build_and_test: &build_and_test_steps jobs: StyleCheck: docker: - - image: circleci/python:3.9 + - image: circleci/python:3.10 steps: - checkout - run: sudo pip install black codespell flake8 From 5167166c24c738a7e23d4d38372d8a418d8be9f8 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 22 Feb 2022 07:40:04 +0100 Subject: [PATCH 099/136] CircleCI: Upgrade to Python 3.10 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ff87ea20..0799ef91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ jobs: Python3: docker: - - image: circleci/python:3.9 + - image: circleci/python:3.10 steps: *build_and_test_steps From 2c4a640e0b94e32eceb23a8636f5fb8809e61f17 Mon Sep 17 00:00:00 2001 From: Robert Heine Date: Tue, 3 May 2022 10:05:24 -0400 Subject: [PATCH 100/136] force gameheart to exit if game is played in LotV --- sc2reader/engine/plugins/gameheart.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sc2reader/engine/plugins/gameheart.py b/sc2reader/engine/plugins/gameheart.py index 1e412031..d3905624 100644 --- a/sc2reader/engine/plugins/gameheart.py +++ b/sc2reader/engine/plugins/gameheart.py @@ -34,6 +34,11 @@ def handleInitGame(self, event, replay): yield PluginExit(self, code=0, details=dict()) return + # Exit plugin if game is LOTV as LOTV games dont use GameHeart + if replay.expansion == "LotV": + yield PluginExit(self, code=0, details=dict()) + return + start_frame = -1 actual_players = {} for event in replay.tracker_events: From 2f1afc4639962e47646201b0b093597d6acb66c6 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Wed, 4 May 2022 22:35:03 -0700 Subject: [PATCH 101/136] mark release version 1.8.0 --- CHANGELOG.rst | 9 +++++++++ sc2reader/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index bb518cc3..ba71454a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ CHANGELOG ============ +1.8.0 - May 4, 2022 +------------------- +* Fix various typos in docs #146 +* Fix various URLs for blizzard resources #151 #154 #156 +* Fix Ravager data #161 +* Add CommandManagerStateEvent #162 +* Fix participant state from gameheart #171 + + 1.7.0 - May 17, 2021 -------------------- * Add DOI to the README #128 diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 4d601ca8..419f47b5 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -20,7 +20,7 @@ """ from __future__ import absolute_import, print_function, unicode_literals, division -__version__ = "1.7.0" +__version__ = "1.8.0" import os import sys diff --git a/setup.py b/setup.py index 0ce5ce32..f0f57022 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.7.0", + version="1.8.0", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From 2a493562c23a6535c2bed4c22be35da876849d79 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 25 May 2022 18:26:00 +0200 Subject: [PATCH 102/136] Drop support for legacy Python --- .circleci/config.yml | 6 -- docs/source/conf.py | 1 - examples/sc2autosave.py | 41 ++++++----- examples/sc2store.py | 3 +- generate_build_data.py | 22 +++--- new_units.py | 8 +-- sc2reader/__init__.py | 2 - sc2reader/constants.py | 3 - sc2reader/data/__init__.py | 19 +++--- sc2reader/data/create_lookup.py | 4 +- sc2reader/decoders.py | 21 +++--- sc2reader/engine/__init__.py | 3 - sc2reader/engine/engine.py | 7 +- sc2reader/engine/events.py | 10 +-- sc2reader/engine/plugins/__init__.py | 3 - sc2reader/engine/plugins/apm.py | 5 +- sc2reader/engine/plugins/context.py | 34 +++++---- sc2reader/engine/plugins/creeptracker.py | 15 ++-- sc2reader/engine/plugins/gameheart.py | 5 +- sc2reader/engine/plugins/selection.py | 6 +- sc2reader/engine/plugins/supply.py | 15 ++-- sc2reader/engine/utils.py | 7 +- sc2reader/events/__init__.py | 3 - sc2reader/events/base.py | 6 +- sc2reader/events/game.py | 55 +++++++-------- sc2reader/events/message.py | 11 ++- sc2reader/events/tracker.py | 41 ++++++----- sc2reader/exceptions.py | 8 +-- sc2reader/factories/__init__.py | 2 - sc2reader/factories/plugins/replay.py | 13 ++-- sc2reader/factories/plugins/utils.py | 13 ++-- sc2reader/factories/sc2factory.py | 17 ++--- sc2reader/log_utils.py | 5 +- sc2reader/objects.py | 55 +++++++-------- sc2reader/readers.py | 37 +++++----- sc2reader/resources.py | 87 ++++++++++++------------ sc2reader/scripts/__init__.py | 3 - sc2reader/scripts/sc2attributes.py | 14 ++-- sc2reader/scripts/sc2json.py | 2 - sc2reader/scripts/sc2parse.py | 44 ++++++------ sc2reader/scripts/sc2printer.py | 48 +++++++------ sc2reader/scripts/sc2replayer.py | 10 ++- sc2reader/scripts/utils.py | 3 - sc2reader/utils.py | 15 ++-- test_replays/test_replays.py | 69 ++++++++----------- test_s2gs/test_all.py | 2 - 46 files changed, 334 insertions(+), 469 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0799ef91..9c0a5914 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,11 +25,6 @@ jobs: - run: black . --check - Python2: - docker: - - image: circleci/python:2.7.18 - steps: *build_and_test_steps - Python3: docker: - image: circleci/python:3.10 @@ -41,5 +36,4 @@ workflows: build: jobs: - StyleCheck - - Python2 - Python3 diff --git a/docs/source/conf.py b/docs/source/conf.py index 17164624..ead7fdd3 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # sc2reader documentation build configuration file, created by # sphinx-quickstart on Sun May 01 12:39:48 2011. diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index f7091ba4..b7f1ffbd 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """sc2autosave is a utility for reorganizing and renaming Starcraft II files. Overview @@ -78,16 +77,16 @@ keeps the script from looking into the 'Saved' subdirectory. sc2autosave \ - --source ~/My\ Documents/Starcraft\ II/Accounts/.../Mutliplayer \ - --dest ~/My\ Documents/Starcraft\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ --period 10 \ --depth 0 This next configuration runs in batch mode using the default renaming format. sc2autosave \ - --source ~/My\ Documents/Starcraft\ II/Accounts/.../Mutliplayer \ - --dest ~/My\ Documents/Starcraft\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ --rename (ZvP) Lost Temple: ShadesofGray(Z) vs Trisfall(P).SC2Replay @@ -97,8 +96,8 @@ by replay format and favors ShadesofGray in the player and team orderings. sc2autosave \ - --source ~/My\ Documents/Starcraft\ II/Accounts/.../Mutliplayer \ - --dest ~/My\ Documents/Starcraft\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ --rename "{:format}/{:matchup} on {:map}: {:teams}" \ --player-format "{:name}({:play_race})" \ --team-order-by number \ @@ -113,8 +112,8 @@ length to show both minutes and seconds. sc2autosave \ - --source ~/My\ Documents/Starcraft\ II/Accounts/.../Mutliplayer \ - --dest ~/My\ Documents/Starcraft\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ --rename "{:matchup}/({:length}) {:map}: {:teams}" \ --player-format "{:name}({:play_race})" \ --team-order-by number \ @@ -200,7 +199,7 @@ def run(args): directory = make_directory(args, ("parse_error",)) new_path = os.path.join(directory, file_name) source_path = path[len(args.source) :] - args.log.write("Error parsing replay: {0}".format(source_path)) + args.log.write(f"Error parsing replay: {source_path}") if not args.dryrun: args.action.run(path, new_path) @@ -250,7 +249,7 @@ def run(args): def filter_out_replay(args, replay): - player_names = set([player.name for player in replay.players]) + player_names = {player.name for player in replay.players} filter_out_player = not set(args.filter_player) & player_names if args.filter_rule == "ALLOW": @@ -262,7 +261,7 @@ def filter_out_replay(args, replay): # We need to create these compare functions at runtime because the ordering # hinges on the --favored PLAYER options passed in from the command line. def create_compare_funcs(args): - favored_set = set(name.lower() for name in args.favored) + favored_set = {name.lower() for name in args.favored} def player_compare(player1, player2): # Normalize the player names and generate our key metrics @@ -290,8 +289,8 @@ def player_compare(player1, player2): def team_compare(team1, team2): # Normalize the team name lists and generate our key metrics - team1_names = set(p.name.lower() for p in team1.players) - team2_names = set(p.name.lower() for p in team2.players) + team1_names = {p.name.lower() for p in team1.players} + team2_names = {p.name.lower() for p in team2.players} team1_favored = team1_names & favored_set team2_favored = team2_names & favored_set @@ -341,7 +340,7 @@ def make_directory(args, path_parts): for part in path_parts: directory = os.path.join(directory, part) if not os.path.exists(directory): - args.log.write("Creating subfolder: {0}\n".format(directory)) + args.log.write(f"Creating subfolder: {directory}\n") if not args.dryrun: os.mkdir(directory) elif not os.path.isdir(directory): @@ -351,7 +350,7 @@ def make_directory(args, path_parts): def scan(args, state): - args.log.write("SCANNING: {0}\n".format(args.source)) + args.log.write(f"SCANNING: {args.source}\n") files = sc2reader.utils.get_files( path=args.source, regex=args.exclude_files, @@ -374,13 +373,13 @@ def reset(args): exit("Cannot reset, destination must be directory: {0}", args.dest) print( - "About to reset directory: {0}\nAll files and subdirectories will be removed.".format( + "About to reset directory: {}\nAll files and subdirectories will be removed.".format( args.dest ) ) choice = raw_input("Proceed anyway? (y/n) ") if choice.lower() == "y": - args.log.write("Removing old directory: {0}\n".format(args.dest)) + args.log.write(f"Removing old directory: {args.dest}\n") if not args.dryrun: print(args.dest) shutil.rmtree(args.dest) @@ -404,13 +403,13 @@ def setup(args): if not args.dryrun: os.mkdir(args.dest) else: - args.log.write("Creating destination: {0}\n".format(args.dest)) + args.log.write(f"Creating destination: {args.dest}\n") elif not os.path.isdir(args.dest): sys.exit("Destination must be a directory.\n\nScript Aborted") data_file = os.path.join(args.dest, "sc2autosave.dat") - args.log.write("Loading state from file: {0}\n".format(data_file)) + args.log.write(f"Loading state from file: {data_file}\n") if os.path.isfile(data_file) and not args.reset: with open(data_file) as file: return cPickle.load(file) @@ -425,7 +424,7 @@ def save_state(state, args): with open(data_file, "w") as file: cPickle.dump(state, file) else: - args.log.write("Writing state to file: {0}\n".format(data_file)) + args.log.write(f"Writing state to file: {data_file}\n") def main(): diff --git a/examples/sc2store.py b/examples/sc2store.py index 0a072f43..6aff2a98 100755 --- a/examples/sc2store.py +++ b/examples/sc2store.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- import cPickle import os @@ -196,7 +195,7 @@ def main(): for path in args.paths: for file_name in sc2reader.utils.get_files(path, depth=0): - print("CREATING: {0}".format(file_name)) + print(f"CREATING: {file_name}") db.add(Game(sc2reader.read_file(file_name), db)) db.commit() diff --git a/generate_build_data.py b/generate_build_data.py index 32bdbbd2..a13f9807 100644 --- a/generate_build_data.py +++ b/generate_build_data.py @@ -58,7 +58,7 @@ def generate_build_data(balance_data_path): elif unit_id == "Drone": build_ability_name = "ZergBuild" else: - build_ability_name = "{}Build".format(unit_id) + build_ability_name = f"{unit_id}Build" if build_ability_index: abilities[build_ability_index] = build_ability_name @@ -77,7 +77,7 @@ def generate_build_data(balance_data_path): while len(ability_lookup[build_ability_name]) <= command_index: ability_lookup[build_ability_name].append("") - build_command_name = "Build{}".format(built_unit_id) + build_command_name = f"Build{built_unit_id}" ability_lookup[build_ability_name][ command_index ] = build_command_name @@ -87,7 +87,7 @@ def generate_build_data(balance_data_path): train_ability_index = train_unit_elements[0].get("ability") if train_ability_index: - train_ability_name = "{}Train".format(unit_id) + train_ability_name = f"{unit_id}Train" abilities[train_ability_index] = train_ability_name if train_ability_name not in ability_lookup: @@ -137,7 +137,7 @@ def generate_build_data(balance_data_path): ): ability_lookup[train_ability_name].append("") - train_command_name = "Train{}".format(trained_unit_name) + train_command_name = f"Train{trained_unit_name}" ability_lookup[train_ability_name][ command_index ] = train_command_name @@ -145,7 +145,7 @@ def generate_build_data(balance_data_path): research_upgrade_elements = root.findall("./researches/upgrade") if research_upgrade_elements: research_ability_index = research_upgrade_elements[0].get("ability") - research_ability_name = "{}Research".format(unit_id) + research_ability_name = f"{unit_id}Research" abilities[research_ability_index] = research_ability_name @@ -163,7 +163,7 @@ def generate_build_data(balance_data_path): while len(ability_lookup[research_ability_name]) <= command_index: ability_lookup[research_ability_name].append("") - research_command_name = "Research{}".format(researched_upgrade_id) + research_command_name = f"Research{researched_upgrade_id}" ability_lookup[research_ability_name][ command_index ] = research_command_name @@ -175,7 +175,7 @@ def generate_build_data(balance_data_path): sorted(abilities.items(), key=lambda x: int(x[0])) ) - unit_lookup = dict((unit_name, unit_name) for _, unit_name in sorted_units.items()) + unit_lookup = {unit_name: unit_name for _, unit_name in sorted_units.items()} return sorted_units, sorted_abilities, unit_lookup, ability_lookup @@ -258,7 +258,7 @@ def main(): unit_lookup_path = os.path.join( args.project_path, "sc2reader", "data", "unit_lookup.csv" ) - with open(unit_lookup_path, "r") as file: + with open(unit_lookup_path) as file: csv_reader = csv.reader(file, delimiter=",", lineterminator=os.linesep) old_unit_lookup = collections.OrderedDict( [(row[0], row[1]) for row in csv_reader if len(row) > 1] @@ -267,7 +267,7 @@ def main(): ability_lookup_path = os.path.join( args.project_path, "sc2reader", "data", "ability_lookup.csv" ) - with open(ability_lookup_path, "r") as file: + with open(ability_lookup_path) as file: csv_reader = csv.reader(file, delimiter=",", lineterminator=os.linesep) old_ability_lookup = collections.OrderedDict( [(row[0], row[1:]) for row in csv_reader if len(row) > 0] @@ -290,7 +290,7 @@ def main(): "sc2reader", "data", args.expansion, - "{}_units.csv".format(args.build_version), + f"{args.build_version}_units.csv", ) with open(units_file_path, "w") as file: csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) @@ -302,7 +302,7 @@ def main(): "sc2reader", "data", args.expansion, - "{}_abilities.csv".format(args.build_version), + f"{args.build_version}_abilities.csv", ) with open(abilities_file_path, "w") as file: csv_writer = csv.writer(file, delimiter=",", lineterminator=os.linesep) diff --git a/new_units.py b/new_units.py index dcc609a0..86d82ee5 100644 --- a/new_units.py +++ b/new_units.py @@ -15,11 +15,11 @@ str_id, title = entry.strip().split(",") UNIT_LOOKUP[str_id] = title -with open(sys.argv[1], "r") as new_units: +with open(sys.argv[1]) as new_units: for line in new_units: new_unit_name = line.strip().split(",")[1] if new_unit_name not in UNIT_LOOKUP: - print("{0},{1}".format(new_unit_name, new_unit_name)) + print(f"{new_unit_name},{new_unit_name}") print("") print("") @@ -31,8 +31,8 @@ str_id, abilities = entry.split(",", 1) ABIL_LOOKUP[str_id] = abilities.split(",") -with open(sys.argv[2], "r") as new_abilities: +with open(sys.argv[2]) as new_abilities: for line in new_abilities: new_ability_name = line.strip().split(",")[1] if new_ability_name not in ABIL_LOOKUP: - print("{0},{1}".format(new_ability_name, new_ability_name)) + print(f"{new_ability_name},{new_ability_name}") diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 419f47b5..415b270d 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ sc2reader ~~~~~~~~~~~ @@ -18,7 +17,6 @@ :copyright: (c) 2011 by Graylin Kim. :license: MIT, see LICENSE for more details. """ -from __future__ import absolute_import, print_function, unicode_literals, division __version__ = "1.8.0" diff --git a/sc2reader/constants.py b/sc2reader/constants.py index 1edfa70e..db34afa7 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - # These are found in Repack-MPQ/fileset.{locale}#Mods#Core.SC2Mod#{locale}.SC2Data/LocalizedData/Editor/EditorCategoryStrings.txt # EDSTR_CATEGORY_Race # EDSTR_PLAYERPROPS_RACE diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 6cbb9425..cb333d47 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import json import pkgutil @@ -41,7 +38,7 @@ train_commands = json.loads(command_data) -class Unit(object): +class Unit: """Represents an in-game unit.""" def __init__(self, unit_id): @@ -190,7 +187,7 @@ def is_army(self): return self._type_class.is_army if self._type_class else False def __str__(self): - return "{0} [{1:X}]".format(self.name, self.id) + return f"{self.name} [{self.id:X}]" def __cmp__(self, other): return cmp(self.id, other.id) @@ -220,7 +217,7 @@ def __repr__(self): return str(self) -class UnitType(object): +class UnitType: """Represents an in game unit type""" def __init__( @@ -271,7 +268,7 @@ def __init__( self.is_army = is_army -class Ability(object): +class Ability: """Represents an in-game ability""" def __init__( @@ -297,7 +294,7 @@ def __init__( @loggable -class Build(object): +class Build: """ :param build_id: The build number identifying this dataset. @@ -342,7 +339,7 @@ def change_type(self, unit, new_type, frame): unit.set_type(unit_type, frame) else: self.logger.error( - "Unable to change type of {0} to {1} [frame {2}]; unit type not found in build {3}".format( + "Unable to change type of {} to {} [frame {}]; unit type not found in build {}".format( unit, new_type, frame, self.id ) ) @@ -402,7 +399,7 @@ def add_unit_type( def load_build(expansion, version): build = Build(version) - unit_file = "{0}/{1}_units.csv".format(expansion, version) + unit_file = f"{expansion}/{version}_units.csv" for entry in ( pkgutil.get_data("sc2reader.data", unit_file).decode("utf8").split("\n") ): @@ -421,7 +418,7 @@ def load_build(expansion, version): build.add_unit_type(**values) - abil_file = "{0}/{1}_abilities.csv".format(expansion, version) + abil_file = f"{expansion}/{version}_abilities.csv" build.add_ability(ability_id=0, name="RightClick", title="Right Click") for entry in ( pkgutil.get_data("sc2reader.data", abil_file).decode("utf8").split("\n") diff --git a/sc2reader/data/create_lookup.py b/sc2reader/data/create_lookup.py index 2ffa2560..427d1e27 100755 --- a/sc2reader/data/create_lookup.py +++ b/sc2reader/data/create_lookup.py @@ -1,10 +1,10 @@ abilities = dict() -with open("hots_abilities.csv", "r") as f: +with open("hots_abilities.csv") as f: for line in f: num, ability = line.strip("\r\n ").split(",") abilities[ability] = [""] * 32 -with open("command_lookup.csv", "r") as f: +with open("command_lookup.csv") as f: for line in f: ability, commands = line.strip("\r\n ").split("|", 1) abilities[ability] = commands.split("|") diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index 56674a1c..9085ab7d 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from io import BytesIO import struct @@ -12,7 +9,7 @@ from ordereddict import OrderedDict -class ByteDecoder(object): +class ByteDecoder: """ :param contents: The string or file-like object to decode :param endian: Either > or <. Indicates the endian the bytes are stored in. @@ -144,7 +141,7 @@ def read_cstring(self, encoding="utf8"): cstring.write(c) -class BitPackedDecoder(object): +class BitPackedDecoder: """ :param contents: The string of file-like object to decode @@ -306,9 +303,9 @@ def read_bytes(self, count): temp_buffer = BytesIO() prev_byte = self._next_byte lo_mask, hi_mask = self._bit_masks[self._bit_shift] - for next_byte in struct.unpack(str("B") * count, data): + for next_byte in struct.unpack("B" * count, data): temp_buffer.write( - struct.pack(str("B"), prev_byte & hi_mask | next_byte & lo_mask) + struct.pack("B", prev_byte & hi_mask | next_byte & lo_mask) ) prev_byte = next_byte @@ -357,7 +354,7 @@ def read_bits(self, count): result |= self._buffer.read_uint32() << bits else: - for byte in struct.unpack(str("B") * bytes, self._read(bytes)): + for byte in struct.unpack("B" * bytes, self._read(bytes)): bits -= 8 result |= byte << bits @@ -413,9 +410,9 @@ def read_struct(self, datatype=None): elif datatype == 0x05: # Struct entries = self.read_vint() - data = dict( - [(self.read_vint(), self.read_struct()) for i in range(entries)] - ) + data = { + self.read_vint(): self.read_struct() for i in range(entries) + } elif datatype == 0x06: # u8 data = ord(self._buffer.read(1)) @@ -430,6 +427,6 @@ def read_struct(self, datatype=None): data = self.read_vint() else: - raise TypeError("Unknown Data Structure: '{0}'".format(datatype)) + raise TypeError(f"Unknown Data Structure: '{datatype}'") return data diff --git a/sc2reader/engine/__init__.py b/sc2reader/engine/__init__.py index b3b8ad87..e76bf41b 100644 --- a/sc2reader/engine/__init__.py +++ b/sc2reader/engine/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import sys from sc2reader.engine.engine import GameEngine from sc2reader.engine.events import PluginExit diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index 15c0af66..9c7ee68f 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import collections from sc2reader.events import * from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit -class GameEngine(object): +class GameEngine: """ GameEngine Specification -------------------------- @@ -200,7 +197,7 @@ def run(self, replay): def _get_event_handlers(self, event, plugins): return sum( - [self._get_plugin_event_handlers(plugin, event) for plugin in plugins], [] + (self._get_plugin_event_handlers(plugin, event) for plugin in plugins), [] ) def _get_plugin_event_handlers(self, plugin, event): diff --git a/sc2reader/engine/events.py b/sc2reader/engine/events.py index 8857a4b1..fc6d4728 100644 --- a/sc2reader/engine/events.py +++ b/sc2reader/engine/events.py @@ -1,16 +1,12 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - - -class InitGameEvent(object): +class InitGameEvent: name = "InitGame" -class EndGameEvent(object): +class EndGameEvent: name = "EndGame" -class PluginExit(object): +class PluginExit: name = "PluginExit" def __init__(self, plugin, code=0, details=None): diff --git a/sc2reader/engine/plugins/__init__.py b/sc2reader/engine/plugins/__init__.py index 7ddff085..ca62c70c 100644 --- a/sc2reader/engine/plugins/__init__.py +++ b/sc2reader/engine/plugins/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from sc2reader.engine.plugins.apm import APMTracker from sc2reader.engine.plugins.selection import SelectionTracker from sc2reader.engine.plugins.context import ContextLoader diff --git a/sc2reader/engine/plugins/apm.py b/sc2reader/engine/plugins/apm.py index ec6fee7c..d1b7ad38 100644 --- a/sc2reader/engine/plugins/apm.py +++ b/sc2reader/engine/plugins/apm.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from collections import defaultdict -class APMTracker(object): +class APMTracker: """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is any Selection, ControlGroup, or Command event. diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 5a8fb34a..1a07ca6c 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -1,13 +1,11 @@ -# -*- coding: utf-8 -*- # TODO: Dry this up a bit! -from __future__ import absolute_import, print_function, unicode_literals, division from sc2reader.log_utils import loggable from sc2reader.utils import Length @loggable -class ContextLoader(object): +class ContextLoader: name = "ContextLoader" def handleInitGame(self, event, replay): @@ -45,7 +43,7 @@ def handleCommandEvent(self, event, replay): event.logger.error("\t" + player.__str__()) self.logger.error( - "{0}\t{1}\tMissing ability {2:X} from {3}".format( + "{}\t{}\tMissing ability {:X} from {}".format( event.frame, event.player.name, event.ability_id, @@ -60,7 +58,7 @@ def handleCommandEvent(self, event, replay): if event.other_unit_id in replay.objects: event.other_unit = replay.objects[event.other_unit_id] elif event.other_unit_id is not None: - self.logger.error("Other unit {0} not found".format(event.other_unit_id)) + self.logger.error(f"Other unit {event.other_unit_id} not found") def handleTargetUnitCommandEvent(self, event, replay): self.last_target_ability_event[event.player.pid] = event @@ -199,13 +197,13 @@ def handleUnitDiedEvent(self, event, replay): del replay.active_units[event.unit_id_index] else: self.logger.error( - "Unable to delete unit index {0} at {1} [{2}], index not active.".format( + "Unable to delete unit index {} at {} [{}], index not active.".format( event.killer_pid, Length(seconds=event.second), event.frame ) ) else: self.logger.error( - "Unit {0} died at {1} [{2}] before it was born!".format( + "Unit {} died at {} [{}] before it was born!".format( event.unit_id, Length(seconds=event.second), event.frame ) ) @@ -217,7 +215,7 @@ def handleUnitDiedEvent(self, event, replay): event.killing_player.killed_units.append(event.unit) elif event.killing_player_id: self.logger.error( - "Unknown killing player id {0} at {1} [{2}]".format( + "Unknown killing player id {} at {} [{}]".format( event.killing_player_id, Length(seconds=event.second), event.frame ) ) @@ -229,7 +227,7 @@ def handleUnitDiedEvent(self, event, replay): event.killing_unit.killed_units.append(event.unit) elif event.killing_unit_id: self.logger.error( - "Unknown killing unit id {0} at {1} [{2}]".format( + "Unknown killing unit id {} at {} [{}]".format( event.killing_unit_id, Length(seconds=event.second), event.frame ) ) @@ -245,7 +243,7 @@ def handleUnitOwnerChangeEvent(self, event, replay): event.unit = replay.objects[event.unit_id] else: self.logger.error( - "Unit {0} owner changed at {1} [{2}] before it was born!".format( + "Unit {} owner changed at {} [{}] before it was born!".format( event.unit_id, Length(seconds=event.second), event.frame ) ) @@ -265,7 +263,7 @@ def handleUnitTypeChangeEvent(self, event, replay): replay.datapack.change_type(event.unit, event.unit_type_name, event.frame) else: self.logger.error( - "Unit {0} type changed at {1} [{2}] before it was born!".format( + "Unit {} type changed at {} [{}] before it was born!".format( event.unit_id, Length(seconds=event.second) ) ) @@ -308,7 +306,7 @@ def handleUnitDoneEvent(self, event, replay): event.unit.finished_at = event.frame else: self.logger.error( - "Unit {0} done at {1} [{2}] before it was started!".format( + "Unit {} done at {} [{}] before it was started!".format( event.killer_pid, Length(seconds=event.second), event.frame ) ) @@ -324,7 +322,7 @@ def handleUnitPositionsEvent(self, event, replay): event.units[unit] = unit.location else: self.logger.error( - "Unit at active_unit index {0} moved at {1} [{2}] but it doesn't exist!".format( + "Unit at active_unit index {} moved at {} [{}] but it doesn't exist!".format( event.killer_pid, Length(seconds=event.second), event.frame ) ) @@ -338,7 +336,7 @@ def load_message_game_player(self, event, replay): event.player.events.append(event) elif event.pid != 16: self.logger.error( - "Bad pid ({0}) for event {1} at {2} [{3}].".format( + "Bad pid ({}) for event {} at {} [{}].".format( event.pid, event.__class__, Length(seconds=event.second), @@ -354,7 +352,7 @@ def load_message_game_player(self, event, replay): event.player.events.append(event) elif event.pid != 16: self.logger.error( - "Bad pid ({0}) for event {1} at {2} [{3}].".format( + "Bad pid ({}) for event {} at {} [{}].".format( event.pid, event.__class__, Length(seconds=event.second), @@ -369,7 +367,7 @@ def load_tracker_player(self, event, replay): event.player = replay.entity[event.pid] else: self.logger.error( - "Bad pid ({0}) for event {1} at {2} [{3}].".format( + "Bad pid ({}) for event {} at {} [{}].".format( event.pid, event.__class__, Length(seconds=event.second), @@ -382,7 +380,7 @@ def load_tracker_upkeeper(self, event, replay): event.unit_upkeeper = replay.entity[event.upkeep_pid] elif event.upkeep_pid != 0: self.logger.error( - "Bad upkeep_pid ({0}) for event {1} at {2} [{3}].".format( + "Bad upkeep_pid ({}) for event {} at {} [{}].".format( event.upkeep_pid, event.__class__, Length(seconds=event.second), @@ -395,7 +393,7 @@ def load_tracker_controller(self, event, replay): event.unit_controller = replay.entity[event.control_pid] elif event.control_pid != 0: self.logger.error( - "Bad control_pid ({0}) for event {1} at {2} [{3}].".format( + "Bad control_pid ({}) for event {} at {} [{}].".format( event.control_pid, event.__class__, Length(seconds=event.second), diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 5a56b4f2..90f01e0c 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from io import BytesIO try: @@ -21,7 +18,7 @@ from itertools import tee # The creep tracker plugin -class CreepTracker(object): +class CreepTracker: """ The Creep tracker populates player.max_creep_spread and player.creep_spread by minute @@ -41,14 +38,14 @@ def handleInitGame(self, event, replay): if player.play_race[0] == "Z": self.creepTracker.init_cgu_lists(player.pid) except Exception as e: - print("Whoa! {}".format(e)) + print(f"Whoa! {e}") pass def handleUnitDiedEvent(self, event, replay): try: self.creepTracker.remove_from_list(event.unit_id, event.second) except Exception as e: - print("Whoa! {}".format(e)) + print(f"Whoa! {e}") pass def handleUnitInitEvent(self, event, replay): @@ -62,7 +59,7 @@ def handleUnitInitEvent(self, event, replay): event.second, ) except Exception as e: - print("Whoa! {}".format(e)) + print(f"Whoa! {e}") pass def handleUnitBornEvent(self, event, replay): @@ -76,7 +73,7 @@ def handleUnitBornEvent(self, event, replay): event.second, ) except Exception as e: - print("Whoa! {}".format(e)) + print(f"Whoa! {e}") pass def handleEndGame(self, event, replay): @@ -98,7 +95,7 @@ def handleEndGame(self, event, replay): ## Else statement is for players with no creep spread(ie: not Zerg) player.max_creep_spread = 0 except Exception as e: - print("Whoa! {}".format(e)) + print(f"Whoa! {e}") pass diff --git a/sc2reader/engine/plugins/gameheart.py b/sc2reader/engine/plugins/gameheart.py index d3905624..f2af2e82 100644 --- a/sc2reader/engine/plugins/gameheart.py +++ b/sc2reader/engine/plugins/gameheart.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from datetime import datetime from sc2reader.utils import Length, get_real_type from sc2reader.objects import Observer, Team @@ -8,7 +5,7 @@ from sc2reader.constants import GAME_SPEED_FACTOR -class GameHeartNormalizer(object): +class GameHeartNormalizer: """ normalize a GameHeart replay to: 1) reset frames to the game start diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 52f50c65..006ac78e 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -1,8 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - - -class SelectionTracker(object): +class SelectionTracker: """ Tracks a player's active selection as an input into other plugins. diff --git a/sc2reader/engine/plugins/supply.py b/sc2reader/engine/plugins/supply.py index 8305e50b..6935b847 100644 --- a/sc2reader/engine/plugins/supply.py +++ b/sc2reader/engine/plugins/supply.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from collections import defaultdict -class SupplyTracker(object): +class SupplyTracker: def add_to_units_alive(self, event, replay): unit_name = event.unit_type_name if unit_name in self.unit_name_to_supply: @@ -14,7 +11,7 @@ def add_to_units_alive(self, event, replay): time_built = 0 if time_built < 0 else time_built new_unit = (supplyCount, event.unit_id) self.units_alive[event.control_pid].append(new_unit) - total_supply = sum([x[0] for x in self.units_alive[event.control_pid]]) + total_supply = sum(x[0] for x in self.units_alive[event.control_pid]) replay.players[event.control_pid - 1].current_food_used[ time_built ] = total_supply @@ -33,7 +30,7 @@ def add_to_units_alive(self, event, replay): time_complete = event.second + build_time supply_gen_unit = (supply_gen_count, event.unit_id) self.supply_gen[event.control_pid].append(supply_gen_unit) - total_supply_gen = sum([x[0] for x in self.supply_gen[event.control_pid]]) + total_supply_gen = sum(x[0] for x in self.supply_gen[event.control_pid]) replay.players[event.control_pid - 1].current_food_made[ time_complete ] = total_supply_gen @@ -45,7 +42,7 @@ def add_to_units_alive(self, event, replay): replay.players[event.control_pid - 1].current_food_made[time_complete], ) else: - print("Unit name {0} does not exist".format(event.unit_type_name)) + print(f"Unit name {event.unit_type_name} does not exist") return def remove_from_units_alive(self, event, replay): @@ -54,7 +51,7 @@ def remove_from_units_alive(self, event, replay): dead_unit = filter(lambda x: x[1] == died_unit_id, self.units_alive[player]) if dead_unit: self.units_alive[player].remove(dead_unit[0]) - total_supply = sum([x[0] for x in self.units_alive[player]]) + total_supply = sum(x[0] for x in self.units_alive[player]) replay.players[player - 1].current_food_used[ event.second @@ -73,7 +70,7 @@ def remove_from_units_alive(self, event, replay): ) if dead_supply_gen: self.supply_gen[player].remove(dead_supply_gen[0]) - total_supply_gen = sum([x[0] for x in self.supply_gen[player]]) + total_supply_gen = sum(x[0] for x in self.supply_gen[player]) replay.players[player - 1].current_food_made[ event.second ] = total_supply_gen diff --git a/sc2reader/engine/utils.py b/sc2reader/engine/utils.py index c8ca4af5..4c62597e 100644 --- a/sc2reader/engine/utils.py +++ b/sc2reader/engine/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from bisect import bisect_left @@ -13,7 +10,7 @@ def __init__(self, initial_state): def __getitem__(self, frame): if frame in self: - return super(GameState, self).__getitem__(frame) + return super().__getitem__(frame) # Get the previous frame from our sorted frame list # bisect_left returns the left most key where an item is @@ -42,4 +39,4 @@ def __setitem__(self, frame, value): self._frames.insert(bisect_left(self._frames, frame), frame) self._frameset.add(frame) - super(GameState, self).__setitem__(frame, value) + super().__setitem__(frame, value) diff --git a/sc2reader/events/__init__.py b/sc2reader/events/__init__.py index 6ceaa632..126f482a 100644 --- a/sc2reader/events/__init__.py +++ b/sc2reader/events/__init__.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - # Export all events of all types to the package interface from sc2reader.events import base, game, message, tracker from sc2reader.events.base import * diff --git a/sc2reader/events/base.py b/sc2reader/events/base.py index 89f16c5e..7245c4e2 100644 --- a/sc2reader/events/base.py +++ b/sc2reader/events/base.py @@ -1,6 +1,2 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - - -class Event(object): +class Event: name = "Event" diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 82232011..2d04c4c2 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from sc2reader.utils import Length from sc2reader.events.base import Event from sc2reader.log_utils import loggable @@ -41,14 +38,14 @@ def _str_prefix(self): if getattr(self, "pid", 16) == 16: player_name = "Global" elif self.player and not self.player.name: - player_name = "Player {0} - ({1})".format( + player_name = "Player {} - ({})".format( self.player.pid, self.player.play_race ) elif self.player: player_name = self.player.name else: player_name = "no name" - return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) + return f"{Length(seconds=int(self.frame / 16))}\t{player_name:<15} " def __str__(self): return self._str_prefix() + self.name @@ -61,7 +58,7 @@ class GameStartEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(GameStartEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: ??? self.data = data @@ -73,7 +70,7 @@ class PlayerLeaveEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(PlayerLeaveEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: ??? self.data = data @@ -86,7 +83,7 @@ class UserOptionsEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(UserOptionsEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: self.game_fully_downloaded = data["game_fully_downloaded"] @@ -145,7 +142,7 @@ class CommandEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(CommandEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: Flags on the command??? self.flags = data["flags"] @@ -240,19 +237,19 @@ def __init__(self, frame, pid, data): def __str__(self): string = self._str_prefix() if self.has_ability: - string += "Ability ({0:X})".format(self.ability_id) + string += f"Ability ({self.ability_id:X})" if self.ability: - string += " - {0}".format(self.ability.name) + string += f" - {self.ability.name}" else: string += "Right Click" if self.ability_type == "TargetUnit": - string += "; Target: {0} [{1:0>8X}]".format( + string += "; Target: {} [{:0>8X}]".format( self.target.name, self.target_unit_id ) if self.ability_type in ("TargetPoint", "TargetUnit"): - string += "; Location: {0}".format(str(self.location)) + string += f"; Location: {str(self.location)}" return string @@ -268,7 +265,7 @@ class BasicCommandEvent(CommandEvent): """ def __init__(self, frame, pid, data): - super(BasicCommandEvent, self).__init__(frame, pid, data) + super().__init__(frame, pid, data) class TargetPointCommandEvent(CommandEvent): @@ -284,7 +281,7 @@ class TargetPointCommandEvent(CommandEvent): """ def __init__(self, frame, pid, data): - super(TargetPointCommandEvent, self).__init__(frame, pid, data) + super().__init__(frame, pid, data) #: The x coordinate of the target. Available for TargetPoint and TargetUnit type events. self.x = self.ability_type_data["point"].get("x", 0) / 4096.0 @@ -312,7 +309,7 @@ class TargetUnitCommandEvent(CommandEvent): """ def __init__(self, frame, pid, data): - super(TargetUnitCommandEvent, self).__init__(frame, pid, data) + super().__init__(frame, pid, data) #: Flags set on the target unit. Available for TargetUnit type events self.target_flags = self.ability_type_data.get("flags", None) @@ -393,7 +390,7 @@ class DataCommandEvent(CommandEvent): """ def __init__(self, frame, pid, data): - super(DataCommandEvent, self).__init__(frame, pid, data) + super().__init__(frame, pid, data) #: Other target data. Available for Data type events. self.target_data = self.ability_type_data.get("data", None) @@ -410,7 +407,7 @@ class CommandManagerStateEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(CommandManagerStateEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: Always 1? self.state = data["state"] @@ -433,7 +430,7 @@ class SelectionEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(SelectionEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The control group being modified. 10 for active selection self.control_group = data["control_group_index"] @@ -554,7 +551,7 @@ class ControlGroupEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(ControlGroupEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: Index to the control group being modified self.control_group = data["control_group_index"] @@ -612,7 +609,7 @@ class CameraEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(CameraEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The x coordinate of the center of the camera self.x = (data["target"]["x"] if data["target"] is not None else 0) / 256.0 @@ -633,7 +630,7 @@ def __init__(self, frame, pid, data): self.yaw = data["yaw"] def __str__(self): - return self._str_prefix() + "{0} at ({1}, {2})".format( + return self._str_prefix() + "{} at ({}, {})".format( self.name, self.x, self.y ) @@ -646,7 +643,7 @@ class ResourceTradeEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(ResourceTradeEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The id of the player sending the resources self.sender_id = pid @@ -676,7 +673,7 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " transfer {0} minerals, {1} gas, {2} terrazine, and {3} custom to {4}".format( + return self._str_prefix() + " transfer {} minerals, {} gas, {} terrazine, and {} custom to {}".format( self.minerals, self.vespene, self.terrazine, @@ -691,7 +688,7 @@ class ResourceRequestEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(ResourceRequestEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: An array of resources sent self.resources = data["resources"] @@ -711,7 +708,7 @@ def __init__(self, frame, pid, data): def __str__(self): return ( self._str_prefix() - + " requests {0} minerals, {1} gas, {2} terrazine, and {3} custom".format( + + " requests {} minerals, {} gas, {} terrazine, and {} custom".format( self.minerals, self.vespene, self.terrazine, self.custom_resource ) ) @@ -723,7 +720,7 @@ class ResourceRequestFulfillEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(ResourceRequestFulfillEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The id of the request being fulfilled self.request_id = data["request_id"] @@ -735,7 +732,7 @@ class ResourceRequestCancelEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(ResourceRequestCancelEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The id of the request being cancelled self.request_id = data["request_id"] @@ -747,7 +744,7 @@ class HijackReplayGameEvent(GameEvent): """ def __init__(self, frame, pid, data): - super(HijackReplayGameEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The method used. Not sure what 0/1 represent self.method = data["method"] diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index 57da5dce..c1848f64 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from sc2reader.events.base import Event from sc2reader.utils import Length from sc2reader.log_utils import loggable @@ -27,7 +24,7 @@ def __init__(self, frame, pid): def _str_prefix(self): player_name = self.player.name if getattr(self, "pid", 16) != 16 else "Global" - return "{0}\t{1:<15} ".format(Length(seconds=int(self.frame / 16)), player_name) + return f"{Length(seconds=int(self.frame / 16))}\t{player_name:<15} " def __str__(self): return self._str_prefix() + self.name @@ -40,7 +37,7 @@ class ChatEvent(MessageEvent): """ def __init__(self, frame, pid, target, text): - super(ChatEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The numerical target type. 0 = to all; 2 = to allies; 4 = to observers. self.target = target @@ -64,7 +61,7 @@ class ProgressEvent(MessageEvent): """ def __init__(self, frame, pid, progress): - super(ProgressEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: Marks the load progress for the player. Scaled 0-100. self.progress = progress @@ -77,7 +74,7 @@ class PingEvent(MessageEvent): """ def __init__(self, frame, pid, target, x, y): - super(PingEvent, self).__init__(frame, pid) + super().__init__(frame, pid) #: The numerical target type. 0 = to all; 2 = to allies; 4 = to observers. self.target = target diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 6220d5fe..7355ef35 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import functools from sc2reader.events.base import Event @@ -29,7 +26,7 @@ def load_context(self, replay): pass def _str_prefix(self): - return "{0}\t ".format(Length(seconds=int(self.frame / 16))) + return f"{Length(seconds=int(self.frame / 16))}\t " def __str__(self): return self._str_prefix() + self.name @@ -39,7 +36,7 @@ class PlayerSetupEvent(TrackerEvent): """Sent during game setup to help us organize players better""" def __init__(self, frames, data, build): - super(PlayerSetupEvent, self).__init__(frames) + super().__init__(frames) #: The player id of the player we are setting up self.pid = data[0] @@ -68,7 +65,7 @@ class PlayerStatsEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(PlayerStatsEvent, self).__init__(frames) + super().__init__(frames) #: Id of the player the stats are for self.pid = data[0] @@ -275,7 +272,7 @@ def __init__(self, frames, data, build): ) def __str__(self): - return self._str_prefix() + "{0: >15} - Stats Update".format(str(self.player)) + return self._str_prefix() + f"{str(self.player): >15} - Stats Update" class UnitBornEvent(TrackerEvent): @@ -291,7 +288,7 @@ class UnitBornEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitBornEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -337,7 +334,7 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit born {1}".format( + return self._str_prefix() + "{: >15} - Unit born {}".format( str(self.unit_upkeeper), self.unit ) @@ -349,7 +346,7 @@ class UnitDiedEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitDiedEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -412,7 +409,7 @@ def __init__(self, frames, data, build): ) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit died {1}.".format( + return self._str_prefix() + "{: >15} - Unit died {}.".format( str(self.unit.owner), self.unit ) @@ -424,7 +421,7 @@ class UnitOwnerChangeEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitOwnerChangeEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -451,7 +448,7 @@ def __init__(self, frames, data, build): self.unit_controller = None def __str__(self): - return self._str_prefix() + "{0: >15} took {1}".format( + return self._str_prefix() + "{: >15} took {}".format( str(self.unit_upkeeper), self.unit ) @@ -464,7 +461,7 @@ class UnitTypeChangeEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitTypeChangeEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -482,7 +479,7 @@ def __init__(self, frames, data, build): self.unit_type_name = data[2].decode("utf8") def __str__(self): - return self._str_prefix() + "{0: >15} - Unit {1} type changed to {2}".format( + return self._str_prefix() + "{: >15} - Unit {} type changed to {}".format( str(self.unit.owner), self.unit, self.unit_type_name ) @@ -493,7 +490,7 @@ class UpgradeCompleteEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UpgradeCompleteEvent, self).__init__(frames) + super().__init__(frames) #: The player that completed the upgrade self.pid = data[0] @@ -508,7 +505,7 @@ def __init__(self, frames, data, build): self.count = data[2] def __str__(self): - return self._str_prefix() + "{0: >15} - {1} upgrade completed".format( + return self._str_prefix() + "{: >15} - {} upgrade completed".format( str(self.player), self.upgrade_type_name ) @@ -521,7 +518,7 @@ class UnitInitEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitInitEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -567,7 +564,7 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{0: >15} - Unit initiated {1}".format( + return self._str_prefix() + "{: >15} - Unit initiated {}".format( str(self.unit_upkeeper), self.unit ) @@ -579,7 +576,7 @@ class UnitDoneEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitDoneEvent, self).__init__(frames) + super().__init__(frames) #: The index portion of the unit id self.unit_id_index = data[0] @@ -594,7 +591,7 @@ def __init__(self, frames, data, build): self.unit = None def __str__(self): - return self._str_prefix() + "{0: >15} - Unit {1} done".format( + return self._str_prefix() + "{: >15} - Unit {} done".format( str(self.unit.owner), self.unit ) @@ -607,7 +604,7 @@ class UnitPositionsEvent(TrackerEvent): """ def __init__(self, frames, data, build): - super(UnitPositionsEvent, self).__init__(frames) + super().__init__(frames) #: The starting unit index point. self.first_unit_index = data[0] diff --git a/sc2reader/exceptions.py b/sc2reader/exceptions.py index dff2d31b..5dc20e81 100644 --- a/sc2reader/exceptions.py +++ b/sc2reader/exceptions.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - - class SC2ReaderError(Exception): pass @@ -29,10 +25,10 @@ class MultipleMatchingFilesError(SC2ReaderError): class ReadError(SC2ReaderError): def __init__(self, msg, type, location, replay=None, game_events=[], buffer=None): self.__dict__.update(locals()) - super(ReadError, self).__init__(msg) + super().__init__(msg) def __str__(self): - return "{0}, Type: {1}".format(self.msg, self.type) + return f"{self.msg}, Type: {self.type}" class ParseError(SC2ReaderError): diff --git a/sc2reader/factories/__init__.py b/sc2reader/factories/__init__.py index c6c469f6..744e1aae 100644 --- a/sc2reader/factories/__init__.py +++ b/sc2reader/factories/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, print_function, unicode_literals, division - from sc2reader.factories.sc2factory import SC2Factory from sc2reader.factories.sc2factory import FileCachedSC2Factory from sc2reader.factories.sc2factory import DictCachedSC2Factory diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index f7669ef7..b3ba681b 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import json from collections import defaultdict @@ -159,7 +156,7 @@ def SelectionTracker(replay): selections[event.control_group] = control_group if debug: logger.info( - "[{0}] {1} selected {2} units: {3}".format( + "[{}] {} selected {} units: {}".format( Length(seconds=event.second), person.name, len(selections[0x0A].objects), @@ -172,7 +169,7 @@ def SelectionTracker(replay): selections[event.control_group] = selections[0x0A].copy() if debug: logger.info( - "[{0}] {1} set hotkey {2} to current selection".format( + "[{}] {} set hotkey {} to current selection".format( Length(seconds=event.second), person.name, event.hotkey ) ) @@ -185,7 +182,7 @@ def SelectionTracker(replay): selections[event.control_group] = control_group if debug: logger.info( - "[{0}] {1} added current selection to hotkey {2}".format( + "[{}] {} added current selection to hotkey {}".format( Length(seconds=event.second), person.name, event.hotkey ) ) @@ -197,7 +194,7 @@ def SelectionTracker(replay): selections[0xA] = control_group if debug: logger.info( - "[{0}] {1} retrieved hotkey {2}, {3} units: {4}".format( + "[{}] {} retrieved hotkey {}, {} units: {}".format( Length(seconds=event.second), person.name, event.control_group, @@ -216,7 +213,7 @@ def SelectionTracker(replay): person.selection_errors += 1 if debug: logger.warn( - "Error detected in deselection mode {0}.".format( + "Error detected in deselection mode {}.".format( event.mask_type ) ) diff --git a/sc2reader/factories/plugins/utils.py b/sc2reader/factories/plugins/utils.py index 7eef123a..f747c36f 100644 --- a/sc2reader/factories/plugins/utils.py +++ b/sc2reader/factories/plugins/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from bisect import bisect_left from collections import defaultdict from datetime import datetime @@ -40,7 +37,7 @@ def __init__(self, initial_state): def __getitem__(self, frame): if frame in self: - return super(GameState, self).__getitem__(frame) + return super().__getitem__(frame) # Get the previous frame from our sorted frame list # bisect_left returns the left most key where an item is @@ -69,11 +66,11 @@ def __setitem__(self, frame, value): self._frames.insert(bisect_left(self._frames, frame), frame) self._frameset.add(frame) - super(GameState, self).__setitem__(frame, value) + super().__setitem__(frame, value) @loggable -class UnitSelection(object): +class UnitSelection: def __init__(self, objects=None): self.objects = objects or list() @@ -95,7 +92,7 @@ def deselect(self, mode, data): # pad to the right mask = mask + [False] * (len(self.objects) - len(mask)) - self.logger.debug("Deselection Mask: {0}".format(mask)) + self.logger.debug(f"Deselection Mask: {mask}") self.objects = [ obj for (slct, obj) in filter( @@ -130,7 +127,7 @@ def copy(self): class PlayerSelection(defaultdict): def __init__(self): - super(PlayerSelection, self).__init__(UnitSelection) + super().__init__(UnitSelection) def copy(self): new = PlayerSelection() diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index 1bde3ac1..e4b38c6f 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from collections import defaultdict from io import BytesIO import os @@ -27,7 +24,7 @@ @log_utils.loggable -class SC2Factory(object): +class SC2Factory: """ The SC2Factory class acts as a generic loader interface for all available to sc2reader resources. At current time this includes @@ -262,7 +259,7 @@ def get_remote_cache_key(self, remote_resource): def load_remote_resource_contents(self, remote_resource, **options): cache_key = self.get_remote_cache_key(remote_resource) if not self.cache_has(cache_key): - resource = super(CachedSC2Factory, self).load_remote_resource_contents( + resource = super().load_remote_resource_contents( remote_resource, **options ) self.cache_set(cache_key, resource) @@ -290,15 +287,15 @@ class FileCachedSC2Factory(CachedSC2Factory): """ def __init__(self, cache_dir, **options): - super(FileCachedSC2Factory, self).__init__(**options) + super().__init__(**options) self.cache_dir = os.path.abspath(cache_dir) if not os.path.isdir(self.cache_dir): raise ValueError( - "cache_dir ({0}) must be an existing directory.".format(self.cache_dir) + f"cache_dir ({self.cache_dir}) must be an existing directory." ) elif not os.access(self.cache_dir, os.F_OK | os.W_OK | os.R_OK): raise ValueError( - "Must have read/write access to {0} for local file caching.".format( + "Must have read/write access to {} for local file caching.".format( self.cache_dir ) ) @@ -333,7 +330,7 @@ class DictCachedSC2Factory(CachedSC2Factory): """ def __init__(self, cache_max_size=0, **options): - super(DictCachedSC2Factory, self).__init__(**options) + super().__init__(**options) self.cache_dict = dict() self.cache_used = dict() self.cache_max_size = cache_max_size @@ -366,7 +363,7 @@ class DoubleCachedSC2Factory(DictCachedSC2Factory, FileCachedSC2Factory): """ def __init__(self, cache_dir, cache_max_size=0, **options): - super(DoubleCachedSC2Factory, self).__init__( + super().__init__( cache_max_size, cache_dir=cache_dir, **options ) diff --git a/sc2reader/log_utils.py b/sc2reader/log_utils.py index c4288820..3337e348 100644 --- a/sc2reader/log_utils.py +++ b/sc2reader/log_utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import logging try: @@ -78,7 +75,7 @@ def get_logger(entity): return logging.getLogger(entity.__module__ + "." + entity.__name__) except AttributeError: - raise TypeError("Cannot retrieve logger for {0}.".format(entity)) + raise TypeError(f"Cannot retrieve logger for {entity}.") def loggable(cls): diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 11ef8eb7..2949b516 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import hashlib import math from collections import namedtuple @@ -12,7 +9,7 @@ Location = namedtuple("Location", ["x", "y"]) -class Team(object): +class Team: """ The team object primarily a container object for organizing :class:`Player` objects with some metadata. As such, it implements iterable and can be @@ -22,7 +19,7 @@ class Team(object): """ #: A unique hash identifying the team of players - hash = str() + hash = '' #: The team number as recorded in the replay number = int() @@ -32,7 +29,7 @@ class Team(object): #: The result of the game for this team. #: One of "Win", "Loss", or "Unknown" - result = str() + result = '' def __init__(self, number): self.number = number @@ -56,7 +53,7 @@ def hash(self): return hashlib.sha256(raw_hash).hexdigest() def __str__(self): - return "Team {0}: {1}".format( + return "Team {}: {}".format( self.number, ", ".join([str(p) for p in self.players]) ) @@ -65,14 +62,14 @@ def __repr__(self): @log_utils.loggable -class Attribute(object): +class Attribute: def __init__(self, header, attr_id, player, value): self.header = header self.id = attr_id self.player = player if self.id not in LOBBY_PROPERTIES: - self.logger.info("Unknown attribute id: {0}".format(self.id)) + self.logger.info(f"Unknown attribute id: {self.id}") self.name = "Unknown" self.value = None else: @@ -80,17 +77,17 @@ def __init__(self, header, attr_id, player, value): try: self.value = lookup[value.strip("\x00 ")[::-1]] except KeyError: - self.logger.info("Missing attribute value: {0}".format(value)) + self.logger.info(f"Missing attribute value: {value}") self.value = None def __repr__(self): return str(self) def __str__(self): - return "[{0}] {1}: {2}".format(self.player, self.name, self.value) + return f"[{self.player}] {self.name}: {self.value}" -class Entity(object): +class Entity: """ :param integer sid: The entity's unique slot id. :param dict slot_data: The slot data associated with this entity @@ -164,7 +161,7 @@ def format(self, format_string): return format_string.format(**self.__dict__) -class Player(object): +class Player: """ :param integer pid: The player's unique player id. :param dict detail_data: The detail data associated with this player @@ -253,7 +250,7 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): self.toon_id = detail_data["bnet"]["uid"] -class User(object): +class User: """ :param integer uid: The user's unique user id :param dict init_data: The init data associated with this user @@ -317,7 +314,7 @@ def __init__(self, sid, slot_data, uid, init_data, pid): self.pid = pid def __str__(self): - return "Observer {0} - {1}".format(self.uid, self.name) + return f"Observer {self.uid} - {self.name}" def __repr__(self): return str(self) @@ -342,7 +339,7 @@ def __init__(self, sid, slot_data, pid, detail_data, attribute_data): self.name = detail_data["name"] def __str__(self): - return "Player {0} - {1} ({2})".format(self.pid, self.name, self.play_race) + return f"Player {self.pid} - {self.name} ({self.play_race})" def __repr__(self): return str(self) @@ -369,7 +366,7 @@ def __init__( Player.__init__(self, pid, slot_data, detail_data, attribute_data) def __str__(self): - return "Player {0} - {1} ({2})".format(self.pid, self.name, self.play_race) + return f"Player {self.pid} - {self.name} ({self.play_race})" def __repr__(self): return str(self) @@ -388,10 +385,10 @@ class PlayerSummary: teamid = int() #: The race the player played in the game. - play_race = str() + play_race = '' #: The race the player picked in the lobby. - pick_race = str() + pick_race = '' #: If the player is a computer is_ai = False @@ -406,7 +403,7 @@ class PlayerSummary: subregion = int() #: The player's region, such as us, eu, sea - region = str() + region = '' #: unknown1 unknown1 = int() @@ -429,11 +426,11 @@ def __init__(self, pid): def __str__(self): if not self.is_ai: - return "User {0}-S2-{1}-{2}".format( + return "User {}-S2-{}-{}".format( self.region.upper(), self.subregion, self.bnetid ) else: - return "AI ({0})".format(self.play_race) + return f"AI ({self.play_race})" def __repr__(self): return str(self) @@ -441,7 +438,7 @@ def __repr__(self): def get_stats(self): s = "" for k in self.stats: - s += "{0}: {1}\n".format(self.stats_pretty_names[k], self.stats[k]) + s += f"{self.stats_pretty_names[k]}: {self.stats[k]}\n" return s.strip() @@ -480,10 +477,10 @@ def as_points(self): return list(zip(self.times, self.values)) def __str__(self): - return "Graph with {0} values".format(len(self.times)) + return f"Graph with {len(self.times)} values" -class MapInfoPlayer(object): +class MapInfoPlayer: """ Describes the player data as found in the MapInfo document of SC2Map archives. """ @@ -542,7 +539,7 @@ def __init__(self, pid, control, color, race, unknown, start_point, ai, decal): @log_utils.loggable -class MapInfo(object): +class MapInfo: """ Represents the data encoded into the MapInfo file inside every SC2Map archive """ @@ -553,7 +550,7 @@ def __init__(self, contents): data = ByteDecoder(contents, endian="LITTLE") magic = data.read_string(4) if magic != "MapI": - self.logger.warn("Invalid MapInfo file: {0}".format(magic)) + self.logger.warn(f"Invalid MapInfo file: {magic}") return #: The map info file format version @@ -572,7 +569,7 @@ def __init__(self, contents): self.small_preview_type = data.read_uint32() #: (Optional) Small map preview path; relative to root of map archive - self.small_preview_path = str() + self.small_preview_path = '' if self.small_preview_type == 2: self.small_preview_path = data.read_cstring() @@ -580,7 +577,7 @@ def __init__(self, contents): self.large_preview_type = data.read_uint32() #: (Optional) Large map preview path; relative to root of map archive - self.large_preview_path = str() + self.large_preview_path = '' if self.large_preview_type == 2: self.large_preview_path = data.read_cstring() diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 89ba67fc..92aeefb9 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import struct from sc2reader.exceptions import ParseError, ReadError @@ -12,7 +9,7 @@ from sc2reader.decoders import BitPackedDecoder, ByteDecoder -class InitDataReader(object): +class InitDataReader: def __call__(self, data, replay): data = BitPackedDecoder(data) result = dict( @@ -292,11 +289,11 @@ def __call__(self, data, replay): ), ) if not data.done(): - raise ValueError("{0} bytes left!".format(data.length - data.tell())) + raise ValueError(f"{data.length - data.tell()} bytes left!") return result -class AttributesEventsReader(object): +class AttributesEventsReader: def __call__(self, data, replay): data = ByteDecoder(data, endian="LITTLE") data.read_bytes(5 if replay.base_build >= 17326 else 4) @@ -314,7 +311,7 @@ def __call__(self, data, replay): return result -class DetailsReader(object): +class DetailsReader: def __call__(self, data, replay): details = BitPackedDecoder(data).read_struct() return dict( @@ -363,7 +360,7 @@ def __call__(self, data, replay): ) -class MessageEventsReader(object): +class MessageEventsReader: def __call__(self, data, replay): data = BitPackedDecoder(data) pings = list() @@ -402,7 +399,7 @@ def __call__(self, data, replay): return dict(pings=pings, messages=messages, packets=packets) -class GameEventsReader_Base(object): +class GameEventsReader_Base: def __init__(self): self.EVENT_DISPATCH = { 0: (None, self.unknown_event), @@ -523,7 +520,7 @@ def __call__(self, data, replay): # Otherwise throw a read error else: raise ReadError( - "Event type {0} unknown at position {1}.".format( + "Event type {} unknown at position {}.".format( hex(event_type), hex(event_start) ), event_type, @@ -539,7 +536,7 @@ def __call__(self, data, replay): return game_events except ParseError as e: raise ReadError( - "Parse error '{0}' unknown at position {1}.".format( + "Parse error '{}' unknown at position {}.".format( e.msg, hex(event_start) ), event_type, @@ -550,7 +547,7 @@ def __call__(self, data, replay): ) except EOFError as e: raise ReadError( - "EOFError error '{0}' unknown at position {1}.".format( + "EOFError error '{}' unknown at position {}.".format( e.msg, hex(event_start) ), event_type, @@ -585,7 +582,7 @@ def read_selection_bitmask(self, data, mask_length): bits_left -= 8 # Compile the finished mask into a large integer for bit checks - bit_mask = sum([c << (i * 8) for i, c in enumerate(mask)]) + bit_mask = sum(c << (i * 8) for i, c in enumerate(mask)) # Change mask representation from an int to a bit array with # True => Deselect, False => Keep @@ -1097,7 +1094,7 @@ class GameEventsReader_16939(GameEventsReader_16755): class GameEventsReader_17326(GameEventsReader_16939): def __init__(self): - super(GameEventsReader_17326, self).__init__() + super().__init__() self.EVENT_DISPATCH.update({59: (None, self.trigger_mouse_moved_event)}) @@ -1257,7 +1254,7 @@ class GameEventsReader_21029(GameEventsReader_19595): class GameEventsReader_22612(GameEventsReader_21029): def __init__(self): - super(GameEventsReader_22612, self).__init__() + super().__init__() self.EVENT_DISPATCH.update( { @@ -1551,7 +1548,7 @@ def trigger_dialog_control_event(self, data): class GameEventsReader_24247(GameEventsReader_HotSBeta): def __init__(self): - super(GameEventsReader_24247, self).__init__() + super().__init__() self.EVENT_DISPATCH.update( { @@ -1736,7 +1733,7 @@ def game_user_join_event(self, data): class GameEventsReader_34784(GameEventsReader_27950): def __init__(self): - super(GameEventsReader_34784, self).__init__() + super().__init__() self.EVENT_DISPATCH.update( { @@ -1987,7 +1984,7 @@ def control_group_update_event(self, data): class GameEventsReader_38215(GameEventsReader_36442): def __init__(self): - super(GameEventsReader_38215, self).__init__() + super().__init__() self.EVENT_DISPATCH.update( { @@ -2188,7 +2185,7 @@ class GameEventsReader_65895(GameEventsReader_64469): """ def __init__(self): - super(GameEventsReader_65895, self).__init__() + super().__init__() self.EVENT_DISPATCH.update( {116: (None, self.set_sync_loading), 117: (None, self.set_sync_playing)} @@ -2260,7 +2257,7 @@ def command_event(self, data): ) -class TrackerEventsReader(object): +class TrackerEventsReader: def __init__(self): self.EVENT_DISPATCH = { 0: PlayerStatsEvent, diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 428e9cc9..564801d3 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - from collections import defaultdict, namedtuple from datetime import datetime import hashlib @@ -29,7 +26,7 @@ from sc2reader.constants import GAME_SPEED_FACTOR, LOBBY_PROPERTIES -class Resource(object): +class Resource: def __init__(self, file_object, filename=None, factory=None, **options): self.factory = factory self.opt = options @@ -50,7 +47,7 @@ class Replay(Resource): attributes = defaultdict(dict) #: Fully qualified filename of the replay file represented. - filename = str() + filename = '' #: Total number of frames in this game at 16 frames per second. frames = int() @@ -62,27 +59,27 @@ class Replay(Resource): base_build = int() #: The full version release string as seen on Battle.net - release_string = str() + release_string = '' #: A tuple of the individual pieces of the release string versions = tuple() #: The game speed: Slower, Slow, Normal, Fast, Faster - speed = str() + speed = '' #: Deprecated, use :attr:`game_type` or :attr:`real_type` instead - type = str() + type = '' #: The game type chosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA - game_type = str() + game_type = '' #: The real type of the replay as observed by counting players on teams. #: For outmatched games, the smaller team numbers come first. #: Example Values: 1v1, 2v2, 3v3, FFA, 2v4, etc. - real_type = str() + real_type = '' #: The category of the game, Ladder and Private - category = str() + category = '' #: A flag for public ladder games is_ladder = bool() @@ -91,10 +88,10 @@ class Replay(Resource): is_private = bool() #: The raw hash name of the s2ma resource as hosted on bnet depots - map_hash = str() + map_hash = '' #: The name of the map the game was played on - map_name = str() + map_name = '' #: A reference to the loaded :class:`Map` resource. map = None @@ -130,7 +127,7 @@ class Replay(Resource): real_length = None #: The region the game was played on: us, eu, sea, etc - region = str() + region = '' #: An integrated list of all the game events events = list() @@ -187,10 +184,10 @@ class Replay(Resource): #: A sha256 hash uniquely representing the combination of people in the game. #: Can be used in conjunction with date times to match different replays #: of the game game. - people_hash = str() + people_hash = '' #: SC2 Expansion. One of 'WoL', 'HotS' - expansion = str() + expansion = '' #: True of the game was resumed from a replay resume_from_replay = False @@ -210,7 +207,7 @@ def __init__( do_tracker_events=True, **options ): - super(Replay, self).__init__(replay_file, filename, **options) + super().__init__(replay_file, filename, **options) self.datapack = None self.raw_data = dict() @@ -277,7 +274,7 @@ def __init__( self.frames = header_data[3] self.build = self.versions[4] self.base_build = self.versions[5] - self.release_string = "{0}.{1}.{2}.{3}".format(*self.versions[1:5]) + self.release_string = "{}.{}.{}.{}".format(*self.versions[1:5]) fps = self.game_fps if 34784 <= self.build: # lotv replay, adjust time fps = self.game_fps * 1.4 @@ -396,17 +393,17 @@ def load_details(self): dependency_hashes = [d.hash for d in details["cache_handles"]] if ( - hashlib.sha256("Standard Data: Void.SC2Mod".encode("utf8")).hexdigest() + hashlib.sha256(b"Standard Data: Void.SC2Mod").hexdigest() in dependency_hashes ): self.expansion = "LotV" elif ( - hashlib.sha256("Standard Data: Swarm.SC2Mod".encode("utf8")).hexdigest() + hashlib.sha256(b"Standard Data: Swarm.SC2Mod").hexdigest() in dependency_hashes ): self.expansion = "HotS" elif ( - hashlib.sha256("Standard Data: Liberty.SC2Mod".encode("utf8")).hexdigest() + hashlib.sha256(b"Standard Data: Liberty.SC2Mod").hexdigest() in dependency_hashes ): self.expansion = "WoL" @@ -550,14 +547,14 @@ def get_team(team_id): # Pull results up for teams for team in self.teams: - results = set([p.result for p in team.players]) + results = {p.result for p in team.players} if len(results) == 1: team.result = list(results)[0] if team.result == "Win": self.winner = team else: self.logger.warn( - "Conflicting results for Team {0}: {1}".format(team.number, results) + f"Conflicting results for Team {team.number}: {results}" ) team.result = "Unknown" @@ -884,7 +881,7 @@ def _get_reader(self, data_file): return reader else: raise ValueError( - "Valid {0} reader could not found for build {1}".format( + "Valid {} reader could not found for build {}".format( data_file, self.build ) ) @@ -904,7 +901,7 @@ def _read_data(self, data_file, reader): "replay.message.events", "replay.tracker.events", ]: - raise ValueError("{0} not found in archive".format(data_file)) + raise ValueError(f"{data_file} not found in archive") def __getstate__(self): state = self.__dict__.copy() @@ -915,19 +912,19 @@ def __getstate__(self): class Map(Resource): def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): - super(Map, self).__init__(map_file, filename, **options) + super().__init__(map_file, filename, **options) #: The localized (only enUS supported right now) map name. - self.name = str() + self.name = '' #: The localized (only enUS supported right now) map author. - self.author = str() + self.author = '' #: The localized (only enUS supported right now) map description. - self.description = str() + self.description = '' #: The localized (only enUS supported right now) map website. - self.website = str() + self.website = '' #: The unique hash used to identify this map on bnet's depots. self.hash = map_hash @@ -949,7 +946,7 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option # just because US English wasn't a concern of the map author. # TODO: Make this work regardless of the localizations available. game_strings_file = self.archive.read_file( - "enUS.SC2Data\LocalizedData\GameStrings.txt" + r"enUS.SC2Data\LocalizedData\GameStrings.txt" ) if game_strings_file: for line in game_strings_file.decode("utf8").split("\r\n"): @@ -1018,7 +1015,7 @@ class GameSummary(Resource): """ #: Game speed - game_speed = str() + game_speed = '' #: Game length (real-time) real_length = int() @@ -1045,7 +1042,7 @@ class GameSummary(Resource): localization_urls = dict() def __init__(self, summary_file, filename=None, lang="enUS", **options): - super(GameSummary, self).__init__(summary_file, filename, lang=lang, **options) + super().__init__(summary_file, filename, lang=lang, **options) #: A dict of team# -> teams self.team = dict() @@ -1072,8 +1069,8 @@ def __init__(self, summary_file, filename=None, lang="enUS", **options): self.localization_urls = dict() self.lobby_properties = dict() self.lobby_player_properties = dict() - self.game_type = str() - self.real_type = str() + self.game_type = '' + self.real_type = '' # The first 16 bytes appear to be some sort of compression header buffer = BitPackedDecoder(zlib.decompress(summary_file.read()[16:])) @@ -1256,7 +1253,7 @@ def use_property(prop, player=None): # Because of the above complication we resort to a set intersection of # the applicable values and the set of required values. - if not set(requirement.values[val][0] for val in values) & set(req[1]): + if not {requirement.values[val][0] for val in values} & set(req[1]): break else: @@ -1283,7 +1280,7 @@ def use_property(prop, player=None): def load_player_stats(self): translation = self.translations[self.opt["lang"]] - stat_items = sum([p[0] for p in self.parts[3:]], []) + stat_items = sum((p[0] for p in self.parts[3:]), []) for item in stat_items: # Each stat item is laid out as follows @@ -1336,7 +1333,7 @@ def load_player_stats(self): ) ) elif stat_id != 83886080: # We know this one is always bad. - self.logger.warn("Untranslatable key = {0}".format(stat_id)) + self.logger.warn(f"Untranslatable key = {stat_id}") # Once we've compiled all the build commands we need to make # sure they are properly sorted for presentation. @@ -1432,7 +1429,7 @@ def load_players(self): self.player[player.pid] = player def __str__(self): - return "{0} - {1} {2}".format( + return "{} - {} {}".format( self.start_time, self.game_length, "v".join( @@ -1445,19 +1442,19 @@ class MapHeader(Resource): """**Experimental**""" #: The name of the map - name = str() + name = '' #: Hash of map file - map_hash = str() + map_hash = '' #: Link to the map file - map_url = str() + map_url = '' #: Hash of the map image - image_hash = str() + image_hash = '' #: Link to the image of the map (.s2mv) - image_url = str() + image_url = '' #: Localization dictionary, {language, url} localization_urls = dict() @@ -1466,7 +1463,7 @@ class MapHeader(Resource): blizzard = False def __init__(self, header_file, filename=None, **options): - super(MapHeader, self).__init__(header_file, filename, **options) + super().__init__(header_file, filename, **options) self.data = BitPackedDecoder(header_file).read_struct() # Name diff --git a/sc2reader/scripts/__init__.py b/sc2reader/scripts/__init__.py index e826f791..551b70ab 100755 --- a/sc2reader/scripts/__init__.py +++ b/sc2reader/scripts/__init__.py @@ -1,5 +1,2 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - # import submodules from sc2reader.scripts import utils diff --git a/sc2reader/scripts/sc2attributes.py b/sc2reader/scripts/sc2attributes.py index a794e235..7eba02ca 100644 --- a/sc2reader/scripts/sc2attributes.py +++ b/sc2reader/scripts/sc2attributes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # Recursively searches for s2gs files in specified paths. Adds # new attributes and values and allows the user to choose when @@ -28,7 +27,6 @@ # those decisions. The decisions are pickled instead of in json # because the data structure is too complex for the json format. # -from __future__ import absolute_import, print_function, unicode_literals, division import argparse import json @@ -64,7 +62,7 @@ def main(): attributes = dict() if os.path.exists(data_path): - with open(data_path, "r") as data_file: + with open(data_path) as data_file: data = json.load(data_file) attributes = data.get("attributes", attributes) decisions = pickle.loads(data.get("decisions", "(dp0\n.")) @@ -100,7 +98,7 @@ def main(): attribute_values[str(value_key)] = value_name - attributes["{0:0>4}".format(group_key)] = ( + attributes[f"{group_key:0>4}"] = ( group_name, attribute_values, ) @@ -122,15 +120,15 @@ def get_choice(s2gs_key, old_value, new_value): key = frozenset([s2gs_key, old_value, new_value]) if key not in decisions: print( - "Naming conflict on {0}: {1} != {2}".format(s2gs_key, old_value, new_value) + f"Naming conflict on {s2gs_key}: {old_value} != {new_value}" ) print("Which do you want to use?") - print(" (o) Old value '{0}'".format(old_value)) - print(" (n) New value '{0}'".format(new_value)) + print(f" (o) Old value '{old_value}'") + print(f" (n) New value '{new_value}'") while True: answer = raw_input("Choose 'o' or 'n' then press enter: ").lower() if answer not in ("o", "n"): - print("Invalid choice `{0}`".format(answer)) + print(f"Invalid choice `{answer}`") else: break decisions[key] = {"o": old_value, "n": new_value}[answer] diff --git a/sc2reader/scripts/sc2json.py b/sc2reader/scripts/sc2json.py index c7e842e8..d2fdb8ef 100755 --- a/sc2reader/scripts/sc2json.py +++ b/sc2reader/scripts/sc2json.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division import sc2reader from sc2reader.factories.plugins.replay import toJSON diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 868f897a..3f0aab6f 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- """ This script is intended for use debugging parse issues on replays. @@ -16,7 +15,6 @@ If there are parse exceptions, this script should be run to generate an info for the ticket filed. """ -from __future__ import absolute_import, print_function, unicode_literals, division import argparse import sc2reader @@ -46,7 +44,7 @@ def main(): releases_parsed = set() for folder in args.folders: - print("dealing with {0}".format(folder)) + print(f"dealing with {folder}") for path in sc2reader.utils.get_files(folder, extension="SC2Replay"): try: rs = sc2reader.load_replay(path, load_level=0).release_string @@ -57,24 +55,20 @@ def main(): if not args.one_each or replay.is_ladder: replay = sc2reader.load_replay(path, debug=True) - human_pids = set([human.pid for human in replay.humans]) - event_pids = set( - [ + human_pids = {human.pid for human in replay.humans} + event_pids = { event.player.pid for event in replay.events if getattr(event, "player", None) - ] - ) - player_pids = set( - [player.pid for player in replay.players if player.is_human] - ) - ability_pids = set( - [ + } + player_pids = { + player.pid for player in replay.players if player.is_human + } + ability_pids = { event.player.pid for event in replay.events if "CommandEvent" in event.name - ] - ) + } if human_pids != event_pids: print( "Event Pid problem! pids={pids} but event pids={event_pids}".format( @@ -105,9 +99,9 @@ def main(): ) print( "Units were: {units}".format( - units=set( - [obj.name for obj in replay.objects.values()] - ) + units={ + obj.name for obj in replay.objects.values() + } ) ) @@ -124,7 +118,7 @@ def main(): ) print("[ERROR] {}", e) for event in e.game_events[-5:]: - print("{0}".format(event)) + print(f"{event}") print(e.buffer.read_range(e.location, e.location + 50).encode("hex")) print except Exception as e: @@ -137,13 +131,13 @@ def main(): **replay.__dict__ ) ) - print("[ERROR] {0}".format(e)) + print(f"[ERROR] {e}") for pid, attributes in replay.attributes.items(): - print("{0} {1}".format(pid, attributes)) + print(f"{pid} {attributes}") for pid, info in enumerate(replay.players): - print("{0} {1}".format(pid, info)) + print(f"{pid} {info}") for message in replay.messages: - print("{0} {1}".format(message.pid, message.text)) + print(f"{message.pid} {message.text}") traceback.print_exc() print("") except Exception as e2: @@ -153,8 +147,8 @@ def main(): **replay.__dict__ ) ) - print("[ERROR] {0}".format(e)) - print("[ERROR] {0}".format(e2)) + print(f"[ERROR] {e}") + print(f"[ERROR] {e2}") traceback.print_exc() print diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index f27fda49..25b58189 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division import os import argparse @@ -16,37 +14,37 @@ def printReplay(filepath, arguments): replay = sc2reader.load_replay(filepath, debug=True) if arguments.map: - print(" Map: {0}".format(replay.map_name)) + print(f" Map: {replay.map_name}") if arguments.length: - print(" Length: {0} minutes".format(replay.game_length)) + print(f" Length: {replay.game_length} minutes") if arguments.date: - print(" Date: {0}".format(replay.start_time)) + print(f" Date: {replay.start_time}") if arguments.teams: lineups = [team.lineup for team in replay.teams] - print(" Teams: {0}".format("v".join(lineups))) + print(" Teams: {}".format("v".join(lineups))) for team in replay.teams: print( - " Team {0}\t{1} ({2})".format( + " Team {}\t{} ({})".format( team.number, team.players[0].name, team.players[0].pick_race[0] ) ) for player in team.players[1:]: print( - " \t{0} ({1})".format( + " \t{} ({})".format( player.name, player.pick_race[0] ) ) if arguments.observers: print(" Observers:") for observer in replay.observers: - print(" {0}".format(observer.name)) + print(f" {observer.name}") if arguments.messages: print(" Messages:") for message in replay.messages: - print(" {0}".format(message)) + print(f" {message}") if arguments.version: - print(" Version: {0}".format(replay.release_string)) + print(f" Version: {replay.release_string}") print except ReadError as e: @@ -54,19 +52,19 @@ def printReplay(filepath, arguments): return prev = e.game_events[-1] print( - "\nVersion {0} replay:\n\t{1}".format( + "\nVersion {} replay:\n\t{}".format( e.replay.release_string, e.replay.filepath ) ) - print("\t{0}, Type={1:X}".format(e.msg, e.type)) - print("\tPrevious Event: {0}".format(prev.name)) + print(f"\t{e.msg}, Type={e.type:X}") + print(f"\tPrevious Event: {prev.name}") print("\t\t" + prev.bytes.encode("hex")) print("\tFollowing Bytes:") print("\t\t" + e.buffer.read_range(e.location, e.location + 30).encode("hex")) - print("Error with '{0}': ".format(filepath)) + print(f"Error with '{filepath}': ") print(e) except Exception as e: - print("Error with '{0}': ".format(filepath)) + print(f"Error with '{filepath}': ") print(e) raise @@ -75,21 +73,21 @@ def printGameSummary(filepath, arguments): summary = sc2reader.load_game_summary(filepath) if arguments.map: - print(" Map: {0}".format(summary.map_name)) + print(f" Map: {summary.map_name}") if arguments.length: - print(" Length: {0} minutes".format(summary.game_length)) + print(f" Length: {summary.game_length} minutes") if arguments.date: - print(" Date: {0}".format(summary.start_time)) + print(f" Date: {summary.start_time}") if arguments.teams: lineups = [team.lineup for team in summary.teams] - print(" Teams: {0}".format("v".join(lineups))) + print(" Teams: {}".format("v".join(lineups))) for team in summary.teams: - print(" Team {0}\t{1}".format(team.number, team.players[0])) + print(f" Team {team.number}\t{team.players[0]}") for player in team.players[1:]: - print(" \t{0}".format(player)) + print(f" \t{player}") if arguments.builds: for player in summary.players: - print("\n== {0} ==\n".format(player)) + print(f"\n== {player} ==\n") for order in summary.build_orders[player.pid]: msg = " {0:0>2}:{1:0>2} {2:<35} {3:0>2}/{4}" print( @@ -177,12 +175,12 @@ def main(): name, ext = os.path.splitext(filepath) if ext.lower() == ".sc2replay": print( - "\n--------------------------------------\n{0}\n".format(filepath) + f"\n--------------------------------------\n{filepath}\n" ) printReplay(filepath, arguments) elif ext.lower() == ".s2gs": print( - "\n--------------------------------------\n{0}\n".format(filepath) + f"\n--------------------------------------\n{filepath}\n" ) printGameSummary(filepath, arguments) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index 3d8212ca..c3e61b5e 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -1,6 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division try: # Assume that we are on *nix or Mac @@ -22,7 +20,7 @@ def getch(): try: sys.stdin.read(1) break - except IOError: + except OSError: pass finally: termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm) @@ -78,15 +76,15 @@ def main(): for filename in sc2reader.utils.get_files(args.FILE): replay = sc2reader.load_replay(filename, debug=True) - print("Release {0}".format(replay.release_string)) + print(f"Release {replay.release_string}") print( - "{0} on {1} at {2}".format(replay.type, replay.map_name, replay.start_time) + f"{replay.type} on {replay.map_name} at {replay.start_time}" ) print("") for team in replay.teams: print(team) for player in team.players: - print(" {0}".format(player)) + print(f" {player}") print("\n--------------------------\n\n") # Allow picking of the player to 'watch' diff --git a/sc2reader/scripts/utils.py b/sc2reader/scripts/utils.py index cf998e5d..cab32a13 100644 --- a/sc2reader/scripts/utils.py +++ b/sc2reader/scripts/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import argparse import re import textwrap diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 93787212..514ca807 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, print_function, unicode_literals, division - import binascii import os import json @@ -11,7 +8,7 @@ from sc2reader.constants import COLOR_CODES, COLOR_CODES_INV -class DepotFile(object): +class DepotFile: """ :param bytes: The raw bytes representing the depot file @@ -49,7 +46,7 @@ def windows_to_unix(windows_time): @loggable -class Color(object): +class Color: """ Stores a color name and rgba representation of a color. Individual color components can be retrieved with the dot operator:: @@ -145,7 +142,7 @@ def recovery_attempt(): # Python2 and Python3 handle wrapped exceptions with old tracebacks in incompatible ways # Python3 handles it by default and Python2's method won't compile in python3 # Since the underlying traceback isn't important to most people, don't expose it anymore - raise MPQError("Unable to extract file: {0}".format(data_file), e) + raise MPQError(f"Unable to extract file: {data_file}", e) def get_files( @@ -162,7 +159,7 @@ def get_files( """ # os.walk and os.path.isfile fail silently. We want to be loud! if not os.path.exists(path): - raise ValueError("Location `{0}` does not exist".format(path)) + raise ValueError(f"Location `{path}` does not exist") # If an extension is supplied, use it to do a type check if extension: @@ -235,9 +232,9 @@ def secs(self): def __str__(self): if self.hours: - return "{0:0>2}.{1:0>2}.{2:0>2}".format(self.hours, self.mins, self.secs) + return f"{self.hours:0>2}.{self.mins:0>2}.{self.secs:0>2}" else: - return "{0:0>2}.{1:0>2}".format(self.mins, self.secs) + return f"{self.mins:0>2}.{self.secs:0>2}" class JSONDateEncoder(json.JSONEncoder): diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 2b1d1492..a7f7ac37 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import datetime import json from xml.dom import minidom @@ -214,16 +211,14 @@ def test_hots_pids(self): replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") - player_pids = set( - [player.pid for player in replay.players if player.is_human] - ) - ability_pids = set( - [ + player_pids = { + player.pid for player in replay.players if player.is_human + } + ability_pids = { event.player.pid for event in replay.events if "CommandEvent" in event.name - ] - ) + } self.assertEqual(ability_pids, player_pids) def test_wol_pids(self): @@ -231,27 +226,23 @@ def test_wol_pids(self): "test_replays/1.5.4.24540/ggtracker_1471849.SC2Replay" ) self.assertEqual(replay.expansion, "WoL") - ability_pids = set( - [ + ability_pids = { event.player.pid for event in replay.events if "CommandEvent" in event.name - ] - ) - player_pids = set([player.pid for player in replay.players]) + } + player_pids = {player.pid for player in replay.players} self.assertEqual(ability_pids, player_pids) def test_hots_hatchfun(self): replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") - player_pids = set([player.pid for player in replay.players]) - spawner_pids = set( - [ + player_pids = {player.pid for player in replay.players} + spawner_pids = { event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" - ] - ) + } self.assertTrue(spawner_pids.issubset(player_pids)) def test_hots_vs_ai(self): @@ -407,15 +398,13 @@ def test_gameheartnormalizer_plugin(self): # Not a GameHeart game! replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") - player_pids = set([player.pid for player in replay.players]) - spawner_pids = set( - [ + player_pids = {player.pid for player in replay.players} + spawner_pids = { event.player.pid for event in replay.events if "TargetUnitCommandEvent" in event.name and event.ability.name == "SpawnLarva" - ] - ) + } self.assertTrue(spawner_pids.issubset(player_pids)) replay = sc2reader.load_replay("test_replays/gameheart/gameheart.SC2Replay") @@ -541,8 +530,8 @@ def test_30(self): def test_31(self): for i in range(1, 5): - print("DOING {}".format(i)) - replay = sc2reader.load_replay("test_replays/3.1.0/{}.SC2Replay".format(i)) + print(f"DOING {i}") + replay = sc2reader.load_replay(f"test_replays/3.1.0/{i}.SC2Replay") def test_30_map(self): for replayfilename in ["test_replays/3.0.0.38215/third.SC2Replay"]: @@ -587,13 +576,11 @@ def test_funny_minerals(self): if "MineralField" in ou.attributes["UnitType"].value ] mineralFieldNames = list( - set( - [ + { ou.attributes["UnitType"].value for ou in itemlist if "MineralField" in ou.attributes["UnitType"].value - ] - ) + } ) # print(mineralFieldNames) self.assertTrue(len(mineralPosStrs) > 0) @@ -609,7 +596,7 @@ def test_32(self): def test_33(self): for replaynum in range(1, 4): replay = sc2reader.load_replay( - "test_replays/3.3.0/{}.SC2Replay".format(replaynum) + f"test_replays/3.3.0/{replaynum}.SC2Replay" ) self.assertTrue(replay is not None) @@ -727,7 +714,7 @@ def test_game_event_string(self): player.play_race = "TestRace" event = GameEvent(16, 16) event.player = player - self.assertEqual("{0}\t{1:<15} ".format(time, "Global"), event._str_prefix()) + self.assertEqual("{}\t{:<15} ".format(time, "Global"), event._str_prefix()) # Player with name player = MockPlayer() @@ -735,12 +722,12 @@ def test_game_event_string(self): player.play_race = "TestRace" event = GameEvent(16, 1) event.player = player - self.assertEqual("{0}\t{1:<15} ".format(time, player.name), event._str_prefix()) + self.assertEqual(f"{time}\t{player.name:<15} ", event._str_prefix()) # No Player player = MockPlayer() event = GameEvent(16, 1) - self.assertEqual("{0}\t{1:<15} ".format(time, "no name"), event._str_prefix()) + self.assertEqual("{}\t{:<15} ".format(time, "no name"), event._str_prefix()) # Player without name player = MockPlayer() @@ -749,13 +736,13 @@ def test_game_event_string(self): event = GameEvent(16, 1) event.player = player self.assertEqual( - "{0}\tPlayer {1} - ({2}) ".format(time, player.pid, player.play_race), + f"{time}\tPlayer {player.pid} - ({player.play_race}) ", event._str_prefix(), ) class TestGameEngine(unittest.TestCase): - class TestEvent(object): + class TestEvent: name = "TestEvent" def __init__(self, value): @@ -764,7 +751,7 @@ def __init__(self, value): def __str__(self): return self.value - class TestPlugin1(object): + class TestPlugin1: name = "TestPlugin1" def handleInitGame(self, event, replay): @@ -782,7 +769,7 @@ def handleTestEvent(self, event, replay): def handleEndGame(self, event, replay): yield TestGameEngine.TestEvent("g") - class TestPlugin2(object): + class TestPlugin2: name = "TestPlugin2" def handleInitGame(self, event, replay): @@ -797,7 +784,7 @@ def handlePluginExit(self, event, replay): def handleEndGame(self, event, replay): yield TestGameEngine.TestEvent("f") - class MockReplay(object): + class MockReplay: def __init__(self, events): self.events = events @@ -813,7 +800,7 @@ def test_plugin1(self): self.assertEqual(replay.plugin_result["TestPlugin2"], (0, dict())) -class MockPlayer(object): +class MockPlayer: def __init__(self): self.name = None self.play_race = None diff --git a/test_s2gs/test_all.py b/test_s2gs/test_all.py index be867add..75a1daa9 100644 --- a/test_s2gs/test_all.py +++ b/test_s2gs/test_all.py @@ -1,5 +1,3 @@ -# -*- coding: UTF-8 -*- - # Newer unittest features aren't built in for python 2.6 import sys From 1f18ff757e91801190ddffb95da2f12da2c52adf Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 25 May 2022 18:40:18 +0200 Subject: [PATCH 103/136] Update README.rst and setup.py --- README.rst | 16 ++++----- sc2reader/decoders.py | 4 +-- sc2reader/events/game.py | 19 +++++----- sc2reader/factories/plugins/replay.py | 4 +-- sc2reader/factories/sc2factory.py | 8 ++--- sc2reader/objects.py | 14 ++++---- sc2reader/resources.py | 50 +++++++++++++-------------- sc2reader/scripts/sc2attributes.py | 4 +-- sc2reader/scripts/sc2parse.py | 16 ++++----- sc2reader/scripts/sc2printer.py | 8 ++--- sc2reader/scripts/sc2replayer.py | 4 +-- setup.py | 10 ++---- test_replays/test_replays.py | 40 +++++++++------------ 13 files changed, 82 insertions(+), 115 deletions(-) diff --git a/README.rst b/README.rst index dcbcb938..63d74a99 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ What is sc2reader? ==================== -sc2reader is a python library for extracting information from various different Starcraft II resources. These resources currently include Replays, Maps, and Game Summaries; we have plans to add support for Battle.net profiles and would gladly accept adapters to the more entrenched SCII sites such as sc2ranks. +sc2reader is a Python 3 library for extracting information from various different Starcraft II resources. These resources currently include Replays, Maps, and Game Summaries; we have plans to add support for Battle.net profiles and would gladly accept adapters to the more entrenched SCII sites such as sc2ranks. There is a pressing need in the SC2 community for better statistics, better analytics, better tools for organizing and searching replays. Better websites for sharing replays and hosting tournaments. These tools can't be created without first being able to open up Starcraft II game files and analyze the data within. Our goal is to give anyone and everyone the power to construct their own tools, do their own analysis, and hack on their own Starcraft II projects under the open MIT license. @@ -195,7 +195,7 @@ The new GameHeartNormalizerplugin is registered by default. Installation ================ -sc2reader runs on any system with Python 2.6+, 3.2+, or PyPy installed. +sc2reader runs on any system with Python 3.7+, or PyPy3 installed. From PyPI (stable) @@ -203,18 +203,14 @@ From PyPI (stable) Install from the latest release on PyPI with pip:: - pip install sc2reader - -or easy_install:: - - easy_install sc2reader + python3 -m pip install sc2reader or with setuptools (specify a valid x.x.x):: wget http://pypi.python.org/packages/source/s/sc2reader/sc2reader-x.x.x.tar.gz tar -xzf sc2reader-x.x.x.tar.gz cd sc2reader-x.x.x - python setup.py install + python3 setup.py install Releases to PyPi can be very delayed (sorry!), for the latest and greatest you are encouraged to install from Github upstream. @@ -235,7 +231,7 @@ or with setuptools:: wget -O sc2reader-upstream.tar.gz https://github.com/ggtracker/sc2reader/tarball/upstream tar -xzf sc2reader-upstream.tar.gz cd sc2reader-upstream - python setup.py install + python3 setup.py install .. _circle-ci: https://circleci.com/ .. _coveralls.io: https://coveralls.io @@ -250,7 +246,7 @@ Contributors should install from an active git repository using setuptools in `d git clone https://github.com/ggtracker/sc2reader.git cd sc2reader - python setup.py develop + python3 setup.py develop Please review the `CONTRIBUTING.md`_ file and get in touch with us before doing too much work. It'll make everyone happier in the long run. diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index 9085ab7d..ed726356 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -410,9 +410,7 @@ def read_struct(self, datatype=None): elif datatype == 0x05: # Struct entries = self.read_vint() - data = { - self.read_vint(): self.read_struct() for i in range(entries) - } + data = {self.read_vint(): self.read_struct() for i in range(entries)} elif datatype == 0x06: # u8 data = ord(self._buffer.read(1)) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 2d04c4c2..50a9f8f0 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -630,9 +630,7 @@ def __init__(self, frame, pid, data): self.yaw = data["yaw"] def __str__(self): - return self._str_prefix() + "{} at ({}, {})".format( - self.name, self.x, self.y - ) + return self._str_prefix() + "{} at ({}, {})".format(self.name, self.x, self.y) @loggable @@ -673,12 +671,15 @@ def __init__(self, frame, pid, data): self.custom_resource = self.resources[3] if len(self.resources) >= 4 else None def __str__(self): - return self._str_prefix() + " transfer {} minerals, {} gas, {} terrazine, and {} custom to {}".format( - self.minerals, - self.vespene, - self.terrazine, - self.custom_resource, - self.recipient, + return ( + self._str_prefix() + + " transfer {} minerals, {} gas, {} terrazine, and {} custom to {}".format( + self.minerals, + self.vespene, + self.terrazine, + self.custom_resource, + self.recipient, + ) ) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index b3ba681b..7f645eb3 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -213,9 +213,7 @@ def SelectionTracker(replay): person.selection_errors += 1 if debug: logger.warn( - "Error detected in deselection mode {}.".format( - event.mask_type - ) + "Error detected in deselection mode {}.".format(event.mask_type) ) person.selection = player_selections diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index e4b38c6f..c020459f 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -259,9 +259,7 @@ def get_remote_cache_key(self, remote_resource): def load_remote_resource_contents(self, remote_resource, **options): cache_key = self.get_remote_cache_key(remote_resource) if not self.cache_has(cache_key): - resource = super().load_remote_resource_contents( - remote_resource, **options - ) + resource = super().load_remote_resource_contents(remote_resource, **options) self.cache_set(cache_key, resource) else: resource = self.cache_get(cache_key) @@ -363,9 +361,7 @@ class DoubleCachedSC2Factory(DictCachedSC2Factory, FileCachedSC2Factory): """ def __init__(self, cache_dir, cache_max_size=0, **options): - super().__init__( - cache_max_size, cache_dir=cache_dir, **options - ) + super().__init__(cache_max_size, cache_dir=cache_dir, **options) def load_remote_resource_contents(self, remote_resource, **options): cache_key = self.get_remote_cache_key(remote_resource) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 2949b516..d6c9491d 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -19,7 +19,7 @@ class Team: """ #: A unique hash identifying the team of players - hash = '' + hash = "" #: The team number as recorded in the replay number = int() @@ -29,7 +29,7 @@ class Team: #: The result of the game for this team. #: One of "Win", "Loss", or "Unknown" - result = '' + result = "" def __init__(self, number): self.number = number @@ -385,10 +385,10 @@ class PlayerSummary: teamid = int() #: The race the player played in the game. - play_race = '' + play_race = "" #: The race the player picked in the lobby. - pick_race = '' + pick_race = "" #: If the player is a computer is_ai = False @@ -403,7 +403,7 @@ class PlayerSummary: subregion = int() #: The player's region, such as us, eu, sea - region = '' + region = "" #: unknown1 unknown1 = int() @@ -569,7 +569,7 @@ def __init__(self, contents): self.small_preview_type = data.read_uint32() #: (Optional) Small map preview path; relative to root of map archive - self.small_preview_path = '' + self.small_preview_path = "" if self.small_preview_type == 2: self.small_preview_path = data.read_cstring() @@ -577,7 +577,7 @@ def __init__(self, contents): self.large_preview_type = data.read_uint32() #: (Optional) Large map preview path; relative to root of map archive - self.large_preview_path = '' + self.large_preview_path = "" if self.large_preview_type == 2: self.large_preview_path = data.read_cstring() diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 564801d3..2f8085d6 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -47,7 +47,7 @@ class Replay(Resource): attributes = defaultdict(dict) #: Fully qualified filename of the replay file represented. - filename = '' + filename = "" #: Total number of frames in this game at 16 frames per second. frames = int() @@ -59,27 +59,27 @@ class Replay(Resource): base_build = int() #: The full version release string as seen on Battle.net - release_string = '' + release_string = "" #: A tuple of the individual pieces of the release string versions = tuple() #: The game speed: Slower, Slow, Normal, Fast, Faster - speed = '' + speed = "" #: Deprecated, use :attr:`game_type` or :attr:`real_type` instead - type = '' + type = "" #: The game type chosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA - game_type = '' + game_type = "" #: The real type of the replay as observed by counting players on teams. #: For outmatched games, the smaller team numbers come first. #: Example Values: 1v1, 2v2, 3v3, FFA, 2v4, etc. - real_type = '' + real_type = "" #: The category of the game, Ladder and Private - category = '' + category = "" #: A flag for public ladder games is_ladder = bool() @@ -88,10 +88,10 @@ class Replay(Resource): is_private = bool() #: The raw hash name of the s2ma resource as hosted on bnet depots - map_hash = '' + map_hash = "" #: The name of the map the game was played on - map_name = '' + map_name = "" #: A reference to the loaded :class:`Map` resource. map = None @@ -127,7 +127,7 @@ class Replay(Resource): real_length = None #: The region the game was played on: us, eu, sea, etc - region = '' + region = "" #: An integrated list of all the game events events = list() @@ -184,10 +184,10 @@ class Replay(Resource): #: A sha256 hash uniquely representing the combination of people in the game. #: Can be used in conjunction with date times to match different replays #: of the game game. - people_hash = '' + people_hash = "" #: SC2 Expansion. One of 'WoL', 'HotS' - expansion = '' + expansion = "" #: True of the game was resumed from a replay resume_from_replay = False @@ -205,7 +205,7 @@ def __init__( load_level=4, engine=sc2reader.engine, do_tracker_events=True, - **options + **options, ): super().__init__(replay_file, filename, **options) self.datapack = None @@ -915,16 +915,16 @@ def __init__(self, map_file, filename=None, region=None, map_hash=None, **option super().__init__(map_file, filename, **options) #: The localized (only enUS supported right now) map name. - self.name = '' + self.name = "" #: The localized (only enUS supported right now) map author. - self.author = '' + self.author = "" #: The localized (only enUS supported right now) map description. - self.description = '' + self.description = "" #: The localized (only enUS supported right now) map website. - self.website = '' + self.website = "" #: The unique hash used to identify this map on bnet's depots. self.hash = map_hash @@ -1015,7 +1015,7 @@ class GameSummary(Resource): """ #: Game speed - game_speed = '' + game_speed = "" #: Game length (real-time) real_length = int() @@ -1069,8 +1069,8 @@ def __init__(self, summary_file, filename=None, lang="enUS", **options): self.localization_urls = dict() self.lobby_properties = dict() self.lobby_player_properties = dict() - self.game_type = '' - self.real_type = '' + self.game_type = "" + self.real_type = "" # The first 16 bytes appear to be some sort of compression header buffer = BitPackedDecoder(zlib.decompress(summary_file.read()[16:])) @@ -1442,19 +1442,19 @@ class MapHeader(Resource): """**Experimental**""" #: The name of the map - name = '' + name = "" #: Hash of map file - map_hash = '' + map_hash = "" #: Link to the map file - map_url = '' + map_url = "" #: Hash of the map image - image_hash = '' + image_hash = "" #: Link to the image of the map (.s2mv) - image_url = '' + image_url = "" #: Localization dictionary, {language, url} localization_urls = dict() diff --git a/sc2reader/scripts/sc2attributes.py b/sc2reader/scripts/sc2attributes.py index 7eba02ca..6e2ca4d1 100644 --- a/sc2reader/scripts/sc2attributes.py +++ b/sc2reader/scripts/sc2attributes.py @@ -119,9 +119,7 @@ def get_choice(s2gs_key, old_value, new_value): # This way old/new values can be swapped and decision is remembered key = frozenset([s2gs_key, old_value, new_value]) if key not in decisions: - print( - f"Naming conflict on {s2gs_key}: {old_value} != {new_value}" - ) + print(f"Naming conflict on {s2gs_key}: {old_value} != {new_value}") print("Which do you want to use?") print(f" (o) Old value '{old_value}'") print(f" (n) New value '{new_value}'") diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 3f0aab6f..22f792a4 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -57,17 +57,17 @@ def main(): human_pids = {human.pid for human in replay.humans} event_pids = { - event.player.pid - for event in replay.events - if getattr(event, "player", None) + event.player.pid + for event in replay.events + if getattr(event, "player", None) } player_pids = { player.pid for player in replay.players if player.is_human } ability_pids = { - event.player.pid - for event in replay.events - if "CommandEvent" in event.name + event.player.pid + for event in replay.events + if "CommandEvent" in event.name } if human_pids != event_pids: print( @@ -99,9 +99,7 @@ def main(): ) print( "Units were: {units}".format( - units={ - obj.name for obj in replay.objects.values() - } + units={obj.name for obj in replay.objects.values()} ) ) diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 25b58189..634aff53 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -174,14 +174,10 @@ def main(): for filepath in utils.get_files(path, depth=depth): name, ext = os.path.splitext(filepath) if ext.lower() == ".sc2replay": - print( - f"\n--------------------------------------\n{filepath}\n" - ) + print(f"\n--------------------------------------\n{filepath}\n") printReplay(filepath, arguments) elif ext.lower() == ".s2gs": - print( - f"\n--------------------------------------\n{filepath}\n" - ) + print(f"\n--------------------------------------\n{filepath}\n") printGameSummary(filepath, arguments) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index c3e61b5e..c6c16c93 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -77,9 +77,7 @@ def main(): for filename in sc2reader.utils.get_files(args.FILE): replay = sc2reader.load_replay(filename, debug=True) print(f"Release {replay.release_string}") - print( - f"{replay.type} on {replay.map_name} at {replay.start_time}" - ) + print(f"{replay.type} on {replay.map_name} at {replay.start_time}") print("") for team in replay.teams: print(team) diff --git a/setup.py b/setup.py index f0f57022..95bf19fd 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,3 @@ -import sys import setuptools setuptools.setup( @@ -20,13 +19,10 @@ "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Games/Entertainment", @@ -44,9 +40,7 @@ "sc2json = sc2reader.scripts.sc2json:main", ] }, - install_requires=["mpyq>=0.2.3", "argparse", "ordereddict", "unittest2", "pil"] - if float(sys.version[:3]) < 2.7 - else ["mpyq>=0.2.4"], + install_requires=["mpyq", "pillow"], tests_require=["pytest"], packages=setuptools.find_packages(), include_package_data=True, diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index a7f7ac37..022be926 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -211,13 +211,11 @@ def test_hots_pids(self): replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") - player_pids = { - player.pid for player in replay.players if player.is_human - } + player_pids = {player.pid for player in replay.players if player.is_human} ability_pids = { - event.player.pid - for event in replay.events - if "CommandEvent" in event.name + event.player.pid + for event in replay.events + if "CommandEvent" in event.name } self.assertEqual(ability_pids, player_pids) @@ -227,9 +225,7 @@ def test_wol_pids(self): ) self.assertEqual(replay.expansion, "WoL") ability_pids = { - event.player.pid - for event in replay.events - if "CommandEvent" in event.name + event.player.pid for event in replay.events if "CommandEvent" in event.name } player_pids = {player.pid for player in replay.players} self.assertEqual(ability_pids, player_pids) @@ -238,10 +234,10 @@ def test_hots_hatchfun(self): replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") player_pids = {player.pid for player in replay.players} spawner_pids = { - event.player.pid - for event in replay.events - if "TargetUnitCommandEvent" in event.name - and event.ability.name == "SpawnLarva" + event.player.pid + for event in replay.events + if "TargetUnitCommandEvent" in event.name + and event.ability.name == "SpawnLarva" } self.assertTrue(spawner_pids.issubset(player_pids)) @@ -400,10 +396,10 @@ def test_gameheartnormalizer_plugin(self): replay = sc2reader.load_replay("test_replays/2.0.0.24247/molten.SC2Replay") player_pids = {player.pid for player in replay.players} spawner_pids = { - event.player.pid - for event in replay.events - if "TargetUnitCommandEvent" in event.name - and event.ability.name == "SpawnLarva" + event.player.pid + for event in replay.events + if "TargetUnitCommandEvent" in event.name + and event.ability.name == "SpawnLarva" } self.assertTrue(spawner_pids.issubset(player_pids)) @@ -577,9 +573,9 @@ def test_funny_minerals(self): ] mineralFieldNames = list( { - ou.attributes["UnitType"].value - for ou in itemlist - if "MineralField" in ou.attributes["UnitType"].value + ou.attributes["UnitType"].value + for ou in itemlist + if "MineralField" in ou.attributes["UnitType"].value } ) # print(mineralFieldNames) @@ -595,9 +591,7 @@ def test_32(self): def test_33(self): for replaynum in range(1, 4): - replay = sc2reader.load_replay( - f"test_replays/3.3.0/{replaynum}.SC2Replay" - ) + replay = sc2reader.load_replay(f"test_replays/3.3.0/{replaynum}.SC2Replay") self.assertTrue(replay is not None) def test_33_shift_click_calldown_mule(self): From 6acc9e10e950cb1a7388c8bd8b6706df8cd8dea9 Mon Sep 17 00:00:00 2001 From: Robert Heine Date: Sun, 7 Aug 2022 14:20:56 -0400 Subject: [PATCH 104/136] adjust attribute to reflect whats being checked --- sc2reader/engine/plugins/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 1a07ca6c..d047c25f 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -307,7 +307,7 @@ def handleUnitDoneEvent(self, event, replay): else: self.logger.error( "Unit {} done at {} [{}] before it was started!".format( - event.killer_pid, Length(seconds=event.second), event.frame + event.unit_id, Length(seconds=event.second), event.frame ) ) @@ -323,7 +323,7 @@ def handleUnitPositionsEvent(self, event, replay): else: self.logger.error( "Unit at active_unit index {} moved at {} [{}] but it doesn't exist!".format( - event.killer_pid, Length(seconds=event.second), event.frame + event.unit_index, Length(seconds=event.second), event.frame ) ) From c4dd5da9f3dfd1ac9535b35e8c90f296b1683c36 Mon Sep 17 00:00:00 2001 From: Robert Heine Date: Sun, 7 Aug 2022 17:06:15 -0400 Subject: [PATCH 105/136] unit_index isnt an attribute of an event its a var --- sc2reader/engine/plugins/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index d047c25f..42b03d3d 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -323,7 +323,7 @@ def handleUnitPositionsEvent(self, event, replay): else: self.logger.error( "Unit at active_unit index {} moved at {} [{}] but it doesn't exist!".format( - event.unit_index, Length(seconds=event.second), event.frame + unit_index, Length(seconds=event.second), event.frame ) ) From 310cb1dba370d5371b31b35273e37e106f0f91ea Mon Sep 17 00:00:00 2001 From: Robert Heine Date: Sun, 7 Aug 2022 17:20:10 -0400 Subject: [PATCH 106/136] change value to better reflect logged error --- sc2reader/engine/plugins/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 42b03d3d..4adfc0e1 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -198,7 +198,7 @@ def handleUnitDiedEvent(self, event, replay): else: self.logger.error( "Unable to delete unit index {} at {} [{}], index not active.".format( - event.killer_pid, Length(seconds=event.second), event.frame + event.unit_id_index, Length(seconds=event.second), event.frame ) ) else: From 5400aa0ee82eb4ee93a2dd5d8cdd648c1dfc623a Mon Sep 17 00:00:00 2001 From: Bean Date: Mon, 13 Feb 2023 21:03:48 +0800 Subject: [PATCH 107/136] add banalce data for 89720 --- sc2reader/data/LotV/89720_abilities.csv | 409 +++++++++ sc2reader/data/LotV/89720_units.csv | 1058 +++++++++++++++++++++++ sc2reader/data/__init__.py | 1 + sc2reader/data/ability_lookup.csv | 3 +- sc2reader/resources.py | 6 +- 5 files changed, 1475 insertions(+), 2 deletions(-) create mode 100644 sc2reader/data/LotV/89720_abilities.csv create mode 100644 sc2reader/data/LotV/89720_units.csv diff --git a/sc2reader/data/LotV/89720_abilities.csv b/sc2reader/data/LotV/89720_abilities.csv new file mode 100644 index 00000000..b6bf64d9 --- /dev/null +++ b/sc2reader/data/LotV/89720_abilities.csv @@ -0,0 +1,409 @@ +40,Taunt +41,stop +43,move +46,attack +62,SprayTerran +63,SprayZerg +64,SprayProtoss +65,SalvageShared +67,GhostHoldFire +68,GhostWeaponsFree +70,Explode +71,FleetBeaconResearch +72,FungalGrowth +73,GuardianShield +74,MULERepair +76,NexusTrainMothership +77,Feedback +78,MassRecall +80,HallucinationArchon +81,HallucinationColossus +82,HallucinationHighTemplar +83,HallucinationImmortal +84,HallucinationPhoenix +85,HallucinationProbe +86,HallucinationStalker +87,HallucinationVoidRay +88,HallucinationWarpPrism +89,HallucinationZealot +90,MULEGather +92,CalldownMULE +93,GravitonBeam +97,SpawnChangeling +104,Rally +105,ProgressRally +106,RallyCommand +107,RallyNexus +108,RallyHatchery +109,RoachWarrenResearch +112,NeuralParasite +113,SpawnLarva +114,StimpackMarauder +115,SupplyDrop +119,UltraliskCavernResearch +121,SCVHarvest +122,ProbeHarvest +124,que1 +125,que5 +126,que5CancelToSelection +128,que5Addon +129,BuildInProgress +130,Repair +131,TerranBuild +133,Stimpack +134,GhostCloak +136,MedivacHeal +137,SiegeMode +138,Unsiege +139,BansheeCloak +140,MedivacTransport +141,ScannerSweep +142,Yamato +143,AssaultMode +144,FighterMode +145,BunkerTransport +146,CommandCenterTransport +147,CommandCenterLiftOff +148,CommandCenterLand +149,BarracksBuild +150,BarracksLiftOff +151,FactoryFlyingBuild +152,FactoryLiftOff +153,StarportBuild +154,StarportLiftOff +155,FactoryLand +156,StarportLand +157,OrbitalCommandTrain +158,BarracksLand +159,SupplyDepotLower +160,SupplyDepotRaise +161,BarracksTrain +162,FactoryTrain +163,StarportTrain +164,EngineeringBayResearch +166,GhostAcademyTrain +167,BarracksTechLabResearch +168,FactoryTechLabResearch +169,StarportTechLabResearch +170,GhostAcademyResearch +171,ArmoryResearch +172,ProtossBuild +173,WarpPrismTransport +174,GatewayTrain +175,StargateTrain +176,RoboticsFacilityTrain +177,NexusTrain +178,PsiStorm +179,HangarQueue5 +181,CarrierTrain +182,ForgeResearch +183,RoboticsBayResearch +184,TemplarArchiveResearch +185,ZergBuild +186,DroneHarvest +187,EvolutionChamberResearch +188,UpgradeToLair +189,UpgradeToHive +190,UpgradeToGreaterSpire +191,HiveResearch +192,SpawningPoolResearch +193,HydraliskDenResearch +194,GreaterSpireResearch +195,LarvaTrain +196,MorphToBroodLord +197,BurrowBanelingDown +198,BurrowBanelingUp +199,BurrowDroneDown +200,BurrowDroneUp +201,BurrowHydraliskDown +202,BurrowHydraliskUp +203,BurrowRoachDown +204,BurrowRoachUp +205,BurrowZerglingDown +206,BurrowZerglingUp +207,BurrowInfestorTerranDown +208,BurrowInfestorTerranUp +209,RedstoneLavaCritterBurrow +210,RedstoneLavaCritterInjuredBurrow +211,RedstoneLavaCritterUnburrow +212,RedstoneLavaCritterInjuredUnburrow +213,OverlordTransport +216,WarpGateTrain +217,BurrowQueenDown +218,BurrowQueenUp +219,NydusCanalTransport +220,Blink +221,BurrowInfestorDown +222,BurrowInfestorUp +223,MorphToOverseer +224,UpgradeToPlanetaryFortress +225,InfestationPitResearch +226,BanelingNestResearch +227,BurrowUltraliskDown +228,BurrowUltraliskUp +229,UpgradeToOrbital +230,UpgradeToWarpGate +231,MorphBackToGateway +232,OrbitalLiftOff +233,OrbitalCommandLand +234,ForceField +235,PhasingMode +236,TransportMode +237,FusionCoreResearch +238,CyberneticsCoreResearch +239,TwilightCouncilResearch +240,TacNukeStrike +243,EMP +245,HiveTrain +247,Transfusion +256,AttackRedirect +257,StimpackRedirect +258,StimpackMarauderRedirect +260,StopRedirect +261,GenerateCreep +262,QueenBuild +263,SpineCrawlerUproot +264,SporeCrawlerUproot +265,SpineCrawlerRoot +266,SporeCrawlerRoot +267,CreepTumorBurrowedBuild +268,BuildAutoTurret +269,ArchonWarp +270,NydusNetworkBuild +272,Charge +276,Contaminate +279,que5Passive +280,que5PassiveCancelToSelection +283,RavagerCorrosiveBile +305,BurrowLurkerMPDown +306,BurrowLurkerMPUp +309,BurrowRavagerDown +310,BurrowRavagerUp +311,MorphToRavager +312,MorphToTransportOverlord +314,ThorNormalMode +319,DigesterCreepSpray +323,MorphToMothership +348,XelNagaHealingShrine +357,MothershipCoreMassRecall +359,MorphToHellion +369,MorphToHellionTank +377,MorphToSwarmHostBurrowedMP +378,MorphToSwarmHostMP +383,BlindingCloud +385,Yoink +388,ViperConsumeStructure +391,TestZerg +392,VolatileBurstBuilding +399,WidowMineBurrow +400,WidowMineUnburrow +401,WidowMineAttack +402,TornadoMissile +405,HallucinationOracle +406,MedivacSpeedBoost +407,ExtendingBridgeNEWide8Out +408,ExtendingBridgeNEWide8 +409,ExtendingBridgeNWWide8Out +410,ExtendingBridgeNWWide8 +411,ExtendingBridgeNEWide10Out +412,ExtendingBridgeNEWide10 +413,ExtendingBridgeNWWide10Out +414,ExtendingBridgeNWWide10 +415,ExtendingBridgeNEWide12Out +416,ExtendingBridgeNEWide12 +417,ExtendingBridgeNWWide12Out +418,ExtendingBridgeNWWide12 +420,CritterFlee +421,OracleRevelation +429,MothershipCorePurifyNexus +430,XelNaga_Caverns_DoorE +431,XelNaga_Caverns_DoorEOpened +432,XelNaga_Caverns_DoorN +433,XelNaga_Caverns_DoorNE +434,XelNaga_Caverns_DoorNEOpened +435,XelNaga_Caverns_DoorNOpened +436,XelNaga_Caverns_DoorNW +437,XelNaga_Caverns_DoorNWOpened +438,XelNaga_Caverns_DoorS +439,XelNaga_Caverns_DoorSE +440,XelNaga_Caverns_DoorSEOpened +441,XelNaga_Caverns_DoorSOpened +442,XelNaga_Caverns_DoorSW +443,XelNaga_Caverns_DoorSWOpened +444,XelNaga_Caverns_DoorW +445,XelNaga_Caverns_DoorWOpened +446,XelNaga_Caverns_Floating_BridgeNE8Out +447,XelNaga_Caverns_Floating_BridgeNE8 +448,XelNaga_Caverns_Floating_BridgeNW8Out +449,XelNaga_Caverns_Floating_BridgeNW8 +450,XelNaga_Caverns_Floating_BridgeNE10Out +451,XelNaga_Caverns_Floating_BridgeNE10 +452,XelNaga_Caverns_Floating_BridgeNW10Out +453,XelNaga_Caverns_Floating_BridgeNW10 +454,XelNaga_Caverns_Floating_BridgeNE12Out +455,XelNaga_Caverns_Floating_BridgeNE12 +456,XelNaga_Caverns_Floating_BridgeNW12Out +457,XelNaga_Caverns_Floating_BridgeNW12 +458,XelNaga_Caverns_Floating_BridgeH8Out +459,XelNaga_Caverns_Floating_BridgeH8 +460,XelNaga_Caverns_Floating_BridgeV8Out +461,XelNaga_Caverns_Floating_BridgeV8 +462,XelNaga_Caverns_Floating_BridgeH10Out +463,XelNaga_Caverns_Floating_BridgeH10 +464,XelNaga_Caverns_Floating_BridgeV10Out +465,XelNaga_Caverns_Floating_BridgeV10 +466,XelNaga_Caverns_Floating_BridgeH12Out +467,XelNaga_Caverns_Floating_BridgeH12 +468,XelNaga_Caverns_Floating_BridgeV12Out +469,XelNaga_Caverns_Floating_BridgeV12 +470,TemporalField +496,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +497,SnowRefinery_Terran_ExtendingBridgeNEShort8 +498,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +499,SnowRefinery_Terran_ExtendingBridgeNWShort8 +521,CausticSpray +524,MorphToLurker +528,PurificationNovaTargeted +530,LockOn +532,LockOnCancel +534,Hyperjump +536,ThorAPMode +539,NydusWormTransport +540,OracleWeapon +546,LocustMPFlyingSwoop +547,HallucinationDisruptor +548,HallucinationAdept +549,VoidRaySwarmDamageBoost +550,SeekerDummyChannel +551,AiurLightBridgeNE8Out +552,AiurLightBridgeNE8 +553,AiurLightBridgeNE10Out +554,AiurLightBridgeNE10 +555,AiurLightBridgeNE12Out +556,AiurLightBridgeNE12 +557,AiurLightBridgeNW8Out +558,AiurLightBridgeNW8 +559,AiurLightBridgeNW10Out +560,AiurLightBridgeNW10 +561,AiurLightBridgeNW12Out +562,AiurLightBridgeNW12 +575,ShakurasLightBridgeNE8Out +576,ShakurasLightBridgeNE8 +577,ShakurasLightBridgeNE10Out +578,ShakurasLightBridgeNE10 +579,ShakurasLightBridgeNE12Out +580,ShakurasLightBridgeNE12 +581,ShakurasLightBridgeNW8Out +582,ShakurasLightBridgeNW8 +583,ShakurasLightBridgeNW10Out +584,ShakurasLightBridgeNW10 +585,ShakurasLightBridgeNW12Out +586,ShakurasLightBridgeNW12 +587,VoidMPImmortalReviveRebuild +589,ArbiterMPStasisField +590,ArbiterMPRecall +591,CorsairMPDisruptionWeb +592,MorphToGuardianMP +593,MorphToDevourerMP +594,DefilerMPConsume +595,DefilerMPDarkSwarm +596,DefilerMPPlague +597,DefilerMPBurrow +598,DefilerMPUnburrow +599,QueenMPEnsnare +600,QueenMPSpawnBroodlings +601,QueenMPInfestCommandCenter +605,OracleBuild +609,ParasiticBomb +610,AdeptPhaseShift +613,LurkerHoldFire +614,LurkerRemoveHoldFire +617,LiberatorAGTarget +618,LiberatorAATarget +620,AiurLightBridgeAbandonedNE8Out +621,AiurLightBridgeAbandonedNE8 +622,AiurLightBridgeAbandonedNE10Out +623,AiurLightBridgeAbandonedNE10 +624,AiurLightBridgeAbandonedNE12Out +625,AiurLightBridgeAbandonedNE12 +626,AiurLightBridgeAbandonedNW8Out +627,AiurLightBridgeAbandonedNW8 +628,AiurLightBridgeAbandonedNW10Out +629,AiurLightBridgeAbandonedNW10 +630,AiurLightBridgeAbandonedNW12Out +631,AiurLightBridgeAbandonedNW12 +632,KD8Charge +635,AdeptPhaseShiftCancel +636,AdeptShadePhaseShiftCancel +637,SlaynElementalGrab +639,PortCity_Bridge_UnitNE8Out +640,PortCity_Bridge_UnitNE8 +641,PortCity_Bridge_UnitSE8Out +642,PortCity_Bridge_UnitSE8 +643,PortCity_Bridge_UnitNW8Out +644,PortCity_Bridge_UnitNW8 +645,PortCity_Bridge_UnitSW8Out +646,PortCity_Bridge_UnitSW8 +647,PortCity_Bridge_UnitNE10Out +648,PortCity_Bridge_UnitNE10 +649,PortCity_Bridge_UnitSE10Out +650,PortCity_Bridge_UnitSE10 +651,PortCity_Bridge_UnitNW10Out +652,PortCity_Bridge_UnitNW10 +653,PortCity_Bridge_UnitSW10Out +654,PortCity_Bridge_UnitSW10 +655,PortCity_Bridge_UnitNE12Out +656,PortCity_Bridge_UnitNE12 +657,PortCity_Bridge_UnitSE12Out +658,PortCity_Bridge_UnitSE12 +659,PortCity_Bridge_UnitNW12Out +660,PortCity_Bridge_UnitNW12 +661,PortCity_Bridge_UnitSW12Out +662,PortCity_Bridge_UnitSW12 +663,PortCity_Bridge_UnitN8Out +664,PortCity_Bridge_UnitN8 +665,PortCity_Bridge_UnitS8Out +666,PortCity_Bridge_UnitS8 +667,PortCity_Bridge_UnitE8Out +668,PortCity_Bridge_UnitE8 +669,PortCity_Bridge_UnitW8Out +670,PortCity_Bridge_UnitW8 +671,PortCity_Bridge_UnitN10Out +672,PortCity_Bridge_UnitN10 +673,PortCity_Bridge_UnitS10Out +674,PortCity_Bridge_UnitS10 +675,PortCity_Bridge_UnitE10Out +676,PortCity_Bridge_UnitE10 +677,PortCity_Bridge_UnitW10Out +678,PortCity_Bridge_UnitW10 +679,PortCity_Bridge_UnitN12Out +680,PortCity_Bridge_UnitN12 +681,PortCity_Bridge_UnitS12Out +682,PortCity_Bridge_UnitS12 +683,PortCity_Bridge_UnitE12Out +684,PortCity_Bridge_UnitE12 +685,PortCity_Bridge_UnitW12Out +686,PortCity_Bridge_UnitW12 +689,DarkTemplarBlink +693,BattlecruiserAttack +695,BattlecruiserMove +697,BattlecruiserStop +698,BatteryOvercharge +700,AmorphousArmorcloud +702,SpawnLocustsTargeted +703,ViperParasiticBombRelay +704,ParasiticBombRelayDodge +705,VoidRaySwarmDamageBoostCancel +709,ChannelSnipe +712,DarkShrineResearch +713,LurkerDenMPResearch +714,ObserverSiegeMorphtoObserver +715,ObserverMorphtoObserverSiege +716,OverseerMorphtoOverseerSiegeMode +717,OverseerSiegeModeMorphtoOverseer +718,RavenScramblerMissile +720,RavenRepairDroneHeal +721,RavenShredderMissile +722,ChronoBoostEnergyCost +723,NexusMassRecall +729,MorphToBaneling diff --git a/sc2reader/data/LotV/89720_units.csv b/sc2reader/data/LotV/89720_units.csv new file mode 100644 index 00000000..19aa551f --- /dev/null +++ b/sc2reader/data/LotV/89720_units.csv @@ -0,0 +1,1058 @@ +3,System_Snapshot_Dummy +21,Ball +22,StereoscopicOptionsUnit +23,Colossus +24,TechLab +25,Reactor +27,InfestorTerran +28,BanelingCocoon +29,Baneling +30,Mothership +31,PointDefenseDrone +32,Changeling +33,ChangelingZealot +34,ChangelingMarineShield +35,ChangelingMarine +36,ChangelingZerglingWings +37,ChangelingZergling +39,CommandCenter +40,SupplyDepot +41,Refinery +42,Barracks +43,EngineeringBay +44,MissileTurret +45,Bunker +46,RefineryRich +47,SensorTower +48,GhostAcademy +49,Factory +50,Starport +52,Armory +53,FusionCore +54,AutoTurret +55,SiegeTankSieged +56,SiegeTank +57,VikingAssault +58,VikingFighter +59,CommandCenterFlying +60,BarracksTechLab +61,BarracksReactor +62,FactoryTechLab +63,FactoryReactor +64,StarportTechLab +65,StarportReactor +66,FactoryFlying +67,StarportFlying +68,SCV +69,BarracksFlying +70,SupplyDepotLowered +71,Marine +72,Reaper +73,Ghost +74,Marauder +75,Thor +76,Hellion +77,Medivac +78,Banshee +79,Raven +80,Battlecruiser +81,Nuke +82,Nexus +83,Pylon +84,Assimilator +85,Gateway +86,Forge +87,FleetBeacon +88,TwilightCouncil +89,PhotonCannon +90,Stargate +91,TemplarArchive +92,DarkShrine +93,RoboticsBay +94,RoboticsFacility +95,CyberneticsCore +96,Zealot +97,Stalker +98,HighTemplar +99,DarkTemplar +100,Sentry +101,Phoenix +102,Carrier +103,VoidRay +104,WarpPrism +105,Observer +106,Immortal +107,Probe +108,Interceptor +109,Hatchery +110,CreepTumor +111,Extractor +112,SpawningPool +113,EvolutionChamber +114,HydraliskDen +115,Spire +116,UltraliskCavern +117,InfestationPit +118,NydusNetwork +119,BanelingNest +120,RoachWarren +121,SpineCrawler +122,SporeCrawler +123,Lair +124,Hive +125,GreaterSpire +126,Egg +127,Drone +128,Zergling +129,Overlord +130,Hydralisk +131,Mutalisk +132,Ultralisk +133,Roach +134,Infestor +135,Corruptor +136,BroodLordCocoon +137,BroodLord +138,BanelingBurrowed +139,DroneBurrowed +140,HydraliskBurrowed +141,RoachBurrowed +142,ZerglingBurrowed +143,InfestorTerranBurrowed +144,RedstoneLavaCritterBurrowed +145,RedstoneLavaCritterInjuredBurrowed +146,RedstoneLavaCritter +147,RedstoneLavaCritterInjured +148,QueenBurrowed +149,Queen +150,InfestorBurrowed +151,OverlordCocoon +152,Overseer +153,PlanetaryFortress +154,UltraliskBurrowed +155,OrbitalCommand +156,WarpGate +157,OrbitalCommandFlying +158,ForceField +159,WarpPrismPhasing +160,CreepTumorBurrowed +161,CreepTumorQueen +162,SpineCrawlerUprooted +163,SporeCrawlerUprooted +164,Archon +165,NydusCanal +166,BroodlingEscort +167,GhostAlternate +168,GhostNova +169,RichMineralField +170,RichMineralField750 +171,Ursadon +173,LurkerMPBurrowed +174,LurkerMP +175,LurkerDenMP +176,LurkerMPEgg +177,NydusCanalAttacker +178,OverlordTransport +179,Ravager +180,RavagerBurrowed +181,RavagerCocoon +182,TransportOverlordCocoon +183,XelNagaTower +185,Oracle +186,Tempest +188,InfestedTerransEgg +189,Larva +190,OverseerSiegeMode +192,ReaperPlaceholder +193,MarineACGluescreenDummy +194,FirebatACGluescreenDummy +195,MedicACGluescreenDummy +196,MarauderACGluescreenDummy +197,VultureACGluescreenDummy +198,SiegeTankACGluescreenDummy +199,VikingACGluescreenDummy +200,BansheeACGluescreenDummy +201,BattlecruiserACGluescreenDummy +202,OrbitalCommandACGluescreenDummy +203,BunkerACGluescreenDummy +204,BunkerUpgradedACGluescreenDummy +205,MissileTurretACGluescreenDummy +206,HellbatACGluescreenDummy +207,GoliathACGluescreenDummy +208,CycloneACGluescreenDummy +209,WraithACGluescreenDummy +210,ScienceVesselACGluescreenDummy +211,HerculesACGluescreenDummy +212,ThorACGluescreenDummy +213,PerditionTurretACGluescreenDummy +214,FlamingBettyACGluescreenDummy +215,DevastationTurretACGluescreenDummy +216,BlasterBillyACGluescreenDummy +217,SpinningDizzyACGluescreenDummy +218,ZerglingKerriganACGluescreenDummy +219,RaptorACGluescreenDummy +220,QueenCoopACGluescreenDummy +221,HydraliskACGluescreenDummy +222,HydraliskLurkerACGluescreenDummy +223,MutaliskBroodlordACGluescreenDummy +224,BroodLordACGluescreenDummy +225,UltraliskACGluescreenDummy +226,TorrasqueACGluescreenDummy +227,OverseerACGluescreenDummy +228,LurkerACGluescreenDummy +229,SpineCrawlerACGluescreenDummy +230,SporeCrawlerACGluescreenDummy +231,NydusNetworkACGluescreenDummy +232,OmegaNetworkACGluescreenDummy +233,ZerglingZagaraACGluescreenDummy +234,SwarmlingACGluescreenDummy +235,QueenZagaraACGluescreenDummy +236,BanelingACGluescreenDummy +237,SplitterlingACGluescreenDummy +238,AberrationACGluescreenDummy +239,ScourgeACGluescreenDummy +240,CorruptorACGluescreenDummy +241,OverseerZagaraACGluescreenDummy +242,BileLauncherACGluescreenDummy +243,SwarmQueenACGluescreenDummy +244,RoachACGluescreenDummy +245,RoachVileACGluescreenDummy +246,RavagerACGluescreenDummy +247,SwarmHostACGluescreenDummy +248,MutaliskACGluescreenDummy +249,GuardianACGluescreenDummy +250,DevourerACGluescreenDummy +251,ViperACGluescreenDummy +252,BrutaliskACGluescreenDummy +253,LeviathanACGluescreenDummy +254,ZealotACGluescreenDummy +255,ZealotAiurACGluescreenDummy +256,DragoonACGluescreenDummy +257,HighTemplarACGluescreenDummy +258,ArchonACGluescreenDummy +259,ImmortalACGluescreenDummy +260,ObserverACGluescreenDummy +261,PhoenixAiurACGluescreenDummy +262,ReaverACGluescreenDummy +263,TempestACGluescreenDummy +264,PhotonCannonACGluescreenDummy +265,ZealotVorazunACGluescreenDummy +266,ZealotShakurasACGluescreenDummy +267,StalkerShakurasACGluescreenDummy +268,DarkTemplarShakurasACGluescreenDummy +269,CorsairACGluescreenDummy +270,VoidRayACGluescreenDummy +271,VoidRayShakurasACGluescreenDummy +272,OracleACGluescreenDummy +273,DarkArchonACGluescreenDummy +274,DarkPylonACGluescreenDummy +275,ZealotPurifierACGluescreenDummy +276,SentryPurifierACGluescreenDummy +277,ImmortalKaraxACGluescreenDummy +278,ColossusACGluescreenDummy +279,ColossusPurifierACGluescreenDummy +280,PhoenixPurifierACGluescreenDummy +281,CarrierACGluescreenDummy +282,CarrierAiurACGluescreenDummy +283,KhaydarinMonolithACGluescreenDummy +284,ShieldBatteryACGluescreenDummy +285,EliteMarineACGluescreenDummy +286,MarauderCommandoACGluescreenDummy +287,SpecOpsGhostACGluescreenDummy +288,HellbatRangerACGluescreenDummy +289,StrikeGoliathACGluescreenDummy +290,HeavySiegeTankACGluescreenDummy +291,RaidLiberatorACGluescreenDummy +292,RavenTypeIIACGluescreenDummy +293,CovertBansheeACGluescreenDummy +294,RailgunTurretACGluescreenDummy +295,BlackOpsMissileTurretACGluescreenDummy +296,SupplicantACGluescreenDummy +297,StalkerTaldarimACGluescreenDummy +298,SentryTaldarimACGluescreenDummy +299,HighTemplarTaldarimACGluescreenDummy +300,ImmortalTaldarimACGluescreenDummy +301,ColossusTaldarimACGluescreenDummy +302,WarpPrismTaldarimACGluescreenDummy +303,PhotonCannonTaldarimACGluescreenDummy +304,StukovInfestedCivilianACGluescreenDummy +305,StukovInfestedMarineACGluescreenDummy +306,StukovInfestedSiegeTankACGluescreenDummy +307,StukovInfestedDiamondbackACGluescreenDummy +308,StukovInfestedBansheeACGluescreenDummy +309,SILiberatorACGluescreenDummy +310,StukovInfestedBunkerACGluescreenDummy +311,StukovInfestedMissileTurretACGluescreenDummy +312,StukovBroodQueenACGluescreenDummy +313,ZealotFenixACGluescreenDummy +314,SentryFenixACGluescreenDummy +315,AdeptFenixACGluescreenDummy +316,ImmortalFenixACGluescreenDummy +317,ColossusFenixACGluescreenDummy +318,DisruptorACGluescreenDummy +319,ObserverFenixACGluescreenDummy +320,ScoutACGluescreenDummy +321,CarrierFenixACGluescreenDummy +322,PhotonCannonFenixACGluescreenDummy +323,PrimalZerglingACGluescreenDummy +324,RavasaurACGluescreenDummy +325,PrimalRoachACGluescreenDummy +326,FireRoachACGluescreenDummy +327,PrimalGuardianACGluescreenDummy +328,PrimalHydraliskACGluescreenDummy +329,PrimalMutaliskACGluescreenDummy +330,PrimalImpalerACGluescreenDummy +331,PrimalSwarmHostACGluescreenDummy +332,CreeperHostACGluescreenDummy +333,PrimalUltraliskACGluescreenDummy +334,TyrannozorACGluescreenDummy +335,PrimalWurmACGluescreenDummy +336,HHReaperACGluescreenDummy +337,HHWidowMineACGluescreenDummy +338,HHHellionTankACGluescreenDummy +339,HHWraithACGluescreenDummy +340,HHVikingACGluescreenDummy +341,HHBattlecruiserACGluescreenDummy +342,HHRavenACGluescreenDummy +343,HHBomberPlatformACGluescreenDummy +344,HHMercStarportACGluescreenDummy +345,HHMissileTurretACGluescreenDummy +346,TychusReaperACGluescreenDummy +347,TychusFirebatACGluescreenDummy +348,TychusSpectreACGluescreenDummy +349,TychusMedicACGluescreenDummy +350,TychusMarauderACGluescreenDummy +351,TychusWarhoundACGluescreenDummy +352,TychusHERCACGluescreenDummy +353,TychusGhostACGluescreenDummy +354,TychusSCVAutoTurretACGluescreenDummy +355,ZeratulStalkerACGluescreenDummy +356,ZeratulSentryACGluescreenDummy +357,ZeratulDarkTemplarACGluescreenDummy +358,ZeratulImmortalACGluescreenDummy +359,ZeratulObserverACGluescreenDummy +360,ZeratulDisruptorACGluescreenDummy +361,ZeratulWarpPrismACGluescreenDummy +362,ZeratulPhotonCannonACGluescreenDummy +363,MechaZerglingACGluescreenDummy +364,MechaBanelingACGluescreenDummy +365,MechaHydraliskACGluescreenDummy +366,MechaInfestorACGluescreenDummy +367,MechaCorruptorACGluescreenDummy +368,MechaUltraliskACGluescreenDummy +369,MechaOverseerACGluescreenDummy +370,MechaLurkerACGluescreenDummy +371,MechaBattlecarrierLordACGluescreenDummy +372,MechaSpineCrawlerACGluescreenDummy +373,MechaSporeCrawlerACGluescreenDummy +374,TrooperMengskACGluescreenDummy +375,MedivacMengskACGluescreenDummy +376,BlimpMengskACGluescreenDummy +377,MarauderMengskACGluescreenDummy +378,GhostMengskACGluescreenDummy +379,SiegeTankMengskACGluescreenDummy +380,ThorMengskACGluescreenDummy +381,VikingMengskACGluescreenDummy +382,BattlecruiserMengskACGluescreenDummy +383,BunkerDepotMengskACGluescreenDummy +384,MissileTurretMengskACGluescreenDummy +385,ArtilleryMengskACGluescreenDummy +387,RenegadeLongboltMissileWeapon +388,LoadOutSpray@1 +389,LoadOutSpray@2 +390,LoadOutSpray@3 +391,LoadOutSpray@4 +392,LoadOutSpray@5 +393,LoadOutSpray@6 +394,LoadOutSpray@7 +395,LoadOutSpray@8 +396,LoadOutSpray@9 +397,LoadOutSpray@10 +398,LoadOutSpray@11 +399,LoadOutSpray@12 +400,LoadOutSpray@13 +401,LoadOutSpray@14 +402,NeedleSpinesWeapon +403,CorruptionWeapon +404,InfestedTerransWeapon +405,NeuralParasiteWeapon +406,PointDefenseDroneReleaseWeapon +407,HunterSeekerWeapon +408,MULE +410,ThorAAWeapon +411,PunisherGrenadesLMWeapon +412,VikingFighterWeapon +413,ATALaserBatteryLMWeapon +414,ATSLaserBatteryLMWeapon +415,LongboltMissileWeapon +416,D8ChargeWeapon +417,YamatoWeapon +418,IonCannonsWeapon +419,AcidSalivaWeapon +420,SpineCrawlerWeapon +421,SporeCrawlerWeapon +422,GlaiveWurmWeapon +423,GlaiveWurmM2Weapon +424,GlaiveWurmM3Weapon +425,StalkerWeapon +426,EMP2Weapon +427,BacklashRocketsLMWeapon +428,PhotonCannonWeapon +429,ParasiteSporeWeapon +431,Broodling +432,BroodLordBWeapon +435,AutoTurretReleaseWeapon +436,LarvaReleaseMissile +437,AcidSpinesWeapon +438,FrenzyWeapon +439,ContaminateWeapon +451,BeaconArmy +452,BeaconDefend +453,BeaconAttack +454,BeaconHarass +455,BeaconIdle +456,BeaconAuto +457,BeaconDetect +458,BeaconScout +459,BeaconClaim +460,BeaconExpand +461,BeaconRally +462,BeaconCustom1 +463,BeaconCustom2 +464,BeaconCustom3 +465,BeaconCustom4 +470,LiberatorAG +472,PreviewBunkerUpgraded +473,HellionTank +474,Cyclone +475,WidowMine +476,Liberator +478,Adept +479,Disruptor +480,SwarmHostMP +481,Viper +482,ShieldBattery +483,HighTemplarSkinPreview +484,MothershipCore +485,Viking +498,InhibitorZoneSmall +499,InhibitorZoneMedium +500,InhibitorZoneLarge +501,AccelerationZoneSmall +502,AccelerationZoneMedium +503,AccelerationZoneLarge +504,AccelerationZoneFlyingSmall +505,AccelerationZoneFlyingMedium +506,AccelerationZoneFlyingLarge +507,InhibitorZoneFlyingSmall +508,InhibitorZoneFlyingMedium +509,InhibitorZoneFlyingLarge +510,AssimilatorRich +511,RichVespeneGeyser +512,ExtractorRich +513,RavagerCorrosiveBileMissile +514,RavagerWeaponMissile +515,RenegadeMissileTurret +516,Rocks2x2NonConjoined +517,FungalGrowthMissile +518,NeuralParasiteTentacleMissile +519,Beacon_Protoss +520,Beacon_ProtossSmall +521,Beacon_Terran +522,Beacon_TerranSmall +523,Beacon_Zerg +524,Beacon_ZergSmall +525,Lyote +526,CarrionBird +527,KarakMale +528,KarakFemale +529,UrsadakFemaleExotic +530,UrsadakMale +531,UrsadakFemale +532,UrsadakCalf +533,UrsadakMaleExotic +534,UtilityBot +535,CommentatorBot1 +536,CommentatorBot2 +537,CommentatorBot3 +538,CommentatorBot4 +539,Scantipede +540,Dog +541,Sheep +542,Cow +543,InfestedTerransEggPlacement +544,InfestorTerransWeapon +545,MineralField +546,MineralField450 +547,MineralField750 +548,MineralFieldOpaque +549,MineralFieldOpaque900 +550,VespeneGeyser +551,SpacePlatformGeyser +552,DestructibleSearchlight +553,DestructibleBullhornLights +554,DestructibleStreetlight +555,DestructibleSpacePlatformSign +556,DestructibleStoreFrontCityProps +557,DestructibleBillboardTall +558,DestructibleBillboardScrollingText +559,DestructibleSpacePlatformBarrier +560,DestructibleSignsDirectional +561,DestructibleSignsConstruction +562,DestructibleSignsFunny +563,DestructibleSignsIcons +564,DestructibleSignsWarning +565,DestructibleGarage +566,DestructibleGarageLarge +567,DestructibleTrafficSignal +568,TrafficSignal +569,BraxisAlphaDestructible1x1 +570,BraxisAlphaDestructible2x2 +571,DestructibleDebris4x4 +572,DestructibleDebris6x6 +573,DestructibleRock2x4Vertical +574,DestructibleRock2x4Horizontal +575,DestructibleRock2x6Vertical +576,DestructibleRock2x6Horizontal +577,DestructibleRock4x4 +578,DestructibleRock6x6 +579,DestructibleRampDiagonalHugeULBR +580,DestructibleRampDiagonalHugeBLUR +581,DestructibleRampVerticalHuge +582,DestructibleRampHorizontalHuge +583,DestructibleDebrisRampDiagonalHugeULBR +584,DestructibleDebrisRampDiagonalHugeBLUR +585,WarpPrismSkinPreview +586,SiegeTankSkinPreview +587,ThorAP +588,ThorAALance +589,LiberatorSkinPreview +590,OverlordGenerateCreepKeybind +591,MengskStatueAlone +592,MengskStatue +593,WolfStatue +594,GlobeStatue +595,Weapon +596,GlaiveWurmBounceWeapon +597,BroodLordWeapon +598,BroodLordAWeapon +599,CreepBlocker1x1 +600,PermanentCreepBlocker1x1 +601,PathingBlocker1x1 +602,PathingBlocker2x2 +603,AutoTestAttackTargetGround +604,AutoTestAttackTargetAir +605,AutoTestAttacker +606,HelperEmitterSelectionArrow +607,MultiKillObject +608,ShapeGolfball +609,ShapeCone +610,ShapeCube +611,ShapeCylinder +612,ShapeDodecahedron +613,ShapeIcosahedron +614,ShapeOctahedron +615,ShapePyramid +616,ShapeRoundedCube +617,ShapeSphere +618,ShapeTetrahedron +619,ShapeThickTorus +620,ShapeThinTorus +621,ShapeTorus +622,Shape4PointStar +623,Shape5PointStar +624,Shape6PointStar +625,Shape8PointStar +626,ShapeArrowPointer +627,ShapeBowl +628,ShapeBox +629,ShapeCapsule +630,ShapeCrescentMoon +631,ShapeDecahedron +632,ShapeDiamond +633,ShapeFootball +634,ShapeGemstone +635,ShapeHeart +636,ShapeJack +637,ShapePlusSign +638,ShapeShamrock +639,ShapeSpade +640,ShapeTube +641,ShapeEgg +642,ShapeYenSign +643,ShapeX +644,ShapeWatermelon +645,ShapeWonSign +646,ShapeTennisball +647,ShapeStrawberry +648,ShapeSmileyFace +649,ShapeSoccerball +650,ShapeRainbow +651,ShapeSadFace +652,ShapePoundSign +653,ShapePear +654,ShapePineapple +655,ShapeOrange +656,ShapePeanut +657,ShapeO +658,ShapeLemon +659,ShapeMoneyBag +660,ShapeHorseshoe +661,ShapeHockeyStick +662,ShapeHockeyPuck +663,ShapeHand +664,ShapeGolfClub +665,ShapeGrape +666,ShapeEuroSign +667,ShapeDollarSign +668,ShapeBasketball +669,ShapeCarrot +670,ShapeCherry +671,ShapeBaseball +672,ShapeBaseballBat +673,ShapeBanana +674,ShapeApple +675,ShapeCashLarge +676,ShapeCashMedium +677,ShapeCashSmall +678,ShapeFootballColored +679,ShapeLemonSmall +680,ShapeOrangeSmall +681,ShapeTreasureChestOpen +682,ShapeTreasureChestClosed +683,ShapeWatermelonSmall +684,UnbuildableRocksDestructible +685,UnbuildableBricksDestructible +686,UnbuildablePlatesDestructible +687,Debris2x2NonConjoined +688,EnemyPathingBlocker1x1 +689,EnemyPathingBlocker2x2 +690,EnemyPathingBlocker4x4 +691,EnemyPathingBlocker8x8 +692,EnemyPathingBlocker16x16 +693,ScopeTest +694,SentryACGluescreenDummy +695,StukovInfestedTrooperACGluescreenDummy +711,CollapsibleTerranTowerDebris +712,DebrisRampLeft +713,DebrisRampRight +717,LocustMP +718,CollapsibleRockTowerDebris +719,NydusCanalCreeper +720,SwarmHostBurrowedMP +721,WarHound +722,WidowMineBurrowed +723,ExtendingBridgeNEWide8Out +724,ExtendingBridgeNEWide8 +725,ExtendingBridgeNWWide8Out +726,ExtendingBridgeNWWide8 +727,ExtendingBridgeNEWide10Out +728,ExtendingBridgeNEWide10 +729,ExtendingBridgeNWWide10Out +730,ExtendingBridgeNWWide10 +731,ExtendingBridgeNEWide12Out +732,ExtendingBridgeNEWide12 +733,ExtendingBridgeNWWide12Out +734,ExtendingBridgeNWWide12 +736,CollapsibleRockTowerDebrisRampRight +737,CollapsibleRockTowerDebrisRampLeft +738,XelNaga_Caverns_DoorE +739,XelNaga_Caverns_DoorEOpened +740,XelNaga_Caverns_DoorN +741,XelNaga_Caverns_DoorNE +742,XelNaga_Caverns_DoorNEOpened +743,XelNaga_Caverns_DoorNOpened +744,XelNaga_Caverns_DoorNW +745,XelNaga_Caverns_DoorNWOpened +746,XelNaga_Caverns_DoorS +747,XelNaga_Caverns_DoorSE +748,XelNaga_Caverns_DoorSEOpened +749,XelNaga_Caverns_DoorSOpened +750,XelNaga_Caverns_DoorSW +751,XelNaga_Caverns_DoorSWOpened +752,XelNaga_Caverns_DoorW +753,XelNaga_Caverns_DoorWOpened +754,XelNaga_Caverns_Floating_BridgeNE8Out +755,XelNaga_Caverns_Floating_BridgeNE8 +756,XelNaga_Caverns_Floating_BridgeNW8Out +757,XelNaga_Caverns_Floating_BridgeNW8 +758,XelNaga_Caverns_Floating_BridgeNE10Out +759,XelNaga_Caverns_Floating_BridgeNE10 +760,XelNaga_Caverns_Floating_BridgeNW10Out +761,XelNaga_Caverns_Floating_BridgeNW10 +762,XelNaga_Caverns_Floating_BridgeNE12Out +763,XelNaga_Caverns_Floating_BridgeNE12 +764,XelNaga_Caverns_Floating_BridgeNW12Out +765,XelNaga_Caverns_Floating_BridgeNW12 +766,XelNaga_Caverns_Floating_BridgeH8Out +767,XelNaga_Caverns_Floating_BridgeH8 +768,XelNaga_Caverns_Floating_BridgeV8Out +769,XelNaga_Caverns_Floating_BridgeV8 +770,XelNaga_Caverns_Floating_BridgeH10Out +771,XelNaga_Caverns_Floating_BridgeH10 +772,XelNaga_Caverns_Floating_BridgeV10Out +773,XelNaga_Caverns_Floating_BridgeV10 +774,XelNaga_Caverns_Floating_BridgeH12Out +775,XelNaga_Caverns_Floating_BridgeH12 +776,XelNaga_Caverns_Floating_BridgeV12Out +777,XelNaga_Caverns_Floating_BridgeV12 +780,CollapsibleTerranTowerPushUnitRampLeft +781,CollapsibleTerranTowerPushUnitRampRight +784,CollapsibleRockTowerPushUnit +785,CollapsibleTerranTowerPushUnit +786,CollapsibleRockTowerPushUnitRampRight +787,CollapsibleRockTowerPushUnitRampLeft +788,DigesterCreepSprayTargetUnit +789,DigesterCreepSprayUnit +790,NydusCanalAttackerWeapon +791,ViperConsumeStructureWeapon +794,ResourceBlocker +795,TempestWeapon +796,YoinkMissile +800,YoinkVikingAirMissile +802,YoinkVikingGroundMissile +804,YoinkSiegeTankMissile +806,WarHoundWeapon +808,EyeStalkWeapon +811,WidowMineWeapon +812,WidowMineAirWeapon +813,MothershipCoreWeaponWeapon +814,TornadoMissileWeapon +815,TornadoMissileDummyWeapon +816,TalonsMissileWeapon +817,CreepTumorMissile +818,LocustMPEggAMissileWeapon +819,LocustMPEggBMissileWeapon +820,LocustMPWeapon +822,RepulsorCannonWeapon +826,CollapsibleRockTowerDiagonal +827,CollapsibleTerranTowerDiagonal +828,CollapsibleTerranTowerRampLeft +829,CollapsibleTerranTowerRampRight +830,Ice2x2NonConjoined +831,IceProtossCrates +832,ProtossCrates +833,TowerMine +834,PickupPalletGas +835,PickupPalletMinerals +836,PickupScrapSalvage1x1 +837,PickupScrapSalvage2x2 +838,PickupScrapSalvage3x3 +839,RoughTerrain +840,UnbuildableBricksSmallUnit +841,UnbuildablePlatesSmallUnit +842,UnbuildablePlatesUnit +843,UnbuildableRocksSmallUnit +844,XelNagaHealingShrine +845,InvisibleTargetDummy +846,ProtossVespeneGeyser +847,CollapsibleRockTower +848,CollapsibleTerranTower +849,ThornLizard +850,CleaningBot +851,DestructibleRock6x6Weak +852,ProtossSnakeSegmentDemo +853,PhysicsCapsule +854,PhysicsCube +855,PhysicsCylinder +856,PhysicsKnot +857,PhysicsL +858,PhysicsPrimitives +859,PhysicsSphere +860,PhysicsStar +861,CreepBlocker4x4 +862,DestructibleCityDebris2x4Vertical +863,DestructibleCityDebris2x4Horizontal +864,DestructibleCityDebris2x6Vertical +865,DestructibleCityDebris2x6Horizontal +866,DestructibleCityDebris4x4 +867,DestructibleCityDebris6x6 +868,DestructibleCityDebrisHugeDiagonalBLUR +869,DestructibleCityDebrisHugeDiagonalULBR +870,TestZerg +871,PathingBlockerRadius1 +872,DestructibleRockEx12x4Vertical +873,DestructibleRockEx12x4Horizontal +874,DestructibleRockEx12x6Vertical +875,DestructibleRockEx12x6Horizontal +876,DestructibleRockEx14x4 +877,DestructibleRockEx16x6 +878,DestructibleRockEx1DiagonalHugeULBR +879,DestructibleRockEx1DiagonalHugeBLUR +880,DestructibleRockEx1VerticalHuge +881,DestructibleRockEx1HorizontalHuge +882,DestructibleIce2x4Vertical +883,DestructibleIce2x4Horizontal +884,DestructibleIce2x6Vertical +885,DestructibleIce2x6Horizontal +886,DestructibleIce4x4 +887,DestructibleIce6x6 +888,DestructibleIceDiagonalHugeULBR +889,DestructibleIceDiagonalHugeBLUR +890,DestructibleIceVerticalHuge +891,DestructibleIceHorizontalHuge +892,DesertPlanetSearchlight +893,DesertPlanetStreetlight +894,UnbuildableBricksUnit +895,UnbuildableRocksUnit +896,ZerusDestructibleArch +897,Artosilope +898,Anteplott +899,LabBot +900,Crabeetle +901,CollapsibleRockTowerRampRight +902,CollapsibleRockTowerRampLeft +903,LabMineralField +904,LabMineralField750 +919,CollapsibleRockTowerDebrisRampLeftGreen +920,CollapsibleRockTowerDebrisRampRightGreen +921,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +922,SnowRefinery_Terran_ExtendingBridgeNEShort8 +923,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +924,SnowRefinery_Terran_ExtendingBridgeNWShort8 +929,Tarsonis_DoorN +930,Tarsonis_DoorNLowered +931,Tarsonis_DoorNE +932,Tarsonis_DoorNELowered +933,Tarsonis_DoorE +934,Tarsonis_DoorELowered +935,Tarsonis_DoorNW +936,Tarsonis_DoorNWLowered +937,CompoundMansion_DoorN +938,CompoundMansion_DoorNLowered +939,CompoundMansion_DoorNE +940,CompoundMansion_DoorNELowered +941,CompoundMansion_DoorE +942,CompoundMansion_DoorELowered +943,CompoundMansion_DoorNW +944,CompoundMansion_DoorNWLowered +946,LocustMPFlying +947,AiurLightBridgeNE8Out +948,AiurLightBridgeNE8 +949,AiurLightBridgeNE10Out +950,AiurLightBridgeNE10 +951,AiurLightBridgeNE12Out +952,AiurLightBridgeNE12 +953,AiurLightBridgeNW8Out +954,AiurLightBridgeNW8 +955,AiurLightBridgeNW10Out +956,AiurLightBridgeNW10 +957,AiurLightBridgeNW12Out +958,AiurLightBridgeNW12 +959,AiurTempleBridgeNE8Out +961,AiurTempleBridgeNE10Out +963,AiurTempleBridgeNE12Out +965,AiurTempleBridgeNW8Out +967,AiurTempleBridgeNW10Out +969,AiurTempleBridgeNW12Out +971,ShakurasLightBridgeNE8Out +972,ShakurasLightBridgeNE8 +973,ShakurasLightBridgeNE10Out +974,ShakurasLightBridgeNE10 +975,ShakurasLightBridgeNE12Out +976,ShakurasLightBridgeNE12 +977,ShakurasLightBridgeNW8Out +978,ShakurasLightBridgeNW8 +979,ShakurasLightBridgeNW10Out +980,ShakurasLightBridgeNW10 +981,ShakurasLightBridgeNW12Out +982,ShakurasLightBridgeNW12 +983,VoidMPImmortalReviveCorpse +984,GuardianCocoonMP +985,GuardianMP +986,DevourerCocoonMP +987,DevourerMP +988,DefilerMPBurrowed +989,DefilerMP +990,OracleStasisTrap +991,DisruptorPhased +992,AiurLightBridgeAbandonedNE8Out +993,AiurLightBridgeAbandonedNE8 +994,AiurLightBridgeAbandonedNE10Out +995,AiurLightBridgeAbandonedNE10 +996,AiurLightBridgeAbandonedNE12Out +997,AiurLightBridgeAbandonedNE12 +998,AiurLightBridgeAbandonedNW8Out +999,AiurLightBridgeAbandonedNW8 +1000,AiurLightBridgeAbandonedNW10Out +1001,AiurLightBridgeAbandonedNW10 +1002,AiurLightBridgeAbandonedNW12Out +1003,AiurLightBridgeAbandonedNW12 +1004,CollapsiblePurifierTowerDebris +1005,PortCity_Bridge_UnitNE8Out +1006,PortCity_Bridge_UnitNE8 +1007,PortCity_Bridge_UnitSE8Out +1008,PortCity_Bridge_UnitSE8 +1009,PortCity_Bridge_UnitNW8Out +1010,PortCity_Bridge_UnitNW8 +1011,PortCity_Bridge_UnitSW8Out +1012,PortCity_Bridge_UnitSW8 +1013,PortCity_Bridge_UnitNE10Out +1014,PortCity_Bridge_UnitNE10 +1015,PortCity_Bridge_UnitSE10Out +1016,PortCity_Bridge_UnitSE10 +1017,PortCity_Bridge_UnitNW10Out +1018,PortCity_Bridge_UnitNW10 +1019,PortCity_Bridge_UnitSW10Out +1020,PortCity_Bridge_UnitSW10 +1021,PortCity_Bridge_UnitNE12Out +1022,PortCity_Bridge_UnitNE12 +1023,PortCity_Bridge_UnitSE12Out +1024,PortCity_Bridge_UnitSE12 +1025,PortCity_Bridge_UnitNW12Out +1026,PortCity_Bridge_UnitNW12 +1027,PortCity_Bridge_UnitSW12Out +1028,PortCity_Bridge_UnitSW12 +1029,PortCity_Bridge_UnitN8Out +1030,PortCity_Bridge_UnitN8 +1031,PortCity_Bridge_UnitS8Out +1032,PortCity_Bridge_UnitS8 +1033,PortCity_Bridge_UnitE8Out +1034,PortCity_Bridge_UnitE8 +1035,PortCity_Bridge_UnitW8Out +1036,PortCity_Bridge_UnitW8 +1037,PortCity_Bridge_UnitN10Out +1038,PortCity_Bridge_UnitN10 +1039,PortCity_Bridge_UnitS10Out +1040,PortCity_Bridge_UnitS10 +1041,PortCity_Bridge_UnitE10Out +1042,PortCity_Bridge_UnitE10 +1043,PortCity_Bridge_UnitW10Out +1044,PortCity_Bridge_UnitW10 +1045,PortCity_Bridge_UnitN12Out +1046,PortCity_Bridge_UnitN12 +1047,PortCity_Bridge_UnitS12Out +1048,PortCity_Bridge_UnitS12 +1049,PortCity_Bridge_UnitE12Out +1050,PortCity_Bridge_UnitE12 +1051,PortCity_Bridge_UnitW12Out +1052,PortCity_Bridge_UnitW12 +1053,PurifierRichMineralField +1054,PurifierRichMineralField750 +1055,CollapsibleRockTowerPushUnitRampLeftGreen +1056,CollapsibleRockTowerPushUnitRampRightGreen +1071,CollapsiblePurifierTowerPushUnit +1073,LocustMPPrecursor +1074,ReleaseInterceptorsBeacon +1075,AdeptPhaseShift +1076,HydraliskImpaleMissile +1077,CycloneMissileLargeAir +1078,CycloneMissile +1079,CycloneMissileLarge +1080,OracleWeapon +1081,TempestWeaponGround +1082,ScoutMPAirWeaponLeft +1083,ScoutMPAirWeaponRight +1084,ArbiterMPWeaponMissile +1085,GuardianMPWeapon +1086,DevourerMPWeaponMissile +1087,DefilerMPDarkSwarmWeapon +1088,QueenMPEnsnareMissile +1089,QueenMPSpawnBroodlingsMissile +1090,LightningBombWeapon +1091,HERCPlacement +1092,GrappleWeapon +1095,CausticSprayMissile +1096,ParasiticBombMissile +1097,ParasiticBombDummy +1098,AdeptWeapon +1099,AdeptUpgradeWeapon +1100,LiberatorMissile +1101,LiberatorDamageMissile +1102,LiberatorAGMissile +1103,KD8Charge +1104,KD8ChargeWeapon +1106,SlaynElementalGrabWeapon +1107,SlaynElementalGrabAirUnit +1108,SlaynElementalGrabGroundUnit +1109,SlaynElementalWeapon +1114,CollapsibleRockTowerRampLeftGreen +1115,CollapsibleRockTowerRampRightGreen +1116,DestructibleExpeditionGate6x6 +1117,DestructibleZergInfestation3x3 +1118,HERC +1119,Moopy +1120,Replicant +1121,SeekerMissile +1122,AiurTempleBridgeDestructibleNE8Out +1123,AiurTempleBridgeDestructibleNE10Out +1124,AiurTempleBridgeDestructibleNE12Out +1125,AiurTempleBridgeDestructibleNW8Out +1126,AiurTempleBridgeDestructibleNW10Out +1127,AiurTempleBridgeDestructibleNW12Out +1128,AiurTempleBridgeDestructibleSW8Out +1129,AiurTempleBridgeDestructibleSW10Out +1130,AiurTempleBridgeDestructibleSW12Out +1131,AiurTempleBridgeDestructibleSE8Out +1132,AiurTempleBridgeDestructibleSE10Out +1133,AiurTempleBridgeDestructibleSE12Out +1135,FlyoverUnit +1136,CorsairMP +1137,ScoutMP +1139,ArbiterMP +1140,ScourgeMP +1141,DefilerMPPlagueWeapon +1142,QueenMP +1143,XelNagaDestructibleRampBlocker6S +1144,XelNagaDestructibleRampBlocker6SE +1145,XelNagaDestructibleRampBlocker6E +1146,XelNagaDestructibleRampBlocker6NE +1147,XelNagaDestructibleRampBlocker6N +1148,XelNagaDestructibleRampBlocker6NW +1149,XelNagaDestructibleRampBlocker6W +1150,XelNagaDestructibleRampBlocker6SW +1151,XelNagaDestructibleRampBlocker8S +1152,XelNagaDestructibleRampBlocker8SE +1153,XelNagaDestructibleRampBlocker8E +1154,XelNagaDestructibleRampBlocker8NE +1155,XelNagaDestructibleRampBlocker8N +1156,XelNagaDestructibleRampBlocker8NW +1157,XelNagaDestructibleRampBlocker8W +1158,XelNagaDestructibleRampBlocker8SW +1159,XelNagaDestructibleBlocker6S +1160,XelNagaDestructibleBlocker6SE +1161,XelNagaDestructibleBlocker6E +1162,XelNagaDestructibleBlocker6NE +1163,XelNagaDestructibleBlocker6N +1164,XelNagaDestructibleBlocker6NW +1165,XelNagaDestructibleBlocker6W +1166,XelNagaDestructibleBlocker6SW +1167,XelNagaDestructibleBlocker8S +1168,XelNagaDestructibleBlocker8SE +1169,XelNagaDestructibleBlocker8E +1170,XelNagaDestructibleBlocker8NE +1171,XelNagaDestructibleBlocker8N +1172,XelNagaDestructibleBlocker8NW +1173,XelNagaDestructibleBlocker8W +1174,XelNagaDestructibleBlocker8SW +1175,ReptileCrate +1176,SlaynSwarmHostSpawnFlyer +1177,SlaynElemental +1178,PurifierVespeneGeyser +1179,ShakurasVespeneGeyser +1180,CollapsiblePurifierTowerDiagonal +1181,CreepOnlyBlocker4x4 +1182,BattleStationMineralField +1183,BattleStationMineralField750 +1184,PurifierMineralField +1185,PurifierMineralField750 +1186,Beacon_Nova +1187,Beacon_NovaSmall +1188,Ursula +1189,Elsecaro_Colonist_Hut +1190,SnowGlazeStarterMP +1191,PylonOvercharged +1192,ObserverSiegeMode +1193,RavenRepairDrone +1195,ParasiticBombRelayDummy +1196,BypassArmorDrone +1197,AdeptPiercingWeapon +1198,HighTemplarWeaponMissile +1199,CycloneMissileLargeAirAlternative +1200,RavenScramblerMissile +1201,RavenRepairDroneReleaseWeapon +1202,RavenShredderMissileWeapon +1203,InfestedAcidSpinesWeapon +1204,InfestorEnsnareAttackMissile +1205,SNARE_PLACEHOLDER +1208,CorrosiveParasiteWeapon diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index cb333d47..a83f4932 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -478,6 +478,7 @@ def load_build(expansion, version): "76114", "77379", "80949", + "89720" ): lotv_builds[version] = load_build("LotV", version) diff --git a/sc2reader/data/ability_lookup.csv b/sc2reader/data/ability_lookup.csv index 831c79ef..4eaee18f 100755 --- a/sc2reader/data/ability_lookup.csv +++ b/sc2reader/data/ability_lookup.csv @@ -518,7 +518,7 @@ LocustMPFlyingSwoopAttack,LocustMPFlyingSwoopAttack MorphToTransportOverlord,MorphToTransportOverlord,Cancel BypassArmor,BypassArmor BypassArmorDroneCU,BypassArmorDroneCU -ChannelSnipe,ChannelSnipe +ChannelSnipe,ChannelSnipe,Cancel LockOnAir,LockOnAir PurificationNovaTargetted,PurificationNovaTargetted SnowRefinery_Terran_ExtendingBridgeNEShort8Out,SnowRefinery_Terran_ExtendingBridgeNEShort8Out @@ -866,3 +866,4 @@ BattlecruiserAttack,BattlecruiserAttack BattlecruiserMove,Move,Patrol,HoldPos AmorphousArmorcloud,AmorphousArmorcloud BatteryOvercharge,BatteryOvercharge +MorphToBaneling,MorphToBaneling,Cancel diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 2f8085d6..de47b7e3 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -871,7 +871,11 @@ def register_default_datapacks(self): ) self.register_datapack( datapacks["LotV"]["80949"], - lambda r: r.expansion == "LotV" and 80949 <= r.build, + lambda r: r.expansion == "LotV" and 80949 <= r.build < 89720, + ) + self.register_datapack( + datapacks["LotV"]["89720"], + lambda r: r.expansion == "LotV" and 89720 <= r.build, ) # Internal Methods From 66522ab40818fcbb8177a5ffa7edf1c19204347a Mon Sep 17 00:00:00 2001 From: Bean Date: Mon, 13 Feb 2023 21:26:08 +0800 Subject: [PATCH 108/136] build 89634 also use datapack 89720 --- sc2reader/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index de47b7e3..e29adefd 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -871,11 +871,11 @@ def register_default_datapacks(self): ) self.register_datapack( datapacks["LotV"]["80949"], - lambda r: r.expansion == "LotV" and 80949 <= r.build < 89720, + lambda r: r.expansion == "LotV" and 80949 <= r.build < 89634, ) self.register_datapack( datapacks["LotV"]["89720"], - lambda r: r.expansion == "LotV" and 89720 <= r.build, + lambda r: r.expansion == "LotV" and 89634 <= r.build, ) # Internal Methods From 61f3a2d1f49ebd2c48163467bdcaca8e1c17b241 Mon Sep 17 00:00:00 2001 From: Bean Date: Tue, 14 Feb 2023 10:44:36 +0800 Subject: [PATCH 109/136] fix for linting issues --- examples/sc2autosave.py | 1 - sc2reader/data/__init__.py | 2 +- sc2reader/engine/plugins/creeptracker.py | 1 + sc2reader/readers.py | 1 - sc2reader/resources.py | 1 - sc2reader/scripts/sc2replayer.py | 1 - sc2reader/scripts/utils.py | 1 - test_replays/test_replays.py | 1 - 8 files changed, 2 insertions(+), 7 deletions(-) diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index b7f1ffbd..dae7bff1 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -184,7 +184,6 @@ def run(args): # We break out of this loop in batch mode and on KeyboardInterrupt while True: - # The file scan uses the arguments and the state to filter down to # only new (since the last sync time) files. for path in scan(args, state): diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index a83f4932..ccd6455c 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -478,7 +478,7 @@ def load_build(expansion, version): "76114", "77379", "80949", - "89720" + "89720", ): lotv_builds[version] = load_build("LotV", version) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 90f01e0c..39bb6b91 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -17,6 +17,7 @@ from collections import defaultdict from itertools import tee + # The creep tracker plugin class CreepTracker: """ diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 92aeefb9..77fb681d 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -2126,7 +2126,6 @@ def trigger_ping_event(self, data): class GameEventsReader_64469(GameEventsReader_38996): - # this function is exactly the same as command_event() from GameEventsReader_38996 # with the only change being that flags now has 26 bits instead of 25. def command_event(self, data): diff --git a/sc2reader/resources.py b/sc2reader/resources.py index e29adefd..924f7bde 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -40,7 +40,6 @@ def __init__(self, file_object, filename=None, factory=None, **options): class Replay(Resource): - #: A nested dictionary of player => { attr_name : attr_value } for #: known attributes. Player 16 represents the global context and #: contains attributes like game speed. diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index c6c16c93..d78410b0 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -94,7 +94,6 @@ def main(): # Allow specification of events to `show` # Loop through the events for event in events: - if ( isinstance(event, CommandEvent) or isinstance(event, SelectionEvent) diff --git a/sc2reader/scripts/utils.py b/sc2reader/scripts/utils.py index cab32a13..1f87baa4 100644 --- a/sc2reader/scripts/utils.py +++ b/sc2reader/scripts/utils.py @@ -35,7 +35,6 @@ def _split_lines(self, text, width): main_indent = len(re.match(r"( *)", text).group(1)) # Wrap each line individually to allow for partial formatting for line in text.splitlines(): - # Get this line's indent and figure out what indent to use # if the line wraps. Account for lists of small variety. indent = len(re.match(r"( *)", line).group(1)) diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 022be926..a63c59fc 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -208,7 +208,6 @@ def test_hots_pids(self): "test_replays/2.0.0.24247/molten.SC2Replay", "test_replays/2.0.0.23925/Akilon Wastes.SC2Replay", ]: - replay = sc2reader.load_replay(replayfilename) self.assertEqual(replay.expansion, "HotS") player_pids = {player.pid for player in replay.players if player.is_human} From cba20f2a4ad55fe44011c4ee5c40b8bdac534c91 Mon Sep 17 00:00:00 2001 From: dlithio Date: Wed, 16 Aug 2023 15:44:31 -0500 Subject: [PATCH 110/136] Identify all control group event types --- sc2reader/events/game.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 50a9f8f0..26fc2813 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -521,17 +521,21 @@ def __str__(self): def create_control_group_event(frame, pid, data): update_type = data["control_group_update"] - if update_type == 0: + if update_type in [0, 4]: + # 0 is the normal set command. + # 4 is used when you steal and set. If required, type 4 will be followed by autogenerated + # selection and control group events that remove the units from their other groups. return SetControlGroupEvent(frame, pid, data) - elif update_type == 1: + elif update_type in [1, 5]: + # 1 is the normal add command. + # 5 is used when you steal and add. If required, type 5 will be followed by autogenerated + # selection and control group events that remove the units from their other groups. return AddToControlGroupEvent(frame, pid, data) elif update_type == 2: return GetControlGroupEvent(frame, pid, data) elif update_type == 3: - # TODO: What could this be?!? - return ControlGroupEvent(frame, pid, data) + return DeleteControlGroupEvent(frame, pid, data) else: - # No idea what this is but we're seeing update_types of 4 and 5 in 3.0 return ControlGroupEvent(frame, pid, data) @@ -589,6 +593,15 @@ class AddToControlGroupEvent(SetControlGroupEvent): """ +class DeleteControlGroupEvent(ControlGroupEvent): + """ + Extends :class:`ControlGroupEvent` + + This event deletes the control group (all units are removed). This happens when all + units are stolen from the event group (alt, alt+shift modifiers by default). + """ + + class GetControlGroupEvent(ControlGroupEvent): """ Extends :class:`ControlGroupEvent` From 97b101e1db62930d342098966ab5eb85e0a05077 Mon Sep 17 00:00:00 2001 From: NumberPigeon Date: Wed, 30 Aug 2023 18:45:15 +0800 Subject: [PATCH 111/136] feat: add region_id attr to Entity --- sc2reader/objects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index d6c9491d..1922ccb8 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -135,6 +135,9 @@ def __init__(self, sid, slot_data): toon_handle = self.toon_handle or "0-S2-0-0" parts = toon_handle.split("-") + #: The Battle.net region id the entity is registered to + self.region_id = int(parts[0]) + #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[int(parts[0])] From ce38298666c080447e5cc530469dda6028d91389 Mon Sep 17 00:00:00 2001 From: NumberPigeon Date: Wed, 30 Aug 2023 22:02:22 +0800 Subject: [PATCH 112/136] feat: add mmr for User class --- sc2reader/objects.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index d6c9491d..54fd3e8e 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -285,6 +285,21 @@ def __init__(self, uid, init_data): #: This is deprecated because it doesn't actually work. self.recorder = None + #: The user's mmr at the time of the game + #: Currently, there are three cases observed for a user that does not have a current mmr: + #: 1. The user has no 'scaled_rating' key in their init_data, + #: 2. The user has a None value for their 'scaled_rating' key, or + #: 3. The user has a negative rating, often -36400. + #: For ease of use, this property will return None in both cases. + if ( + "scaled_rating" in init_data + and init_data["scaled_rating"] is not None + and init_data["scaled_rating"] > 0 + ): + self.mmr = init_data["scaled_rating"] + else: + self.mmr = None + @property def url(self): """ From 77a82cb77f4a2515f6ba0160f7c982146a993d06 Mon Sep 17 00:00:00 2001 From: NumberPigeon <108506296+NumberPigeon@users.noreply.github.com> Date: Wed, 30 Aug 2023 22:54:51 +0800 Subject: [PATCH 113/136] refactor: cleaner code Co-authored-by: Christian Clauss --- sc2reader/objects.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 54fd3e8e..b49f72ab 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -291,14 +291,8 @@ def __init__(self, uid, init_data): #: 2. The user has a None value for their 'scaled_rating' key, or #: 3. The user has a negative rating, often -36400. #: For ease of use, this property will return None in both cases. - if ( - "scaled_rating" in init_data - and init_data["scaled_rating"] is not None - and init_data["scaled_rating"] > 0 - ): - self.mmr = init_data["scaled_rating"] - else: - self.mmr = None + mmr = int(init_data.get("scaled_rating") or 0) + self.mmr = mmr if mmr > 0 else None @property def url(self): From d0685e354c3298e389ef1e25fa0e37aab18c81e3 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 30 Aug 2023 16:26:40 +0200 Subject: [PATCH 114/136] CircleCI: Test on Python 3.11 --- .circleci/config.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c0a5914..96e88466 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,21 +2,22 @@ version: 2.0 build_and_test: &build_and_test_steps - checkout - - run: sudo pip install --upgrade pip - - run: sudo pip install pytest -r requirements.txt - - run: pip install --user . - - run: python --version ; pip --version ; pwd ; ls -l + # Do not use `sudo pip` + # pipx is already installed but `pipx list` is empty + - run: python --version ; pip --version ; pipx --version ; pwd ; ls -l + - run: pip install pytest -r requirements.txt + - run: pip install --editable . - run: pytest jobs: StyleCheck: docker: - - image: circleci/python:3.10 + - image: cimg/python:3.11 steps: - checkout - - run: sudo pip install black codespell flake8 - run: python --version ; pip --version ; pwd ; ls -l + - run: pip install black codespell flake8 ruff - run: codespell -L queenland,uint # stop the build if there are Python syntax errors or undefined names - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics @@ -27,7 +28,7 @@ jobs: Python3: docker: - - image: circleci/python:3.10 + - image: cimg/python:3.11 steps: *build_and_test_steps From 50a137e778786d720b0633c31f5ded9fdc4e9a58 Mon Sep 17 00:00:00 2001 From: NumberPigeon <108506296+NumberPigeon@users.noreply.github.com> Date: Thu, 31 Aug 2023 00:03:27 +0800 Subject: [PATCH 115/136] doc: meaningful tmp var name, while keeping the attribute's name simple Co-authored-by: Christian Clauss --- sc2reader/objects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index b49f72ab..0c5877e1 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -291,8 +291,8 @@ def __init__(self, uid, init_data): #: 2. The user has a None value for their 'scaled_rating' key, or #: 3. The user has a negative rating, often -36400. #: For ease of use, this property will return None in both cases. - mmr = int(init_data.get("scaled_rating") or 0) - self.mmr = mmr if mmr > 0 else None + matchmaking_rating = int(init_data.get("scaled_rating") or 0) + self.mmr = matchmaking_rating if matchmaking_rating > 0 else None @property def url(self): From f9678286db9bb5e06d8dc24cf4582650682776a9 Mon Sep 17 00:00:00 2001 From: NumberPigeon <108506296+NumberPigeon@users.noreply.github.com> Date: Thu, 31 Aug 2023 01:18:12 +0800 Subject: [PATCH 116/136] refactor: reuse region_id Co-authored-by: Christian Clauss --- sc2reader/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 1922ccb8..1488a088 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -139,7 +139,7 @@ def __init__(self, sid, slot_data): self.region_id = int(parts[0]) #: The Battle.net region the entity is registered to - self.region = GATEWAY_LOOKUP[int(parts[0])] + self.region = GATEWAY_LOOKUP[self.region_id] #: The Battle.net subregion the entity is registered to self.subregion = int(parts[2]) From bc73f6550721e1edd52b063a745b7e69ca0d5c3b Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 7 Sep 2023 10:27:21 +0200 Subject: [PATCH 117/136] PEP 621: Migrate from setup.py to pyproject.toml --- pyproject.toml | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 48 --------------------------------- 2 files changed, 72 insertions(+), 48 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..b569cad6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,72 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61.2", +] + +[project] +name = "sc2reader" +description = "Utility for parsing Starcraft II replay files" +keywords = [ + "parser", + "replay", + "sc2", + "starcraft 2", +] +license = {text = "MIT"} +authors = [{name = "Kevin Leung", email = "kkleung89@gmail.com"}] +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Games/Entertainment", + "Topic :: Games/Entertainment :: Real Time Strategy", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", +] +dynamic = [ + "readme", + "version", +] +dependencies = [ + "mpyq", + "pillow", +] +[project.optional-dependencies] +testing = [ + "pytest", +] +[project.urls] +Homepage = "https://github.com/ggtracker/sc2reader" +[project.scripts] +sc2attributes = "sc2reader.scripts.sc2attributes:main" +sc2json = "sc2reader.scripts.sc2json:main" +sc2parse = "sc2reader.scripts.sc2parse:main" +sc2printer = "sc2reader.scripts.sc2printer:main" +sc2replayer = "sc2reader.scripts.sc2replayer:main" + +[tool.setuptools] +include-package-data = true +zip-safe = true +platforms = ["any"] + +[tool.setuptools.dynamic] +readme = {file = ["README.rst", "CHANGELOG.rst"]} +version = {attr = "sc2reader.__version__"} + +[tool.setuptools.packages] +find = {namespaces = false} diff --git a/setup.py b/setup.py deleted file mode 100644 index 95bf19fd..00000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -import setuptools - -setuptools.setup( - license="MIT", - name="sc2reader", - version="1.8.0", - keywords=["starcraft 2", "sc2", "replay", "parser"], - description="Utility for parsing Starcraft II replay files", - long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), - author="Kevin Leung", - author_email="kkleung89@gmail.com", - url="https://github.com/ggtracker/sc2reader", - platforms=["any"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Natural Language :: English", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: PyPy", - "Programming Language :: Python :: Implementation :: CPython", - "Topic :: Games/Entertainment", - "Topic :: Games/Entertainment :: Real Time Strategy", - "Topic :: Software Development", - "Topic :: Software Development :: Libraries", - "Topic :: Utilities", - ], - entry_points={ - "console_scripts": [ - "sc2printer = sc2reader.scripts.sc2printer:main", - "sc2replayer = sc2reader.scripts.sc2replayer:main", - "sc2parse = sc2reader.scripts.sc2parse:main", - "sc2attributes = sc2reader.scripts.sc2attributes:main", - "sc2json = sc2reader.scripts.sc2json:main", - ] - }, - install_requires=["mpyq", "pillow"], - tests_require=["pytest"], - packages=setuptools.find_packages(), - include_package_data=True, - zip_safe=True, -) From 56169d88a059fe7e2aa05eadefacf2f58e1509d4 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 9 Sep 2023 21:00:31 -0400 Subject: [PATCH 118/136] fstrings and fix deprecation of logger.warn --- sc2reader/factories/plugins/replay.py | 4 ++-- sc2reader/log_utils.py | 2 +- sc2reader/objects.py | 4 ++-- sc2reader/resources.py | 4 ++-- sc2reader/utils.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 7f645eb3..05c424b4 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -212,8 +212,8 @@ def SelectionTracker(replay): if error: person.selection_errors += 1 if debug: - logger.warn( - "Error detected in deselection mode {}.".format(event.mask_type) + logger.warning( + f"Error detected in deselection mode {event.mask_type}." ) person.selection = player_selections diff --git a/sc2reader/log_utils.py b/sc2reader/log_utils.py index 3337e348..dc20fe92 100644 --- a/sc2reader/log_utils.py +++ b/sc2reader/log_utils.py @@ -33,7 +33,7 @@ def createLock(self): LEVEL_MAP = dict( DEBUG=logging.DEBUG, INFO=logging.INFO, - WARN=logging.WARN, + WARN=logging.WARNING, ERROR=logging.ERROR, CRITICAL=logging.CRITICAL, ) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 9a7a0e8b..de82cb04 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -562,7 +562,7 @@ def __init__(self, contents): data = ByteDecoder(contents, endian="LITTLE") magic = data.read_string(4) if magic != "MapI": - self.logger.warn(f"Invalid MapInfo file: {magic}") + self.logger.warning(f"Invalid MapInfo file: {magic}") return #: The map info file format version @@ -773,7 +773,7 @@ def __init__(self, contents): self.enemy_flags = data.read_uint(int(math.ceil(self.enemy_flags_length / 8.0))) if data.length != data.tell(): - self.logger.warn("Not all of the MapInfo file was read!") + self.logger.warning("Not all of the MapInfo file was read!") def __str__(self): return self.map_name diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 924f7bde..3e495c30 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -552,7 +552,7 @@ def get_team(team_id): if team.result == "Win": self.winner = team else: - self.logger.warn( + self.logger.warning( f"Conflicting results for Team {team.number}: {results}" ) team.result = "Unknown" @@ -1336,7 +1336,7 @@ def load_player_stats(self): ) ) elif stat_id != 83886080: # We know this one is always bad. - self.logger.warn(f"Untranslatable key = {stat_id}") + self.logger.warning(f"Untranslatable key = {stat_id}") # Once we've compiled all the build commands we need to make # sure they are properly sorted for presentation. diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 514ca807..25678955 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -65,7 +65,7 @@ class Color: def __init__(self, name=None, r=0, g=0, b=0, a=255): if name: if name not in COLOR_CODES_INV: - self.logger.warn("Invalid color name: " + name) + self.logger.warning(f"Invalid color name: {name}") hexstr = COLOR_CODES_INV.get(name, "000000") self.r = int(hexstr[0:2], 16) self.g = int(hexstr[2:4], 16) @@ -78,7 +78,7 @@ def __init__(self, name=None, r=0, g=0, b=0, a=255): self.b = b self.a = a if self.hex not in COLOR_CODES: - self.logger.warn("Invalid color hex value: " + self.hex) + self.logger.warning(f"Invalid color hex value: {self.hex}") self.name = COLOR_CODES.get(self.hex, self.hex) @property From dd282b7e96ccfa5ca9d018738c1fe0778d836241 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 10 Sep 2023 07:14:51 +0200 Subject: [PATCH 119/136] Lint Python code with ruff instead of flake8 --- .circleci/config.yml | 7 ++----- STYLE_GUIDE.rst | 7 +++---- docs/source/conf.py | 4 +++- examples/sc2autosave.py | 3 ++- ruff.toml | 8 ++++++++ sc2reader/engine/plugins/context.py | 2 +- sc2reader/engine/plugins/selection.py | 4 +++- sc2reader/factories/plugins/replay.py | 1 - sc2reader/factories/sc2factory.py | 6 +++--- sc2reader/resources.py | 6 +++--- sc2reader/scripts/sc2replayer.py | 3 ++- sc2reader/utils.py | 12 +++++++----- 12 files changed, 37 insertions(+), 26 deletions(-) create mode 100644 ruff.toml diff --git a/.circleci/config.yml b/.circleci/config.yml index 96e88466..8f73efb1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,12 +17,9 @@ jobs: steps: - checkout - run: python --version ; pip --version ; pwd ; ls -l - - run: pip install black codespell flake8 ruff + - run: pip install black codespell ruff - run: codespell -L queenland,uint - # stop the build if there are Python syntax errors or undefined names - - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - run: ruff . - run: black . --check diff --git a/STYLE_GUIDE.rst b/STYLE_GUIDE.rst index 14f91df5..a25c04ff 100644 --- a/STYLE_GUIDE.rst +++ b/STYLE_GUIDE.rst @@ -1,12 +1,11 @@ STYLE GUIDE ============== -As a rough style guide, please lint your code with black, codespell, and flake8:: +As a rough style guide, please lint your code with black, codespell, and ruff:: - pip install black codespell flake8 + pip install black codespell ruff codespell -L queenland,uint - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + ruff . black . --check More up-to-date checks may be detailed in `.circleci/config.yml`. diff --git a/docs/source/conf.py b/docs/source/conf.py index ead7fdd3..4fc4f46e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,7 +10,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import os +import sys + import sc2reader autodoc_member_order = "bysource" diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index dae7bff1..59c573f0 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -192,8 +192,9 @@ def run(args): replay = sc2reader.load_replay(path, load_level=2) except KeyboardInterrupt: raise - except: + except Exception as e: # Failure to parse + args.log.write(f"{e!r}") file_name = os.path.basename(path) directory = make_directory(args, ("parse_error",)) new_path = os.path.join(directory, file_name) diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..899d7e69 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,8 @@ +ignore = [ + "E402", # Module level import not at top of file + "F401", # module imported but unused; consider using `importlib.util.find_spec` to test for availability + "F403", # Run `removestar` on this codebase + "F405", # Run `removestar` on this codebase + "F841", # Run `ruff --select=F841 --fix .` +] +line-length=129 diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 4adfc0e1..6b0d05dc 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -263,7 +263,7 @@ def handleUnitTypeChangeEvent(self, event, replay): replay.datapack.change_type(event.unit, event.unit_type_name, event.frame) else: self.logger.error( - "Unit {} type changed at {} [{}] before it was born!".format( + "Unit {} type changed at {} before it was born!".format( event.unit_id, Length(seconds=event.second) ) ) diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 006ac78e..4dcc80c2 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -76,7 +76,9 @@ def _deselect(self, selection, mode, data): if mode == "Mask": # Deselect objects according to deselect mask - sfilter = lambda bit_u: not bit_u[0] + def sfilter(bit_u): + return not bit_u[0] + mask = data + [False] * (selection_size - data_size) new_selection = [u for (bit, u) in filter(sfilter, zip(mask, selection))] error = data_size > selection_size diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 7f645eb3..16e31ed0 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -85,7 +85,6 @@ def toDict(replay): "is_ladder": getattr(replay, "is_ladder", False), "is_private": getattr(replay, "is_private", False), "filename": getattr(replay, "filename", None), - "file_time": getattr(replay, "file_time", None), "frames": getattr(replay, "frames", None), "build": getattr(replay, "build", None), "release": getattr(replay, "release_string", None), diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index c020459f..c5298eb3 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -266,13 +266,13 @@ def load_remote_resource_contents(self, remote_resource, **options): return resource def cache_has(self, cache_key): - raise NotImplemented() + raise NotImplementedError() def cache_get(self, cache_key): - raise NotImplemented() + raise NotImplementedError() def cache_set(self, cache_key, value): - raise NotImplemented() + raise NotImplementedError() class FileCachedSC2Factory(CachedSC2Factory): diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 924f7bde..e74a9d7c 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1491,8 +1491,8 @@ def __init__(self, header_file, filename=None, **options): # Parse localization hashes l18n_struct = self.data[0][4][8] - for l in l18n_struct: - parsed_hash = utils.parse_hash(l[1][0]) - self.localization_urls[l[0]] = utils.get_resource_url( + for h in l18n_struct: + parsed_hash = utils.parse_hash(h[1][0]) + self.localization_urls[h[0]] = utils.get_resource_url( parsed_hash["server"], parsed_hash["hash"], parsed_hash["type"] ) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index d78410b0..c569c5ad 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -32,7 +32,8 @@ def getch(): from msvcrt import getch except ImportError as e: # We can't make getch happen, just dump events to the screen - getch = lambda: True + def getch(): + return True import argparse diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 514ca807..9dcb5735 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -163,11 +163,14 @@ def get_files( # If an extension is supplied, use it to do a type check if extension: - type_check = ( - lambda path: os.path.splitext(path)[1][1:].lower() == extension.lower() - ) + + def type_check(path): + return os.path.splitext(path)[1][1:].lower() == extension.lower() + else: - type_check = lambda n: True + + def type_check(n): + return True # os.walk can't handle file paths, only directories if os.path.isfile(path): @@ -315,7 +318,6 @@ def toDict(replay): "is_ladder": getattr(replay, "is_ladder", False), "is_private": getattr(replay, "is_private", False), "filename": getattr(replay, "filename", None), - "file_time": getattr(replay, "file_time", None), "frames": getattr(replay, "frames", None), "build": getattr(replay, "build", None), "release": getattr(replay, "release_string", None), From 01b1cac6597bdab4f8f98e3afab4a888fc6f1920 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 10 Sep 2023 07:37:32 +0200 Subject: [PATCH 120/136] Fix E402 Module level import not at top of file --- examples/sc2store.py | 4 ++-- ruff.toml | 1 - sc2reader/constants.py | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/sc2store.py b/examples/sc2store.py index 6aff2a98..77d0e71b 100755 --- a/examples/sc2store.py +++ b/examples/sc2store.py @@ -11,8 +11,6 @@ from pprint import PrettyPrinter -pprint = PrettyPrinter(indent=2).pprint - from sqlalchemy import create_engine from sqlalchemy import Column, ForeignKey, distinct, Table from sqlalchemy import Integer, String, Sequence, DateTime @@ -23,6 +21,8 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.associationproxy import association_proxy +pprint = PrettyPrinter(indent=2).pprint + Base = declarative_base() party_member = Table( diff --git a/ruff.toml b/ruff.toml index 899d7e69..57d86cdc 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,4 @@ ignore = [ - "E402", # Module level import not at top of file "F401", # module imported but unused; consider using `importlib.util.find_spec` to test for availability "F403", # Run `removestar` on this codebase "F405", # Run `removestar` on this codebase diff --git a/sc2reader/constants.py b/sc2reader/constants.py index db34afa7..a1ae473f 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -1,3 +1,6 @@ +import json +import pkgutil + # These are found in Repack-MPQ/fileset.{locale}#Mods#Core.SC2Mod#{locale}.SC2Data/LocalizedData/Editor/EditorCategoryStrings.txt # EDSTR_CATEGORY_Race # EDSTR_PLAYERPROPS_RACE @@ -101,9 +104,6 @@ } -import json -import pkgutil - attributes_json = pkgutil.get_data("sc2reader.data", "attributes.json").decode("utf8") attributes_dict = json.loads(attributes_json) LOBBY_PROPERTIES = dict() From 9b592cd85592ca4919a0817c4fcc18af35ea522d Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 10 Sep 2023 07:46:01 +0200 Subject: [PATCH 121/136] removestar --in-place . --- sc2reader/engine/engine.py | 3 ++- sc2reader/objects.py | 2 +- sc2reader/readers.py | 15 +++++++++++---- sc2reader/scripts/sc2replayer.py | 3 ++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index 9c7ee68f..1cd131da 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -1,5 +1,6 @@ import collections -from sc2reader.events import * +from sc2reader.events import (CommandEvent, ControlGroupEvent, Event, GameEvent, MessageEvent, + TrackerEvent) from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 9a7a0e8b..0b90e140 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -4,7 +4,7 @@ from sc2reader import utils, log_utils from sc2reader.decoders import ByteDecoder -from sc2reader.constants import * +from sc2reader.constants import GATEWAY_LOOKUP, LOBBY_PROPERTIES, LOCALIZED_RACES Location = namedtuple("Location", ["x", "y"]) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 77fb681d..360100db 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1,10 +1,17 @@ import struct from sc2reader.exceptions import ParseError, ReadError -from sc2reader.objects import * -from sc2reader.events.game import * -from sc2reader.events.message import * -from sc2reader.events.tracker import * +from sc2reader.objects import Attribute +from sc2reader.events.game import (CameraEvent, CommandManagerStateEvent, HijackReplayGameEvent, + PlayerLeaveEvent, ResourceTradeEvent, SelectionEvent, + UpdateTargetPointCommandEvent, UpdateTargetUnitCommandEvent, + UserOptionsEvent, create_command_event, + create_control_group_event) +from sc2reader.events.message import ChatEvent, PingEvent, ProgressEvent +from sc2reader.events.tracker import (PlayerSetupEvent, PlayerStatsEvent, UnitBornEvent, + UnitDiedEvent, UnitDoneEvent, UnitInitEvent, + UnitOwnerChangeEvent, UnitPositionsEvent, UnitTypeChangeEvent, + UpgradeCompleteEvent) from sc2reader.utils import DepotFile from sc2reader.decoders import BitPackedDecoder, ByteDecoder diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index d78410b0..d1f3de44 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -37,7 +37,8 @@ def getch(): import argparse import sc2reader -from sc2reader.events import * +from sc2reader.events import (CameraEvent, CommandEvent, GameStartEvent, PlayerLeaveEvent, + SelectionEvent) def main(): From 34bc4c67f9f407bfa9e39f37e9f5ef02ca0dcfe1 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 10 Sep 2023 08:07:19 +0200 Subject: [PATCH 122/136] Fix undefined name: HotkeyEvent --> ControlGroupEvent --- sc2reader/scripts/sc2replayer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index d1f3de44..3e5a3d1c 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -37,8 +37,8 @@ def getch(): import argparse import sc2reader -from sc2reader.events import (CameraEvent, CommandEvent, GameStartEvent, PlayerLeaveEvent, - SelectionEvent) +from sc2reader.events import (CameraEvent, CommandEvent, ControlGroupEvent, + GameStartEvent, PlayerLeaveEvent, SelectionEvent) def main(): @@ -100,7 +100,7 @@ def main(): or isinstance(event, SelectionEvent) or isinstance(event, PlayerLeaveEvent) or isinstance(event, GameStartEvent) - or (args.hotkeys and isinstance(event, HotkeyEvent)) + or (args.hotkeys and isinstance(event, ControlGroupEvent)) or (args.cameras and isinstance(event, CameraEvent)) ): print(event) From 477a898e43ab4cada4f729e538950261d3739aaa Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 10 Sep 2023 14:53:37 +0200 Subject: [PATCH 123/136] psf/black --- sc2reader/engine/engine.py | 10 ++++++++-- sc2reader/readers.py | 34 +++++++++++++++++++++++--------- sc2reader/scripts/sc2replayer.py | 10 ++++++++-- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index 1cd131da..96b7f759 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -1,6 +1,12 @@ import collections -from sc2reader.events import (CommandEvent, ControlGroupEvent, Event, GameEvent, MessageEvent, - TrackerEvent) +from sc2reader.events import ( + CommandEvent, + ControlGroupEvent, + Event, + GameEvent, + MessageEvent, + TrackerEvent, +) from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 360100db..f65ee0ed 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -2,16 +2,32 @@ from sc2reader.exceptions import ParseError, ReadError from sc2reader.objects import Attribute -from sc2reader.events.game import (CameraEvent, CommandManagerStateEvent, HijackReplayGameEvent, - PlayerLeaveEvent, ResourceTradeEvent, SelectionEvent, - UpdateTargetPointCommandEvent, UpdateTargetUnitCommandEvent, - UserOptionsEvent, create_command_event, - create_control_group_event) +from sc2reader.events.game import ( + CameraEvent, + CommandManagerStateEvent, + HijackReplayGameEvent, + PlayerLeaveEvent, + ResourceTradeEvent, + SelectionEvent, + UpdateTargetPointCommandEvent, + UpdateTargetUnitCommandEvent, + UserOptionsEvent, + create_command_event, + create_control_group_event, +) from sc2reader.events.message import ChatEvent, PingEvent, ProgressEvent -from sc2reader.events.tracker import (PlayerSetupEvent, PlayerStatsEvent, UnitBornEvent, - UnitDiedEvent, UnitDoneEvent, UnitInitEvent, - UnitOwnerChangeEvent, UnitPositionsEvent, UnitTypeChangeEvent, - UpgradeCompleteEvent) +from sc2reader.events.tracker import ( + PlayerSetupEvent, + PlayerStatsEvent, + UnitBornEvent, + UnitDiedEvent, + UnitDoneEvent, + UnitInitEvent, + UnitOwnerChangeEvent, + UnitPositionsEvent, + UnitTypeChangeEvent, + UpgradeCompleteEvent, +) from sc2reader.utils import DepotFile from sc2reader.decoders import BitPackedDecoder, ByteDecoder diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index 3e5a3d1c..06f97acd 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -37,8 +37,14 @@ def getch(): import argparse import sc2reader -from sc2reader.events import (CameraEvent, CommandEvent, ControlGroupEvent, - GameStartEvent, PlayerLeaveEvent, SelectionEvent) +from sc2reader.events import ( + CameraEvent, + CommandEvent, + ControlGroupEvent, + GameStartEvent, + PlayerLeaveEvent, + SelectionEvent, +) def main(): From 3ceb5a20df5daa97f00b6890d86c8ae29c10cd5d Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Fri, 22 Sep 2023 22:00:30 -0700 Subject: [PATCH 124/136] Update CONTRIBUTING.md Minor updates for current tooling --- CONTRIBUTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53017ede..6858ab95 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ Support ========= -For real-time support, please visit #sc2reader on the FreeNode.net IRC. For all other support please use the sc2reader@googlegroups.com mailing list or open an issue in the github tracker. +As of Sept 2023, the best way to get support on sc2reader is on the GitHub project page. Preferably, it would be a new discussion https://github.com/ggtracker/sc2reader/discussions but can also be submitted as n issue Issues ========= @@ -14,7 +14,7 @@ If you can't share your code/replays publicly try to replicate with a smaller sc Patches ========= -Please submit patches by pull request where possible. Patches should add a test to confirm their fix and should not break previously working tests. Circle CI automatically runs tests on each pull request so please check https://circleci.com/gh/ggtracker/sc2reader to see the results of those tests. +Please submit patches by pull request where possible. Patches should add a test to confirm their fix and should not break previously working tests. Circle CI automatically runs tests on each pull request so please check https://circleci.com/gh/ggtracker/sc2reader to see the results of those tests. If you are having trouble running/add/fixing tests for your patch let me know and I'll see if I can help. @@ -22,7 +22,7 @@ If you are having trouble running/add/fixing tests for your patch let me know an Coding Style ============== -We'd like our code to follow PEP8 coding style in this project. +We would like our code to follow [Ruff](https://docs.astral.sh/ruff/) coding style in this project. We use [python/black](https://github.com/python/black) in order to make our lives easier. We propose you do the same within this project, otherwise you might be asked to reformat your pull requests. From e5435e6add32de8e77eeed295f68e6dd868cd677 Mon Sep 17 00:00:00 2001 From: manuelseeger Date: Thu, 28 Dec 2023 13:50:36 +0100 Subject: [PATCH 125/136] Fix division by zero on instant leave replays --- sc2reader/factories/plugins/replay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 676f0117..ff5986eb 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -126,7 +126,7 @@ def APMTracker(replay): elif event.name == "PlayerLeaveEvent": player.seconds_played = event.second - if len(player.apm) > 0: + if len(player.apm) > 0 and player.seconds_played > 0: player.avg_apm = ( sum(player.aps.values()) / float(player.seconds_played) * 60 ) From d832145f3958cd068e599872f9faa07505c5bf0f Mon Sep 17 00:00:00 2001 From: Andrene Date: Wed, 29 May 2024 19:18:39 -0400 Subject: [PATCH 126/136] add type for dialog control events --- sc2reader/events/game.py | 17 +++++++++++++++++ sc2reader/readers.py | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 26fc2813..61889117 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -765,3 +765,20 @@ def __init__(self, frame, pid, data): #: Information on the users hijacking the game self.user_infos = data["user_infos"] + +@loggable +class DialogControlEvent(GameEvent): + """ + Generated when a dialog is interacted with. + """ + def __init__(self, frame, pid, data): + super().__init__(frame, pid) + + #: Identifier for the dialog + self.control_id = data["control_id"] + + #: How dialog was interacted with + self.event_type = self.data["event_type"] + + #: Data specific to event type such as changes or clicks + self.event_data = self.data["event_data"] \ No newline at end of file diff --git a/sc2reader/readers.py b/sc2reader/readers.py index f65ee0ed..9403e133 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -12,6 +12,7 @@ UpdateTargetPointCommandEvent, UpdateTargetUnitCommandEvent, UserOptionsEvent, + DialogControlEvent, create_command_event, create_control_group_event, ) @@ -462,7 +463,7 @@ def __init__(self): 52: (None, self.trigger_purchase_exit_event), 53: (None, self.trigger_planet_mission_launched_event), 54: (None, self.trigger_planet_panel_canceled_event), - 55: (None, self.trigger_dialog_control_event), + 55: (DialogControlEvent, self.trigger_dialog_control_event), 56: (None, self.trigger_sound_length_sync_event), 57: (None, self.trigger_conversation_skipped_event), 58: (None, self.trigger_mouse_clicked_event), From 8e799a79e2ab4c9b8dd4d91b0ef690a8a61760b4 Mon Sep 17 00:00:00 2001 From: Andrene Date: Wed, 29 May 2024 19:26:36 -0400 Subject: [PATCH 127/136] not self --- sc2reader/events/game.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 61889117..38555bfd 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -778,7 +778,7 @@ def __init__(self, frame, pid, data): self.control_id = data["control_id"] #: How dialog was interacted with - self.event_type = self.data["event_type"] + self.event_type = data["event_type"] #: Data specific to event type such as changes or clicks - self.event_data = self.data["event_data"] \ No newline at end of file + self.event_data = data["event_data"] \ No newline at end of file From 2333c32f69cecd5c44770588aaf6ecd59794919f Mon Sep 17 00:00:00 2001 From: Andrene Date: Wed, 29 May 2024 19:45:04 -0400 Subject: [PATCH 128/136] style fixes --- examples/sc2autosave.py | 16 ++++++++-------- sc2reader/engine/plugins/creeptracker.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index 59c573f0..3d826680 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -77,16 +77,16 @@ keeps the script from looking into the 'Saved' subdirectory. sc2autosave \ - --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ - --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer/Saved \ --period 10 \ --depth 0 This next configuration runs in batch mode using the default renaming format. sc2autosave \ - --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ - --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer/Saved \ --rename (ZvP) Lost Temple: ShadesofGray(Z) vs Trisfall(P).SC2Replay @@ -96,8 +96,8 @@ by replay format and favors ShadesofGray in the player and team orderings. sc2autosave \ - --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ - --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer/Saved \ --rename "{:format}/{:matchup} on {:map}: {:teams}" \ --player-format "{:name}({:play_race})" \ --team-order-by number \ @@ -112,8 +112,8 @@ length to show both minutes and seconds. sc2autosave \ - --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Mutliplayer \ - --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplater/Saved \ + --source ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer \ + --dest ~/My\\ Documents/Starcraft\\ II/Accounts/.../Multiplayer/Saved \ --rename "{:matchup}/({:length}) {:map}: {:teams}" \ --player-format "{:name}({:play_race})" \ --team-order-by number \ diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 39bb6b91..2916bdba 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -105,7 +105,7 @@ class creep_tracker: def __init__(self, replay): # if the debug option is selected, minimaps will be printed to a file ##and a stringIO containing the minimap image will be saved for - ##every minite in the game and the minimap with creep highlighted + ##every minute in the game and the minimap with creep highlighted ## will be printed out. self.debug = replay.opt["debug"] ##This list contains creep spread area for each player From 38a52f1a1adc69c771110ee5a6851e341d157a97 Mon Sep 17 00:00:00 2001 From: Andrene Date: Thu, 30 May 2024 08:37:11 -0400 Subject: [PATCH 129/136] ignore assertin --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f73efb1..e89a8abe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,7 +18,7 @@ jobs: - checkout - run: python --version ; pip --version ; pwd ; ls -l - run: pip install black codespell ruff - - run: codespell -L queenland,uint + - run: codespell -L queenland,uint,assertin - run: ruff . - run: black . --check From 702f78118306367e25201e08b2726236ec185e7d Mon Sep 17 00:00:00 2001 From: Andrene Date: Thu, 30 May 2024 14:41:22 -0400 Subject: [PATCH 130/136] style cleaning --- sc2reader/engine/plugins/creeptracker.py | 6 +- sc2reader/events/game.py | 4 +- sc2reader/readers.py | 799 +++++++++++++---------- test_replays/test_replays.py | 4 +- 4 files changed, 471 insertions(+), 342 deletions(-) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 2916bdba..d947c42a 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -123,9 +123,9 @@ def __init__(self, replay): self.unit_name_to_radius = {"CreepTumor": 10, "Hatchery": 8, "NydusCanal": 5} self.radius_to_coordinates = dict() for x in self.unit_name_to_radius: - self.radius_to_coordinates[ - self.unit_name_to_radius[x] - ] = self.radius_to_map_positions(self.unit_name_to_radius[x]) + self.radius_to_coordinates[self.unit_name_to_radius[x]] = ( + self.radius_to_map_positions(self.unit_name_to_radius[x]) + ) # Get map information replayMap = replay.map # extract image from replay package diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 38555bfd..008f6ef9 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -766,11 +766,13 @@ def __init__(self, frame, pid, data): #: Information on the users hijacking the game self.user_infos = data["user_infos"] + @loggable class DialogControlEvent(GameEvent): """ Generated when a dialog is interacted with. """ + def __init__(self, frame, pid, data): super().__init__(frame, pid) @@ -781,4 +783,4 @@ def __init__(self, frame, pid, data): self.event_type = data["event_type"] #: Data specific to event type such as changes or clicks - self.event_data = data["event_data"] \ No newline at end of file + self.event_data = data["event_data"] diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 9403e133..0dbacce4 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -40,48 +40,68 @@ def __call__(self, data, replay): user_initial_data=[ dict( name=data.read_aligned_string(data.read_uint8()), - clan_tag=data.read_aligned_string(data.read_uint8()) - if replay.base_build >= 24764 and data.read_bool() - else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) - if replay.base_build >= 27950 and data.read_bool() - else None, - highest_league=data.read_uint8() - if replay.base_build >= 24764 and data.read_bool() - else None, - combined_race_levels=data.read_uint32() - if replay.base_build >= 24764 and data.read_bool() - else None, + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if replay.base_build >= 24764 and data.read_bool() + else None + ), + clan_logo=( + DepotFile(data.read_aligned_bytes(40)) + if replay.base_build >= 27950 and data.read_bool() + else None + ), + highest_league=( + data.read_uint8() + if replay.base_build >= 24764 and data.read_bool() + else None + ), + combined_race_levels=( + data.read_uint32() + if replay.base_build >= 24764 and data.read_bool() + else None + ), random_seed=data.read_uint32(), race_preference=data.read_uint8() if data.read_bool() else None, - team_preference=data.read_uint8() - if replay.base_build >= 16561 and data.read_bool() - else None, + team_preference=( + data.read_uint8() + if replay.base_build >= 16561 and data.read_bool() + else None + ), test_map=data.read_bool(), test_auto=data.read_bool(), examine=data.read_bool() if replay.base_build >= 21955 else None, - custom_interface=data.read_bool() - if replay.base_build >= 24764 - else None, - test_type=data.read_uint32() - if replay.base_build >= 34784 - else None, + custom_interface=( + data.read_bool() if replay.base_build >= 24764 else None + ), + test_type=( + data.read_uint32() if replay.base_build >= 34784 else None + ), observe=data.read_bits(2), - hero=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - skin=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - mount=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - toon_handle=data.read_aligned_string(data.read_bits(7)) - if replay.base_build >= 34784 - else None, - scaled_rating=data.read_uint32() - 2147483648 - if replay.base_build >= 54518 and data.read_bool() - else None, + hero=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + skin=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + mount=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if replay.base_build >= 34784 + else None + ), + scaled_rating=( + data.read_uint32() - 2147483648 + if replay.base_build >= 54518 and data.read_bool() + else None + ), ) for i in range(data.read_bits(5)) ], @@ -95,27 +115,29 @@ def __call__(self, data, replay): random_races=data.read_bool(), battle_net=data.read_bool(), amm=data.read_bool(), - ranked=data.read_bool() - if replay.base_build >= 34784 and replay.base_build < 38215 - else None, + ranked=( + data.read_bool() + if replay.base_build >= 34784 and replay.base_build < 38215 + else None + ), competitive=data.read_bool(), practice=data.read_bool() if replay.base_build >= 34784 else None, - cooperative=data.read_bool() - if replay.base_build >= 34784 - else None, + cooperative=( + data.read_bool() if replay.base_build >= 34784 else None + ), no_victory_or_defeat=data.read_bool(), - hero_duplicates_allowed=data.read_bool() - if replay.base_build >= 34784 - else None, + hero_duplicates_allowed=( + data.read_bool() if replay.base_build >= 34784 else None + ), fog=data.read_bits(2), observers=data.read_bits(2), user_difficulty=data.read_bits(2), - client_debug_flags=data.read_uint64() - if replay.base_build >= 22612 - else None, - build_coach_enabled=data.read_bool() - if replay.base_build >= 59587 - else None, + client_debug_flags=( + data.read_uint64() if replay.base_build >= 22612 else None + ), + build_coach_enabled=( + data.read_bool() if replay.base_build >= 59587 else None + ), ), game_speed=data.read_bits(3), game_type=data.read_bits(3), @@ -123,9 +145,11 @@ def __call__(self, data, replay): max_observers=data.read_bits(5), max_players=data.read_bits(5), max_teams=data.read_bits(4) + 1, - max_colors=data.read_bits(6) - if replay.base_build >= 17266 - else data.read_bits(5) + 1, + max_colors=( + data.read_bits(6) + if replay.base_build >= 17266 + else data.read_bits(5) + 1 + ), max_races=data.read_uint8() + 1, max_controls=data.read_uint8() + (0 if replay.base_build >= 26490 else 1), @@ -142,36 +166,40 @@ def __call__(self, data, replay): allowedDifficulty=data.read_bits(data.read_bits(6)), allowedControls=data.read_bits(data.read_uint8()), allowed_observe_types=data.read_bits(data.read_bits(2)), - allowed_ai_builds=data.read_bits( - data.read_bits(8 if replay.base_build >= 38749 else 7) - ) - if replay.base_build >= 23925 - else None, + allowed_ai_builds=( + data.read_bits( + data.read_bits(8 if replay.base_build >= 38749 else 7) + ) + if replay.base_build >= 23925 + else None + ), ) for i in range(data.read_bits(5)) ], default_difficulty=data.read_bits(6), - default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) - if replay.base_build >= 23925 - else None, + default_ai_build=( + data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 23925 + else None + ), cache_handles=[ DepotFile(data.read_aligned_bytes(40)) for i in range( data.read_bits(6 if replay.base_build >= 21955 else 4) ) ], - has_extension_mod=data.read_bool() - if replay.base_build >= 27950 - else None, - has_nonBlizzardExtensionMod=data.read_bool() - if replay.base_build >= 42932 - else None, + has_extension_mod=( + data.read_bool() if replay.base_build >= 27950 else None + ), + has_nonBlizzardExtensionMod=( + data.read_bool() if replay.base_build >= 42932 else None + ), is_blizzardMap=data.read_bool(), is_premade_ffa=data.read_bool(), is_coop_mode=data.read_bool() if replay.base_build >= 23925 else None, - is_realtime_mode=data.read_bool() - if replay.base_build >= 54518 - else None, + is_realtime_mode=( + data.read_bool() if replay.base_build >= 54518 else None + ), ), lobby_state=dict( phase=data.read_bits(3), @@ -185,131 +213,158 @@ def __call__(self, data, replay): colorPref=data.read_bits(5) if data.read_bool() else None, race_pref=data.read_uint8() if data.read_bool() else None, difficulty=data.read_bits(6), - ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) - if replay.base_build >= 23925 - else None, + ai_build=( + data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 23925 + else None + ), handicap=data.read_bits( 32 if replay.base_build >= 80669 else 7 ), observe=data.read_bits(2), - logo_index=data.read_uint32() - if replay.base_build >= 32283 - else None, - hero=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - skin=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - mount=data.read_aligned_string(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - artifacts=[ - dict( - type_struct=data.read_aligned_string(data.read_bits(9)) - ) - for i in range(data.read_bits(4)) - ] - if replay.base_build >= 34784 - else None, - working_set_slot_id=data.read_uint8() - if replay.base_build >= 24764 and data.read_bool() - else None, + logo_index=( + data.read_uint32() if replay.base_build >= 32283 else None + ), + hero=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + skin=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + mount=( + data.read_aligned_string(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + artifacts=( + [ + dict( + type_struct=data.read_aligned_string( + data.read_bits(9) + ) + ) + for i in range(data.read_bits(4)) + ] + if replay.base_build >= 34784 + else None + ), + working_set_slot_id=( + data.read_uint8() + if replay.base_build >= 24764 and data.read_bool() + else None + ), rewards=[ data.read_uint32() for i in range( data.read_bits( 17 if replay.base_build >= 34784 - else 6 - if replay.base_build >= 24764 - else 5 + else 6 if replay.base_build >= 24764 else 5 ) ) ], - toon_handle=data.read_aligned_string(data.read_bits(7)) - if replay.base_build >= 17266 - else None, - licenses=[ - data.read_uint32() - for i in range( - data.read_bits( - 16 - if replay.base_build >= 77379 - else 13 - if replay.base_build >= 70154 - else 9 + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if replay.base_build >= 17266 + else None + ), + licenses=( + [ + data.read_uint32() + for i in range( + data.read_bits( + 16 + if replay.base_build >= 77379 + else 13 if replay.base_build >= 70154 else 9 + ) ) - ) - ] - if replay.base_build >= 19132 - else [], - tandem_leader_user_id=data.read_bits(4) - if replay.base_build >= 34784 and data.read_bool() - else None, - commander=data.read_aligned_bytes(data.read_bits(9)) - if replay.base_build >= 34784 - else None, - commander_level=data.read_uint32() - if replay.base_build >= 36442 - else None, - has_silence_penalty=data.read_bool() - if replay.base_build >= 38215 - else None, - tandem_id=data.read_bits(4) - if replay.base_build >= 39576 and data.read_bool() - else None, - commander_mastery_level=data.read_uint32() - if replay.base_build >= 42932 - else None, - commander_mastery_talents=[ - data.read_uint32() for i in range(data.read_bits(3)) - ] - if replay.base_build >= 42932 - else None, - trophy_id=data.read_uint32() - if replay.base_build >= 75689 - else None, - reward_overrides=[ + ] + if replay.base_build >= 19132 + else [] + ), + tandem_leader_user_id=( + data.read_bits(4) + if replay.base_build >= 34784 and data.read_bool() + else None + ), + commander=( + data.read_aligned_bytes(data.read_bits(9)) + if replay.base_build >= 34784 + else None + ), + commander_level=( + data.read_uint32() if replay.base_build >= 36442 else None + ), + has_silence_penalty=( + data.read_bool() if replay.base_build >= 38215 else None + ), + tandem_id=( + data.read_bits(4) + if replay.base_build >= 39576 and data.read_bool() + else None + ), + commander_mastery_level=( + data.read_uint32() if replay.base_build >= 42932 else None + ), + commander_mastery_talents=( + [data.read_uint32() for i in range(data.read_bits(3))] + if replay.base_build >= 42932 + else None + ), + trophy_id=( + data.read_uint32() if replay.base_build >= 75689 else None + ), + reward_overrides=( [ - data.read_uint32(), - [data.read_uint32() for i in range(data.read_bits(17))], + [ + data.read_uint32(), + [ + data.read_uint32() + for i in range(data.read_bits(17)) + ], + ] + for j in range(data.read_bits(17)) ] - for j in range(data.read_bits(17)) - ] - if replay.base_build >= 47185 - else None, - brutal_plus_difficulty=data.read_uint32() - if replay.base_build >= 77379 - else None, - retry_mutation_indexes=[ - data.read_uint32() for i in range(data.read_bits(3)) - ] - if replay.base_build >= 77379 - else None, - ac_enemy_race=data.read_uint32() - if replay.base_build >= 77379 - else None, - ac_enemy_wave_type=data.read_uint32() - if replay.base_build >= 77379 - else None, - selected_commander_prestige=data.read_uint32() - if replay.base_build >= 80871 - else None, + if replay.base_build >= 47185 + else None + ), + brutal_plus_difficulty=( + data.read_uint32() if replay.base_build >= 77379 else None + ), + retry_mutation_indexes=( + [data.read_uint32() for i in range(data.read_bits(3))] + if replay.base_build >= 77379 + else None + ), + ac_enemy_race=( + data.read_uint32() if replay.base_build >= 77379 else None + ), + ac_enemy_wave_type=( + data.read_uint32() if replay.base_build >= 77379 else None + ), + selected_commander_prestige=( + data.read_uint32() if replay.base_build >= 80871 else None + ), ) for i in range(data.read_bits(5)) ], random_seed=data.read_uint32(), host_user_id=data.read_bits(4) if data.read_bool() else None, is_single_player=data.read_bool(), - picked_map_tag=data.read_uint8() - if replay.base_build >= 36442 - else None, + picked_map_tag=( + data.read_uint8() if replay.base_build >= 36442 else None + ), game_duration=data.read_uint32(), default_difficulty=data.read_bits(6), - default_ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) - if replay.base_build >= 24764 - else None, + default_ai_build=( + data.read_bits(8 if replay.base_build >= 38749 else 7) + if replay.base_build >= 24764 + else None + ), ), ) if not data.done(): @@ -357,9 +412,9 @@ def __call__(self, data, replay): observe=p[7], result=p[8], working_set_slot=p[9] if replay.build >= 24764 else None, - hero=p[10] - if replay.build >= 34784 and 10 in p - else None, # hero appears to be present in Heroes replays but not StarCraft 2 replays + hero=( + p[10] if replay.build >= 34784 and 10 in p else None + ), # hero appears to be present in Heroes replays but not StarCraft 2 replays ) for p in details[0] ], @@ -376,9 +431,11 @@ def __call__(self, data, replay): mini_save=details[11], game_speed=details[12], default_difficulty=details[13], - mod_paths=details[14] - if (replay.build >= 22612 and replay.versions[1] == 1) - else None, + mod_paths=( + details[14] + if (replay.build >= 22612 and replay.versions[1] == 1) + else None + ), campaign_index=details[15] if replay.versions[1] == 2 else None, restartAsTransitionMap=details[16] if replay.build > 26490 else None, ) @@ -739,9 +796,11 @@ def control_group_update_event(self, data): return dict( control_group_index=data.read_bits(4), control_group_update=data.read_bits(2), - remove_mask=("Mask", self.read_selection_bitmask(data, data.read_uint8())) - if data.read_bool() - else ("None", None), + remove_mask=( + ("Mask", self.read_selection_bitmask(data, data.read_uint8())) + if data.read_bool() + else ("None", None) + ), ) def selection_sync_check_event(self, data): @@ -1004,13 +1063,17 @@ class GameEventsReader_16561(GameEventsReader_15405): def command_event(self, data): return dict( flags=data.read_bits(17), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -1031,9 +1094,9 @@ def command_event(self, data): unit_tag=data.read_uint32(), unit_link=data.read_uint16(), control_player_id=None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1159,13 +1222,17 @@ class GameEventsReader_18574(GameEventsReader_18092): def command_event(self, data): return dict( flags=data.read_bits(18), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -1186,9 +1253,9 @@ def command_event(self, data): unit_tag=data.read_uint32(), unit_link=data.read_uint16(), control_player_id=None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1210,13 +1277,17 @@ class GameEventsReader_19595(GameEventsReader_19132): def command_event(self, data): return dict( flags=data.read_bits(18), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -1236,12 +1307,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1307,13 +1378,17 @@ def user_options_event(self, data): def command_event(self, data): return dict( flags=data.read_bits(20), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -1333,12 +1408,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1544,9 +1619,11 @@ def selection_delta_event(self, data): def camera_update_event(self, data): return dict( - target=dict(x=data.read_uint16(), y=data.read_uint16()) - if data.read_bool() - else None, + target=( + dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None + ), distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1618,12 +1695,16 @@ def hijack_replay_game_event(self, data): game_user_id=data.read_bits(4), observe=data.read_bits(2), name=data.read_aligned_string(data.read_uint8()), - toon_handle=data.read_aligned_string(data.read_bits(7)) - if data.read_bool() - else None, - clan_tag=data.read_aligned_string(data.read_uint8()) - if data.read_bool() - else None, + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None + ), + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None + ), clan_logo=None, ) for i in range(data.read_bits(5)) @@ -1633,9 +1714,11 @@ def hijack_replay_game_event(self, data): def camera_update_event(self, data): return dict( - target=dict(x=data.read_uint16(), y=data.read_uint16()) - if data.read_bool() - else None, + target=( + dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None + ), distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1656,12 +1739,16 @@ def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) - if data.read_bool() - else None, - clan_tag=data.read_aligned_string(data.read_uint8()) - if data.read_bool() - else None, + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None + ), + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None + ), clan_log=None, ) @@ -1713,15 +1800,21 @@ def hijack_replay_game_event(self, data): game_user_id=data.read_bits(4), observe=data.read_bits(2), name=data.read_aligned_string(data.read_uint8()), - toon_handle=data.read_aligned_string(data.read_bits(7)) - if data.read_bool() - else None, - clan_tag=data.read_aligned_string(data.read_uint8()) - if data.read_bool() - else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) - if data.read_bool() - else None, + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None + ), + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None + ), + clan_logo=( + DepotFile(data.read_aligned_bytes(40)) + if data.read_bool() + else None + ), ) for i in range(data.read_bits(5)) ], @@ -1730,9 +1823,11 @@ def hijack_replay_game_event(self, data): def camera_update_event(self, data): return dict( - target=dict(x=data.read_uint16(), y=data.read_uint16()) - if data.read_bool() - else None, + target=( + dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None + ), distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1743,15 +1838,19 @@ def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) - if data.read_bool() - else None, - clan_tag=data.read_aligned_string(data.read_uint8()) - if data.read_bool() - else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) - if data.read_bool() - else None, + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None + ), + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None + ), + clan_logo=( + DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None + ), ) @@ -1871,13 +1970,17 @@ def command_update_target_unit_event(self, data): def command_event(self, data): return dict( flags=data.read_bits(23), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -1897,12 +2000,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -1949,9 +2052,11 @@ def trigger_ping_event(self, data): def camera_update_event(self, data): return dict( - target=dict(x=data.read_uint16(), y=data.read_uint16()) - if data.read_bool() - else None, + target=( + dict(x=data.read_uint16(), y=data.read_uint16()) + if data.read_bool() + else None + ), distance=data.read_uint16() if data.read_bool() else None, pitch=data.read_uint16() if data.read_bool() else None, yaw=data.read_uint16() if data.read_bool() else None, @@ -1966,15 +2071,19 @@ def game_user_join_event(self, data): return dict( observe=data.read_bits(2), name=data.read_aligned_string(data.read_bits(8)), - toon_handle=data.read_aligned_string(data.read_bits(7)) - if data.read_bool() - else None, - clan_tag=data.read_aligned_string(data.read_uint8()) - if data.read_bool() - else None, - clan_logo=DepotFile(data.read_aligned_bytes(40)) - if data.read_bool() - else None, + toon_handle=( + data.read_aligned_string(data.read_bits(7)) + if data.read_bool() + else None + ), + clan_tag=( + data.read_aligned_string(data.read_uint8()) + if data.read_bool() + else None + ), + clan_logo=( + DepotFile(data.read_aligned_bytes(40)) if data.read_bool() else None + ), hijack=data.read_bool(), hijack_clone_game_user_id=data.read_bits(4) if data.read_bool() else None, ) @@ -2020,13 +2129,17 @@ def __init__(self): def trigger_command_error_event(self, data): return dict( error=data.read_uint32() - 2147483648, - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), ) def trigger_mousewheel_event(self, data): @@ -2041,13 +2154,17 @@ def command_event(self, data): # with the only change being that flags now has 25 bits instead of 23. return dict( flags=data.read_bits(25), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -2067,12 +2184,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -2155,13 +2272,17 @@ class GameEventsReader_64469(GameEventsReader_38996): def command_event(self, data): return dict( flags=data.read_bits(26), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -2181,12 +2302,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), @@ -2233,13 +2354,17 @@ class GameEventsReader_80669(GameEventsReader_65895): def command_event(self, data): return dict( flags=data.read_bits(27), - ability=dict( - ability_link=data.read_uint16(), - ability_command_index=data.read_bits(5), - ability_command_data=data.read_uint8() if data.read_bool() else None, - ) - if data.read_bool() - else None, + ability=( + dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=( + data.read_uint8() if data.read_bool() else None + ), + ) + if data.read_bool() + else None + ), data={ # Choice 0: lambda: ("None", None), 1: lambda: ( @@ -2259,12 +2384,12 @@ def command_event(self, data): timer=data.read_uint8(), unit_tag=data.read_uint32(), unit_link=data.read_uint16(), - control_player_id=data.read_bits(4) - if data.read_bool() - else None, - upkeep_player_id=data.read_bits(4) - if data.read_bool() - else None, + control_player_id=( + data.read_bits(4) if data.read_bool() else None + ), + upkeep_player_id=( + data.read_bits(4) if data.read_bool() else None + ), point=dict( x=data.read_bits(20), y=data.read_bits(20), diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index a63c59fc..4ea53c20 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -170,7 +170,9 @@ def test_kr_realm_and_tampered_messages(self): replay = sc2reader.load_replay("test_replays/1.1.3.16939/11.SC2Replay") self.assertEqual(replay.expansion, "WoL") first = [player for player in replay.players if player.name == "명지대학교"][0] - second = [player for player in replay.players if player.name == "티에스엘사기수"][0] + second = [ + player for player in replay.players if player.name == "티에스엘사기수" + ][0] self.assertEqual(first.url, "https://starcraft2.com/en-us/profile/3/1/258945") self.assertEqual(second.url, "https://starcraft2.com/en-us/profile/3/1/102472") self.assertEqual(replay.messages[0].text, "sc2.replays.net") From 0377720aa29be87da52a143b7cfc2d2daae96a63 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 17 Jan 2025 09:29:53 +0100 Subject: [PATCH 131/136] Split lines with str.splitlines() --- new_units.py | 4 ++-- sc2reader/data/__init__.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/new_units.py b/new_units.py index 86d82ee5..26ffa133 100644 --- a/new_units.py +++ b/new_units.py @@ -9,7 +9,7 @@ import sys UNIT_LOOKUP = dict() -for entry in pkgutil.get_data("sc2reader.data", "unit_lookup.csv").split("\n"): +for entry in pkgutil.get_data("sc2reader.data", "unit_lookup.csv").splitlines(): if not entry: continue str_id, title = entry.strip().split(",") @@ -25,7 +25,7 @@ print("") ABIL_LOOKUP = dict() -for entry in pkgutil.get_data("sc2reader.data", "ability_lookup.csv").split("\n"): +for entry in pkgutil.get_data("sc2reader.data", "ability_lookup.csv").splitlines(): if not entry: continue str_id, abilities = entry.split(",", 1) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index ccd6455c..10598e02 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -15,7 +15,7 @@ ABIL_LOOKUP = dict() for entry in ( - pkgutil.get_data("sc2reader.data", "ability_lookup.csv").decode("utf8").split("\n") + pkgutil.get_data("sc2reader.data", "ability_lookup.csv").decode("utf8").splitlines() ): if not entry: continue @@ -24,7 +24,7 @@ UNIT_LOOKUP = dict() for entry in ( - pkgutil.get_data("sc2reader.data", "unit_lookup.csv").decode("utf8").split("\n") + pkgutil.get_data("sc2reader.data", "unit_lookup.csv").decode("utf8").splitlines() ): if not entry: continue @@ -401,7 +401,7 @@ def load_build(expansion, version): unit_file = f"{expansion}/{version}_units.csv" for entry in ( - pkgutil.get_data("sc2reader.data", unit_file).decode("utf8").split("\n") + pkgutil.get_data("sc2reader.data", unit_file).decode("utf8").splitlines() ): if not entry: continue @@ -421,7 +421,7 @@ def load_build(expansion, version): abil_file = f"{expansion}/{version}_abilities.csv" build.add_ability(ability_id=0, name="RightClick", title="Right Click") for entry in ( - pkgutil.get_data("sc2reader.data", abil_file).decode("utf8").split("\n") + pkgutil.get_data("sc2reader.data", abil_file).decode("utf8").splitlines() ): if not entry: continue From 9401d984ca15ec8d05f371499102afc158895af8 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 17 Jan 2025 09:33:35 +0100 Subject: [PATCH 132/136] CircleCI: ruff check --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e89a8abe..85dfc97c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ jobs: - run: python --version ; pip --version ; pwd ; ls -l - run: pip install black codespell ruff - run: codespell -L queenland,uint,assertin - - run: ruff . + - run: ruff check - run: black . --check From 2a727cc60965a6d7572973c4ea2cc22fe04dda02 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Fri, 17 Jan 2025 09:42:04 +0100 Subject: [PATCH 133/136] sc2reader/objects.py:51:9: F811 --- sc2reader/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 9ee978e7..10dbc755 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -48,7 +48,7 @@ def lineup(self): return "".join(sorted(p.play_race[0].upper() for p in self.players)) @property - def hash(self): + def hash(self): # noqa: F811 raw_hash = ",".join(sorted(p.url for p in self.players)) return hashlib.sha256(raw_hash).hexdigest() From 6b7f782129aaaca780ff4d64109def4bfc1045a9 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2025 18:06:12 +0100 Subject: [PATCH 134/136] Update ruff settings and fix rules C417 and UP032 --- examples/sc2autosave.py | 4 +- generate_build_data.py | 4 +- pyproject.toml | 46 +++++++++------ ruff.toml | 7 --- sc2reader/data/__init__.py | 4 +- sc2reader/engine/plugins/context.py | 74 +++++------------------- sc2reader/engine/plugins/creeptracker.py | 14 ++--- sc2reader/events/game.py | 22 ++----- sc2reader/events/tracker.py | 32 +++++----- sc2reader/factories/plugins/replay.py | 23 ++------ sc2reader/factories/sc2factory.py | 4 +- sc2reader/objects.py | 4 +- sc2reader/readers.py | 12 +--- sc2reader/resources.py | 6 +- sc2reader/scripts/sc2parse.py | 12 +--- sc2reader/scripts/sc2printer.py | 16 +---- sc2reader/utils.py | 2 +- 17 files changed, 91 insertions(+), 195 deletions(-) delete mode 100644 ruff.toml diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index 3d826680..3364b4a9 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -373,9 +373,7 @@ def reset(args): exit("Cannot reset, destination must be directory: {0}", args.dest) print( - "About to reset directory: {}\nAll files and subdirectories will be removed.".format( - args.dest - ) + f"About to reset directory: {args.dest}\nAll files and subdirectories will be removed." ) choice = raw_input("Proceed anyway? (y/n) ") if choice.lower() == "y": diff --git a/generate_build_data.py b/generate_build_data.py index a13f9807..7419e547 100644 --- a/generate_build_data.py +++ b/generate_build_data.py @@ -103,9 +103,7 @@ def generate_build_data(balance_data_path): if element_ability_index != train_ability_index: train_ability_index = element_ability_index - train_ability_name = "{}Train{}".format( - unit_id, trained_unit_name - ) + train_ability_name = f"{unit_id}Train{trained_unit_name}" abilities[train_ability_index] = train_ability_name if train_ability_name not in ability_lookup: diff --git a/pyproject.toml b/pyproject.toml index b569cad6..e33c1d05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ keywords = [ "sc2", "starcraft 2", ] -license = {text = "MIT"} -authors = [{name = "Kevin Leung", email = "kkleung89@gmail.com"}] -requires-python = ">=3.7" +license = { text = "MIT" } +authors = [ { name = "Kevin Leung", email = "kkleung89@gmail.com" } ] +requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", @@ -25,11 +25,11 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Games/Entertainment", @@ -46,27 +46,35 @@ dependencies = [ "mpyq", "pillow", ] -[project.optional-dependencies] -testing = [ +optional-dependencies.testing = [ "pytest", ] -[project.urls] -Homepage = "https://github.com/ggtracker/sc2reader" -[project.scripts] -sc2attributes = "sc2reader.scripts.sc2attributes:main" -sc2json = "sc2reader.scripts.sc2json:main" -sc2parse = "sc2reader.scripts.sc2parse:main" -sc2printer = "sc2reader.scripts.sc2printer:main" -sc2replayer = "sc2reader.scripts.sc2replayer:main" +urls.Homepage = "https://github.com/ggtracker/sc2reader" +scripts.sc2attributes = "sc2reader.scripts.sc2attributes:main" +scripts.sc2json = "sc2reader.scripts.sc2json:main" +scripts.sc2parse = "sc2reader.scripts.sc2parse:main" +scripts.sc2printer = "sc2reader.scripts.sc2printer:main" +scripts.sc2replayer = "sc2reader.scripts.sc2replayer:main" [tool.setuptools] include-package-data = true zip-safe = true -platforms = ["any"] +platforms = [ "any" ] [tool.setuptools.dynamic] -readme = {file = ["README.rst", "CHANGELOG.rst"]} -version = {attr = "sc2reader.__version__"} +readme = { file = [ "README.rst", "CHANGELOG.rst" ] } +version = { attr = "sc2reader.__version__" } [tool.setuptools.packages] -find = {namespaces = false} +find = { namespaces = false } + +[tool.ruff] +line-length = 129 + +lint.ignore = [ + "F401", # module imported but unused; consider using `importlib.util.find_spec` to test for availability + "F403", # Run `removestar` on this codebase + "F405", # Run `removestar` on this codebase + "F841", # Run `ruff --select=F841 --fix .` +] +lint.mccabe.max-complexity = 34 diff --git a/ruff.toml b/ruff.toml deleted file mode 100644 index 57d86cdc..00000000 --- a/ruff.toml +++ /dev/null @@ -1,7 +0,0 @@ -ignore = [ - "F401", # module imported but unused; consider using `importlib.util.find_spec` to test for availability - "F403", # Run `removestar` on this codebase - "F405", # Run `removestar` on this codebase - "F841", # Run `ruff --select=F841 --fix .` -] -line-length=129 diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 10598e02..d97e4885 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -339,9 +339,7 @@ def change_type(self, unit, new_type, frame): unit.set_type(unit_type, frame) else: self.logger.error( - "Unable to change type of {} to {} [frame {}]; unit type not found in build {}".format( - unit, new_type, frame, self.id - ) + f"Unable to change type of {unit} to {new_type} [frame {frame}]; unit type not found in build {self.id}" ) def add_ability( diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 6b0d05dc..38f8b3d2 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -43,12 +43,7 @@ def handleCommandEvent(self, event, replay): event.logger.error("\t" + player.__str__()) self.logger.error( - "{}\t{}\tMissing ability {:X} from {}".format( - event.frame, - event.player.name, - event.ability_id, - replay.datapack.__class__.__name__, - ) + f"{event.frame}\t{event.player.name}\tMissing ability {event.ability_id:X} from {replay.datapack.__class__.__name__}" ) else: @@ -197,15 +192,11 @@ def handleUnitDiedEvent(self, event, replay): del replay.active_units[event.unit_id_index] else: self.logger.error( - "Unable to delete unit index {} at {} [{}], index not active.".format( - event.unit_id_index, Length(seconds=event.second), event.frame - ) + f"Unable to delete unit index {event.unit_id_index} at {Length(seconds=event.second)} [{event.frame}], index not active." ) else: self.logger.error( - "Unit {} died at {} [{}] before it was born!".format( - event.unit_id, Length(seconds=event.second), event.frame - ) + f"Unit {event.unit_id} died at {Length(seconds=event.second)} [{event.frame}] before it was born!" ) if event.killing_player_id in replay.player: @@ -215,9 +206,7 @@ def handleUnitDiedEvent(self, event, replay): event.killing_player.killed_units.append(event.unit) elif event.killing_player_id: self.logger.error( - "Unknown killing player id {} at {} [{}]".format( - event.killing_player_id, Length(seconds=event.second), event.frame - ) + f"Unknown killing player id {event.killing_player_id} at {Length(seconds=event.second)} [{event.frame}]" ) if event.killing_unit_id in replay.objects: @@ -227,9 +216,7 @@ def handleUnitDiedEvent(self, event, replay): event.killing_unit.killed_units.append(event.unit) elif event.killing_unit_id: self.logger.error( - "Unknown killing unit id {} at {} [{}]".format( - event.killing_unit_id, Length(seconds=event.second), event.frame - ) + f"Unknown killing unit id {event.killing_unit_id} at {Length(seconds=event.second)} [{event.frame}]" ) def handleUnitOwnerChangeEvent(self, event, replay): @@ -243,9 +230,7 @@ def handleUnitOwnerChangeEvent(self, event, replay): event.unit = replay.objects[event.unit_id] else: self.logger.error( - "Unit {} owner changed at {} [{}] before it was born!".format( - event.unit_id, Length(seconds=event.second), event.frame - ) + f"Unit {event.unit_id} owner changed at {Length(seconds=event.second)} [{event.frame}] before it was born!" ) if event.unit_upkeeper and event.unit: @@ -263,9 +248,7 @@ def handleUnitTypeChangeEvent(self, event, replay): replay.datapack.change_type(event.unit, event.unit_type_name, event.frame) else: self.logger.error( - "Unit {} type changed at {} before it was born!".format( - event.unit_id, Length(seconds=event.second) - ) + f"Unit {event.unit_id} type changed at {Length(seconds=event.second)} before it was born!" ) def handleUpgradeCompleteEvent(self, event, replay): @@ -306,9 +289,7 @@ def handleUnitDoneEvent(self, event, replay): event.unit.finished_at = event.frame else: self.logger.error( - "Unit {} done at {} [{}] before it was started!".format( - event.unit_id, Length(seconds=event.second), event.frame - ) + f"Unit {event.unit_id} done at {Length(seconds=event.second)} [{event.frame}] before it was started!" ) def handleUnitPositionsEvent(self, event, replay): @@ -322,9 +303,7 @@ def handleUnitPositionsEvent(self, event, replay): event.units[unit] = unit.location else: self.logger.error( - "Unit at active_unit index {} moved at {} [{}] but it doesn't exist!".format( - unit_index, Length(seconds=event.second), event.frame - ) + f"Unit at active_unit index {unit_index} moved at {Length(seconds=event.second)} [{event.frame}] but it doesn't exist!" ) def load_message_game_player(self, event, replay): @@ -336,12 +315,7 @@ def load_message_game_player(self, event, replay): event.player.events.append(event) elif event.pid != 16: self.logger.error( - "Bad pid ({}) for event {} at {} [{}].".format( - event.pid, - event.__class__, - Length(seconds=event.second), - event.frame, - ) + f"Bad pid ({event.pid}) for event {event.__class__} at {Length(seconds=event.second)} [{event.frame}]." ) else: pass # This is a global event @@ -352,12 +326,7 @@ def load_message_game_player(self, event, replay): event.player.events.append(event) elif event.pid != 16: self.logger.error( - "Bad pid ({}) for event {} at {} [{}].".format( - event.pid, - event.__class__, - Length(seconds=event.second), - event.frame, - ) + f"Bad pid ({event.pid}) for event {event.__class__} at {Length(seconds=event.second)} [{event.frame}]." ) else: pass # This is a global event @@ -367,12 +336,7 @@ def load_tracker_player(self, event, replay): event.player = replay.entity[event.pid] else: self.logger.error( - "Bad pid ({}) for event {} at {} [{}].".format( - event.pid, - event.__class__, - Length(seconds=event.second), - event.frame, - ) + f"Bad pid ({event.pid}) for event {event.__class__} at {Length(seconds=event.second)} [{event.frame}]." ) def load_tracker_upkeeper(self, event, replay): @@ -380,12 +344,7 @@ def load_tracker_upkeeper(self, event, replay): event.unit_upkeeper = replay.entity[event.upkeep_pid] elif event.upkeep_pid != 0: self.logger.error( - "Bad upkeep_pid ({}) for event {} at {} [{}].".format( - event.upkeep_pid, - event.__class__, - Length(seconds=event.second), - event.frame, - ) + f"Bad upkeep_pid ({event.upkeep_pid}) for event {event.__class__} at {Length(seconds=event.second)} [{event.frame}]." ) def load_tracker_controller(self, event, replay): @@ -393,10 +352,5 @@ def load_tracker_controller(self, event, replay): event.unit_controller = replay.entity[event.control_pid] elif event.control_pid != 0: self.logger.error( - "Bad control_pid ({}) for event {} at {} [{}].".format( - event.control_pid, - event.__class__, - Length(seconds=event.second), - event.frame, - ) + f"Bad control_pid ({event.control_pid}) for event {event.__class__} at {Length(seconds=event.second)} [{event.frame}]." ) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index d947c42a..7125924d 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -229,7 +229,7 @@ def cgu_in_min_to_cgu_units(self, player_id, cgu_in_minutes): ## this function takes index and value of CGU times and returns ## the cgu units with the maximum length for cgu_per_minute in cgu_in_minutes: - indexes = map(lambda x: x[0], cgu_per_minute) + indexes = (x[0] for x in cgu_per_minute) cgu_units = list() for index in indexes: cgu_units.append(self.creep_gen_units[player_id][index]) @@ -247,7 +247,7 @@ def reduce_cgu_per_minute(self, player_id): cgu_unit_max_per_minute = self.cgu_in_min_to_cgu_units( player_id, cgu_per_minute1 ) - minutes = map(lambda x: int(x[0][1] // 60) * 60, cgu_per_minute2) + minutes = (int(x[0][1] // 60) * 60 for x in cgu_per_minute2) self.creep_gen_units[player_id] = list(cgu_unit_max_per_minute) self.creep_gen_units_times[player_id] = list(minutes) @@ -255,8 +255,8 @@ def get_creep_spread_area(self, player_id): ## iterates through all cgus and and calculate the area for index, cgu_per_player in enumerate(self.creep_gen_units[player_id]): # convert cgu list into centre of circles and radius - cgu_radius = map( - lambda x: (x[1], self.unit_name_to_radius[x[2]]), cgu_per_player + cgu_radius = ( + (x[1], self.unit_name_to_radius[x[2]]) for x in cgu_per_player ) # convert event coords to minimap coords cgu_radius = self.convert_cgu_radius_event_to_map_coord(cgu_radius) @@ -286,9 +286,9 @@ def cgu_radius_to_map_positions(self, cgu_radius, radius_to_coordinates): radius = cgu[1] ## subtract all radius_to_coordinates with centre of ## cgu radius to change centre of circle - cgu_map_position = map( - lambda x: (x[0] + point[0], x[1] + point[1]), - self.radius_to_coordinates[radius], + cgu_map_position = ( + (x[0] + point[0], x[1] + point[1]) + for x in self.radius_to_coordinates[radius] ) total_points_on_map = total_points_on_map | Set(cgu_map_position) return total_points_on_map diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 008f6ef9..0da9afa2 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -38,9 +38,7 @@ def _str_prefix(self): if getattr(self, "pid", 16) == 16: player_name = "Global" elif self.player and not self.player.name: - player_name = "Player {} - ({})".format( - self.player.pid, self.player.play_race - ) + player_name = f"Player {self.player.pid} - ({self.player.play_race})" elif self.player: player_name = self.player.name else: @@ -244,9 +242,7 @@ def __str__(self): string += "Right Click" if self.ability_type == "TargetUnit": - string += "; Target: {} [{:0>8X}]".format( - self.target.name, self.target_unit_id - ) + string += f"; Target: {self.target.name} [{self.target_unit_id:0>8X}]" if self.ability_type in ("TargetPoint", "TargetUnit"): string += f"; Location: {str(self.location)}" @@ -643,7 +639,7 @@ def __init__(self, frame, pid, data): self.yaw = data["yaw"] def __str__(self): - return self._str_prefix() + "{} at ({}, {})".format(self.name, self.x, self.y) + return self._str_prefix() + f"{self.name} at ({self.x}, {self.y})" @loggable @@ -686,13 +682,7 @@ def __init__(self, frame, pid, data): def __str__(self): return ( self._str_prefix() - + " transfer {} minerals, {} gas, {} terrazine, and {} custom to {}".format( - self.minerals, - self.vespene, - self.terrazine, - self.custom_resource, - self.recipient, - ) + + f" transfer {self.minerals} minerals, {self.vespene} gas, {self.terrazine} terrazine, and {self.custom_resource} custom to {self.recipient}" ) @@ -722,9 +712,7 @@ def __init__(self, frame, pid, data): def __str__(self): return ( self._str_prefix() - + " requests {} minerals, {} gas, {} terrazine, and {} custom".format( - self.minerals, self.vespene, self.terrazine, self.custom_resource - ) + + f" requests {self.minerals} minerals, {self.vespene} gas, {self.terrazine} terrazine, and {self.custom_resource} custom" ) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 7355ef35..8e0c6772 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -334,8 +334,9 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{: >15} - Unit born {}".format( - str(self.unit_upkeeper), self.unit + return ( + self._str_prefix() + + f"{str(self.unit_upkeeper): >15} - Unit born {self.unit}" ) @@ -409,8 +410,8 @@ def __init__(self, frames, data, build): ) def __str__(self): - return self._str_prefix() + "{: >15} - Unit died {}.".format( - str(self.unit.owner), self.unit + return ( + self._str_prefix() + f"{str(self.unit.owner): >15} - Unit died {self.unit}." ) @@ -448,9 +449,7 @@ def __init__(self, frames, data, build): self.unit_controller = None def __str__(self): - return self._str_prefix() + "{: >15} took {}".format( - str(self.unit_upkeeper), self.unit - ) + return self._str_prefix() + f"{str(self.unit_upkeeper): >15} took {self.unit}" class UnitTypeChangeEvent(TrackerEvent): @@ -479,8 +478,9 @@ def __init__(self, frames, data, build): self.unit_type_name = data[2].decode("utf8") def __str__(self): - return self._str_prefix() + "{: >15} - Unit {} type changed to {}".format( - str(self.unit.owner), self.unit, self.unit_type_name + return ( + self._str_prefix() + + f"{str(self.unit.owner): >15} - Unit {self.unit} type changed to {self.unit_type_name}" ) @@ -505,8 +505,9 @@ def __init__(self, frames, data, build): self.count = data[2] def __str__(self): - return self._str_prefix() + "{: >15} - {} upgrade completed".format( - str(self.player), self.upgrade_type_name + return ( + self._str_prefix() + + f"{str(self.player): >15} - {self.upgrade_type_name} upgrade completed" ) @@ -564,8 +565,9 @@ def __init__(self, frames, data, build): self.location = (self.x, self.y) def __str__(self): - return self._str_prefix() + "{: >15} - Unit initiated {}".format( - str(self.unit_upkeeper), self.unit + return ( + self._str_prefix() + + f"{str(self.unit_upkeeper): >15} - Unit initiated {self.unit}" ) @@ -591,8 +593,8 @@ def __init__(self, frames, data, build): self.unit = None def __str__(self): - return self._str_prefix() + "{: >15} - Unit {} done".format( - str(self.unit.owner), self.unit + return ( + self._str_prefix() + f"{str(self.unit.owner): >15} - Unit {self.unit} done" ) diff --git a/sc2reader/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index ff5986eb..724a51e3 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -155,12 +155,7 @@ def SelectionTracker(replay): selections[event.control_group] = control_group if debug: logger.info( - "[{}] {} selected {} units: {}".format( - Length(seconds=event.second), - person.name, - len(selections[0x0A].objects), - selections[0x0A], - ) + f"[{Length(seconds=event.second)}] {person.name} selected {len(selections[0x0A].objects)} units: {selections[0x0A]}" ) elif event.name == "SetControlGroupEvent": @@ -168,9 +163,7 @@ def SelectionTracker(replay): selections[event.control_group] = selections[0x0A].copy() if debug: logger.info( - "[{}] {} set hotkey {} to current selection".format( - Length(seconds=event.second), person.name, event.hotkey - ) + f"[{Length(seconds=event.second)}] {person.name} set hotkey {event.hotkey} to current selection" ) elif event.name == "AddToControlGroupEvent": @@ -181,9 +174,7 @@ def SelectionTracker(replay): selections[event.control_group] = control_group if debug: logger.info( - "[{}] {} added current selection to hotkey {}".format( - Length(seconds=event.second), person.name, event.hotkey - ) + f"[{Length(seconds=event.second)}] {person.name} added current selection to hotkey {event.hotkey}" ) elif event.name == "GetControlGroupEvent": @@ -193,13 +184,7 @@ def SelectionTracker(replay): selections[0xA] = control_group if debug: logger.info( - "[{}] {} retrieved hotkey {}, {} units: {}".format( - Length(seconds=event.second), - person.name, - event.control_group, - len(selections[0x0A].objects), - selections[0x0A], - ) + f"[{Length(seconds=event.second)}] {person.name} retrieved hotkey {event.control_group}, {len(selections[0x0A].objects)} units: {selections[0x0A]}" ) else: diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index c5298eb3..40de1c7f 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -293,9 +293,7 @@ def __init__(self, cache_dir, **options): ) elif not os.access(self.cache_dir, os.F_OK | os.W_OK | os.R_OK): raise ValueError( - "Must have read/write access to {} for local file caching.".format( - self.cache_dir - ) + f"Must have read/write access to {self.cache_dir} for local file caching." ) def cache_has(self, cache_key): diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 10dbc755..03632dee 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -438,9 +438,7 @@ def __init__(self, pid): def __str__(self): if not self.is_ai: - return "User {}-S2-{}-{}".format( - self.region.upper(), self.subregion, self.bnetid - ) + return f"User {self.region.upper()}-S2-{self.subregion}-{self.bnetid}" else: return f"AI ({self.play_race})" diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 0dbacce4..c74c7c89 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -601,9 +601,7 @@ def __call__(self, data, replay): # Otherwise throw a read error else: raise ReadError( - "Event type {} unknown at position {}.".format( - hex(event_type), hex(event_start) - ), + f"Event type {hex(event_type)} unknown at position {hex(event_start)}.", event_type, event_start, replay, @@ -617,9 +615,7 @@ def __call__(self, data, replay): return game_events except ParseError as e: raise ReadError( - "Parse error '{}' unknown at position {}.".format( - e.msg, hex(event_start) - ), + f"Parse error '{e.msg}' unknown at position {hex(event_start)}.", event_type, event_start, replay, @@ -628,9 +624,7 @@ def __call__(self, data, replay): ) except EOFError as e: raise ReadError( - "EOFError error '{}' unknown at position {}.".format( - e.msg, hex(event_start) - ), + f"EOFError error '{e.msg}' unknown at position {hex(event_start)}.", event_type, event_start, replay, diff --git a/sc2reader/resources.py b/sc2reader/resources.py index d42ceea2..388f1765 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -578,7 +578,7 @@ def get_team(team_id): # Pretty sure this just never worked, forget about it for now self.recorder = None - entity_names = sorted(map(lambda p: p.name, self.entities)) + entity_names = sorted((p.name for p in self.entities)) hash_input = self.region + ":" + ",".join(entity_names) self.people_hash = hashlib.sha256(hash_input.encode("utf8")).hexdigest() @@ -884,9 +884,7 @@ def _get_reader(self, data_file): return reader else: raise ValueError( - "Valid {} reader could not found for build {}".format( - data_file, self.build - ) + f"Valid {data_file} reader could not found for build {self.build}" ) def _get_datapack(self): diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 22f792a4..4b517908 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -71,9 +71,7 @@ def main(): } if human_pids != event_pids: print( - "Event Pid problem! pids={pids} but event pids={event_pids}".format( - pids=human_pids, event_pids=event_pids - ) + f"Event Pid problem! pids={human_pids} but event pids={event_pids}" ) print( " with {path}: {build} - {real_type} on {map_name} - Played {start_time}".format( @@ -82,9 +80,7 @@ def main(): ) elif player_pids != ability_pids: print( - "Ability Pid problem! pids={pids} but event pids={event_pids}".format( - pids=player_pids, event_pids=ability_pids - ) + f"Ability Pid problem! pids={player_pids} but event pids={ability_pids}" ) print( " with {path}: {build} - {real_type} on {map_name} - Played {start_time}".format( @@ -98,9 +94,7 @@ def main(): ) ) print( - "Units were: {units}".format( - units={obj.name for obj in replay.objects.values()} - ) + f"Units were: {({obj.name for obj in replay.objects.values()})}" ) except sc2reader.exceptions.ReadError as e: diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 634aff53..76241a05 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -24,16 +24,10 @@ def printReplay(filepath, arguments): print(" Teams: {}".format("v".join(lineups))) for team in replay.teams: print( - " Team {}\t{} ({})".format( - team.number, team.players[0].name, team.players[0].pick_race[0] - ) + f" Team {team.number}\t{team.players[0].name} ({team.players[0].pick_race[0]})" ) for player in team.players[1:]: - print( - " \t{} ({})".format( - player.name, player.pick_race[0] - ) - ) + print(f" \t{player.name} ({player.pick_race[0]})") if arguments.observers: print(" Observers:") for observer in replay.observers: @@ -51,11 +45,7 @@ def printReplay(filepath, arguments): raise return prev = e.game_events[-1] - print( - "\nVersion {} replay:\n\t{}".format( - e.replay.release_string, e.replay.filepath - ) - ) + print(f"\nVersion {e.replay.release_string} replay:\n\t{e.replay.filepath}") print(f"\t{e.msg}, Type={e.type:X}") print(f"\tPrevious Event: {prev.name}") print("\t\t" + prev.bytes.encode("hex")) diff --git a/sc2reader/utils.py b/sc2reader/utils.py index dae65a9d..7b638d3f 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -93,7 +93,7 @@ def hex(self): """ The hexadecimal representation of the color """ - return "{0.r:02X}{0.g:02X}{0.b:02X}".format(self) + return f"{self.r:02X}{self.g:02X}{self.b:02X}" def __str__(self): return self.name From 7b8dbf7d2ff2e9160bc6855765c9e79982207f59 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 27 Apr 2025 22:29:24 +0200 Subject: [PATCH 135/136] Fix examples/sc2autosave.py my moving sc2reader.scripts.utils.Formatter --- examples/sc2autosave.py | 72 ++++++++++++++++++++++++++++++++------ examples/sc2store.py | 4 +-- sc2reader/__init__.py | 24 ++++++------- sc2reader/readers.py | 2 +- sc2reader/scripts/utils.py | 59 ------------------------------- sc2reader/utils.py | 6 ++-- 6 files changed, 80 insertions(+), 87 deletions(-) delete mode 100644 sc2reader/scripts/utils.py diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index 3364b4a9..d2ae97c0 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -159,18 +159,71 @@ POST-Parse, how to do it?!?!?!?! """ import argparse -import cPickle import os +import pickle +import re import shutil import sys +import textwrap import time import sc2reader -try: - raw_input # Python 2 -except NameError: - raw_input = input # Python 3 + +class Formatter(argparse.RawTextHelpFormatter): + """FlexiFormatter which respects new line formatting and wraps the rest + + Example: + >>> parser = argparse.ArgumentParser(formatter_class=FlexiFormatter) + >>> parser.add_argument('a',help='''\ + ... This argument's help text will have this first long line\ + ... wrapped to fit the target window size so that your text\ + ... remains flexible. + ... + ... 1. This option list + ... 2. is still persisted + ... 3. and the option strings get wrapped like this\ + ... with an indent for readability. + ... + ... You must use backslashes at the end of lines to indicate that\ + ... you want the text to wrap instead of preserving the newline. + ... ''') + + Only the name of this class is considered a public API. All the methods + provided by the class are considered an implementation detail. + """ + + @classmethod + def new(cls, **options): + return lambda prog: Formatter(prog, **options) + + def _split_lines(self, text, width): + lines = list() + main_indent = len(re.match(r"( *)", text).group(1)) + # Wrap each line individually to allow for partial formatting + for line in text.splitlines(): + # Get this line's indent and figure out what indent to use + # if the line wraps. Account for lists of small variety. + indent = len(re.match(r"( *)", line).group(1)) + list_match = re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line) + if list_match: + sub_indent = indent + len(list_match.group(2)) + else: + sub_indent = indent + + # Textwrap will do all the hard work for us + line = self._whitespace_matcher.sub(" ", line).strip() + new_lines = textwrap.wrap( + text=line, + width=width, + initial_indent=" " * (indent - main_indent), + subsequent_indent=" " * (sub_indent - main_indent), + ) + + # Blank lines get eaten by textwrap, put it back with [' '] + lines.extend(new_lines or [" "]) + + return lines def run(args): @@ -375,8 +428,7 @@ def reset(args): print( f"About to reset directory: {args.dest}\nAll files and subdirectories will be removed." ) - choice = raw_input("Proceed anyway? (y/n) ") - if choice.lower() == "y": + if input("Proceed anyway? (y/n) ").strip().lower() == "y": args.log.write(f"Removing old directory: {args.dest}\n") if not args.dryrun: print(args.dest) @@ -410,7 +462,7 @@ def setup(args): args.log.write(f"Loading state from file: {data_file}\n") if os.path.isfile(data_file) and not args.reset: with open(data_file) as file: - return cPickle.load(file) + return pickle.load(file) else: return sc2reader.utils.AttributeDict(last_sync=0) @@ -420,7 +472,7 @@ def save_state(state, args): data_file = os.path.join(args.dest, "sc2autosave.dat") if not args.dryrun: with open(data_file, "w") as file: - cPickle.dump(state, file) + pickle.dump(state, file) else: args.log.write(f"Writing state to file: {data_file}\n") @@ -429,7 +481,7 @@ def main(): parser = argparse.ArgumentParser( description="Automatically copy new replays to directory", fromfile_prefix_chars="@", - formatter_class=sc2reader.scripts.utils.Formatter.new(max_help_position=35), + formatter_class=Formatter.new(max_help_position=35), epilog="And that's all folks", ) diff --git a/examples/sc2store.py b/examples/sc2store.py index 77d0e71b..7f54a3fb 100755 --- a/examples/sc2store.py +++ b/examples/sc2store.py @@ -1,10 +1,10 @@ #!/usr/bin/env python -import cPickle import os +import pickle import shutil -import sys import sqlite3 +import sys import time import sc2reader diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 415b270d..49b76657 100644 --- a/sc2reader/__init__.py +++ b/sc2reader/__init__.py @@ -1,21 +1,21 @@ """ - sc2reader - ~~~~~~~~~~~ +sc2reader +~~~~~~~~~~~ - A library for loading data from Starcraft II game resources. +A library for loading data from Starcraft II game resources. - SC2Factory methods called on the package will be delegated to the default - SC2Factory. To default to a cached factory set one or more of the following - variables in your environment: +SC2Factory methods called on the package will be delegated to the default +SC2Factory. To default to a cached factory set one or more of the following +variables in your environment: - SC2READER_CACHE_DIR = '/absolute/path/to/existing/cache/directory/' - SC2READER_CACHE_MAX_SIZE = MAXIMUM_CACHE_ENTRIES_TO_HOLD_IN_MEMORY + SC2READER_CACHE_DIR = '/absolute/path/to/existing/cache/directory/' + SC2READER_CACHE_MAX_SIZE = MAXIMUM_CACHE_ENTRIES_TO_HOLD_IN_MEMORY - You can also set the default factory via setFactory, useFileCache, useDictCache, - or useDoubleCache functions. +You can also set the default factory via setFactory, useFileCache, useDictCache, +or useDoubleCache functions. - :copyright: (c) 2011 by Graylin Kim. - :license: MIT, see LICENSE for more details. +:copyright: (c) 2011 by Graylin Kim. +:license: MIT, see LICENSE for more details. """ __version__ = "1.8.0" diff --git a/sc2reader/readers.py b/sc2reader/readers.py index c74c7c89..eab6e4c9 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1857,7 +1857,7 @@ def __init__(self): 25: ( None, self.command_manager_reset_event, - ), # Re-using this old number + ), # Reusing this old number 61: (None, self.trigger_hotkey_pressed_event), 103: (CommandManagerStateEvent, self.command_manager_state_event), 104: ( diff --git a/sc2reader/scripts/utils.py b/sc2reader/scripts/utils.py deleted file mode 100644 index 1f87baa4..00000000 --- a/sc2reader/scripts/utils.py +++ /dev/null @@ -1,59 +0,0 @@ -import argparse -import re -import textwrap - - -class Formatter(argparse.RawTextHelpFormatter): - """FlexiFormatter which respects new line formatting and wraps the rest - - Example: - >>> parser = argparse.ArgumentParser(formatter_class=FlexiFormatter) - >>> parser.add_argument('a',help='''\ - ... This argument's help text will have this first long line\ - ... wrapped to fit the target window size so that your text\ - ... remains flexible. - ... - ... 1. This option list - ... 2. is still persisted - ... 3. and the option strings get wrapped like this\ - ... with an indent for readability. - ... - ... You must use backslashes at the end of lines to indicate that\ - ... you want the text to wrap instead of preserving the newline. - ... ''') - - Only the name of this class is considered a public API. All the methods - provided by the class are considered an implementation detail. - """ - - @classmethod - def new(cls, **options): - return lambda prog: Formatter(prog, **options) - - def _split_lines(self, text, width): - lines = list() - main_indent = len(re.match(r"( *)", text).group(1)) - # Wrap each line individually to allow for partial formatting - for line in text.splitlines(): - # Get this line's indent and figure out what indent to use - # if the line wraps. Account for lists of small variety. - indent = len(re.match(r"( *)", line).group(1)) - list_match = re.match(r"( *)(([*-+>]+|\w+\)|\w+\.) +)", line) - if list_match: - sub_indent = indent + len(list_match.group(2)) - else: - sub_indent = indent - - # Textwrap will do all the hard work for us - line = self._whitespace_matcher.sub(" ", line).strip() - new_lines = textwrap.wrap( - text=line, - width=width, - initial_indent=" " * (indent - main_indent), - subsequent_indent=" " * (sub_indent - main_indent), - ) - - # Blank lines get eaten by textwrap, put it back with [' '] - lines.extend(new_lines or [" "]) - - return lines diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 7b638d3f..660f07bd 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -1,11 +1,11 @@ import binascii -import os import json +import os from datetime import timedelta, datetime -from sc2reader.log_utils import loggable -from sc2reader.exceptions import MPQError from sc2reader.constants import COLOR_CODES, COLOR_CODES_INV +from sc2reader.exceptions import MPQError +from sc2reader.log_utils import loggable class DepotFile: From da0747fbe278ed46a851b40533573d8a678a595a Mon Sep 17 00:00:00 2001 From: vince1st Date: Tue, 29 Apr 2025 19:23:13 +0200 Subject: [PATCH 136/136] Fix: examples/sc2autosave.py update to Python 3 Fixed the file by modifying it to use dict() instead of a custom diction that was deleted, modify the map-name parameter to work with the current library, the example was try with python 3.11 with the command: `python .\examples\sc2autosave.py --rename "{matchup}/({length}) {map} {teams}" --length "%M:%S"` a few parameters are yet to be tested --- docs/source/utilities.rst | 13 ------------- examples/sc2autosave.py | 26 ++++++++++++++------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/docs/source/utilities.rst b/docs/source/utilities.rst index e06078ac..340d87d3 100644 --- a/docs/source/utilities.rst +++ b/docs/source/utilities.rst @@ -26,19 +26,6 @@ Length :members: -PersonDict ---------------- - -.. autoclass:: PersonDict - :members: - - -AttributeDict ------------------- - -.. autoclass:: AttributeDict - :members: - get_files --------------- diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index d2ae97c0..af272530 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -166,6 +166,7 @@ import sys import textwrap import time +from functools import cmp_to_key import sc2reader @@ -268,6 +269,7 @@ def run(args): # Apply the aspects to the rename formatting. #'/' is a special character for creation of subdirectories. # TODO: Handle duplicate replay names, its possible.. + path_parts = args.rename.format(**aspects).split("/") filename = path_parts.pop() + ".SC2Replay" @@ -281,9 +283,9 @@ def run(args): # Log the action and run it if we are live msg = "{0}:\n\tSource: {1}\n\tDest: {2}\n" - args.log.write(msg.format(args.action.type, source_path, dest_path)) + args.log.write(msg.format(args.action["type"], source_path, dest_path)) if not args.dryrun: - args.action.run(path, new_path) + args.action["run"](path, new_path) # After every batch completes, save the state and flush the log # TODO: modify the state to include a list of remaining files @@ -368,19 +370,19 @@ def team_compare(team1, team2): def generate_aspects(args, replay): - teams = sorted(replay.teams, args.team_compare) + teams = sorted(replay.teams, key=cmp_to_key(args.team_compare)) matchups, team_strings = list(), list() for team in teams: - team.players = sorted(team.players, args.player_compare) + team.players = sorted(team.players, key=cmp_to_key(args.player_compare)) composition = sorted(p.play_race[0].upper() for p in team.players) matchups.append("".join(composition)) string = ", ".join(p.format(args.player_format) for p in team.players) team_strings.append(string) - return sc2reader.utils.AttributeDict( + return dict( result=teams[0].result, length=replay.length, - map=replay.map, + map=replay.map_name.replace(":", ""), type=replay.type, date=replay.date.strftime(args.date_format), matchup="v".join(matchups), @@ -412,7 +414,7 @@ def scan(args, state): depth=args.depth, followlinks=args.follow_links, ) - return filter(lambda f: os.path.getctime(f) > state.last_sync, files) + return filter(lambda f: os.path.getctime(f) > state["last_sync"], files) def exit(msg, *args, **kwargs): @@ -439,7 +441,7 @@ def reset(args): def setup(args): args.team_compare, args.player_compare = create_compare_funcs(args) - args.action = sc2reader.utils.AttributeDict( + args.action = dict( type=args.action, run=shutil.copy if args.action == "COPY" else shutil.move ) if not os.path.exists(args.source): @@ -461,17 +463,17 @@ def setup(args): args.log.write(f"Loading state from file: {data_file}\n") if os.path.isfile(data_file) and not args.reset: - with open(data_file) as file: + with open(data_file, "rb") as file: return pickle.load(file) else: - return sc2reader.utils.AttributeDict(last_sync=0) + return dict(last_sync=0) def save_state(state, args): - state.last_sync = time.time() + state["last_sync"] = time.time() data_file = os.path.join(args.dest, "sc2autosave.dat") if not args.dryrun: - with open(data_file, "w") as file: + with open(data_file, "wb") as file: pickle.dump(state, file) else: args.log.write(f"Writing state to file: {data_file}\n")

^xL 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 TjrPxbkZr}^4|DizOHBVC5$%g+ literal 0 HcmV?d00001 diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index d6d8a6a2..8d779eea 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -695,6 +695,12 @@ def test_70154(self): factory = sc2reader.factories.SC2Factory() replay = factory.load_replay(replayfilename) + def test_75689(self): + for replayfilename in ["test_replays/4.10.0.75689/trophy_id_13.SC2Replay"]: + factory = sc2reader.factories.SC2Factory() + replay = factory.load_replay(replayfilename) + self.assertEqual(replay.players[0].trophy_id, 13) + def test_anonymous_replay(self): replayfilename = "test_replays/4.1.2.60604/1.SC2Replay" factory = sc2reader.factories.SC2Factory() From 07b6a1f60f5a13b15904e8360d61229720df50e0 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Mon, 19 Aug 2019 16:52:01 -0700 Subject: [PATCH 023/136] mark version 1.4.0 --- CHANGELOG.rst | 4 ++++ setup.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 83c9398b..69d3c035 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ CHANGELOG ============ +1.4.0 - August 19, 2019 +----------------------- +* Add support for protocol 75689 #95 + 1.3.2 - August 9, 2019 ---------------------- * Allow pytest #84 diff --git a/setup.py b/setup.py index f8922724..90c1d629 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.3.2", + version="1.4.0", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From 8cfef81de6d743425e52bb31d871564d95af61a4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Thu, 17 Oct 2019 11:20:27 +0500 Subject: [PATCH 024/136] CircleCI: Upgrade to Python 3.8 --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1344edfc..d94d8e65 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ build_and_test: &build_and_test_steps jobs: StyleCheck: docker: - - image: circleci/python:3.7 + - image: circleci/python:3.8 steps: - checkout - run: sudo pip install flake8 black @@ -31,7 +31,7 @@ jobs: Python3: docker: - - image: circleci/python:3.7 + - image: circleci/python:3.8 steps: *build_and_test_steps From 24922b3bad9d8d3537545966da83ef7004460992 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Fri, 18 Oct 2019 23:34:35 -0700 Subject: [PATCH 025/136] add balance data for 76114 --- sc2reader/data/LotV/76114_abilities.csv | 411 +++++++++ sc2reader/data/LotV/76114_units.csv | 1023 +++++++++++++++++++++++ sc2reader/data/__init__.py | 2 +- sc2reader/data/ability_lookup.csv | 9 +- sc2reader/data/unit_lookup.csv | 27 + sc2reader/resources.py | 6 +- 6 files changed, 1473 insertions(+), 5 deletions(-) create mode 100644 sc2reader/data/LotV/76114_abilities.csv create mode 100644 sc2reader/data/LotV/76114_units.csv diff --git a/sc2reader/data/LotV/76114_abilities.csv b/sc2reader/data/LotV/76114_abilities.csv new file mode 100644 index 00000000..cf1d1ae1 --- /dev/null +++ b/sc2reader/data/LotV/76114_abilities.csv @@ -0,0 +1,411 @@ +39,Taunt +40,stop +42,move +45,attack +60,SprayTerran +61,SprayZerg +62,SprayProtoss +63,SalvageShared +65,GhostHoldFire +66,GhostWeaponsFree +68,Explode +69,FleetBeaconResearch +70,FungalGrowth +71,GuardianShield +72,MULERepair +73,ZerglingTrain +74,NexusTrainMothership +75,Feedback +76,MassRecall +78,HallucinationArchon +79,HallucinationColossus +80,HallucinationHighTemplar +81,HallucinationImmortal +82,HallucinationPhoenix +83,HallucinationProbe +84,HallucinationStalker +85,HallucinationVoidRay +86,HallucinationWarpPrism +87,HallucinationZealot +88,MULEGather +90,CalldownMULE +91,GravitonBeam +95,SpawnChangeling +102,Rally +103,ProgressRally +104,RallyCommand +105,RallyNexus +106,RallyHatchery +107,RoachWarrenResearch +109,InfestedTerrans +110,NeuralParasite +111,SpawnLarva +112,StimpackMarauder +113,SupplyDrop +117,UltraliskCavernResearch +119,SCVHarvest +120,ProbeHarvest +122,que1 +123,que5 +124,que5CancelToSelection +126,que5Addon +127,BuildInProgress +128,Repair +129,TerranBuild +131,Stimpack +132,GhostCloak +134,MedivacHeal +135,SiegeMode +136,Unsiege +137,BansheeCloak +138,MedivacTransport +139,ScannerSweep +140,Yamato +141,AssaultMode +142,FighterMode +143,BunkerTransport +144,CommandCenterTransport +145,CommandCenterLiftOff +146,CommandCenterLand +147,BarracksFlyingBuild +148,BarracksLiftOff +149,FactoryFlyingBuild +150,FactoryLiftOff +151,StarportFlyingBuild +152,StarportLiftOff +153,FactoryLand +154,StarportLand +155,CommandCenterTrain +156,BarracksLand +157,SupplyDepotLower +158,SupplyDepotRaise +159,BarracksTrain +160,FactoryTrain +161,StarportTrain +162,EngineeringBayResearch +164,GhostAcademyTrain +165,BarracksTechLabResearch +166,FactoryTechLabResearch +167,StarportTechLabResearch +168,GhostAcademyResearch +169,ArmoryResearch +170,ProtossBuild +171,WarpPrismTransport +172,GatewayTrain +173,StargateTrain +174,RoboticsFacilityTrain +175,NexusTrain +176,PsiStorm +177,HangarQueue5 +179,CarrierTrain +180,ForgeResearch +181,RoboticsBayResearch +182,TemplarArchiveResearch +183,ZergBuild +184,DroneHarvest +185,EvolutionChamberResearch +186,UpgradeToLair +187,UpgradeToHive +188,UpgradeToGreaterSpire +189,HiveResearch +190,SpawningPoolResearch +191,HydraliskDenResearch +192,GreaterSpireResearch +193,LarvaTrain +194,MorphToBroodLord +195,BurrowBanelingDown +196,BurrowBanelingUp +197,BurrowDroneDown +198,BurrowDroneUp +199,BurrowHydraliskDown +200,BurrowHydraliskUp +201,BurrowRoachDown +202,BurrowRoachUp +203,BurrowZerglingDown +204,BurrowZerglingUp +205,BurrowInfestorTerranDown +206,BurrowInfestorTerranUp +207,RedstoneLavaCritterBurrow +208,RedstoneLavaCritterInjuredBurrow +209,RedstoneLavaCritterUnburrow +210,RedstoneLavaCritterInjuredUnburrow +211,OverlordTransport +214,WarpGateTrain +215,BurrowQueenDown +216,BurrowQueenUp +217,NydusCanalTransport +218,Blink +219,BurrowInfestorDown +220,BurrowInfestorUp +221,MorphToOverseer +222,UpgradeToPlanetaryFortress +223,InfestationPitResearch +224,BanelingNestResearch +225,BurrowUltraliskDown +226,BurrowUltraliskUp +227,UpgradeToOrbital +228,UpgradeToWarpGate +229,MorphBackToGateway +230,OrbitalLiftOff +231,OrbitalCommandLand +232,ForceField +233,PhasingMode +234,TransportMode +235,FusionCoreResearch +236,CyberneticsCoreResearch +237,TwilightCouncilResearch +238,TacNukeStrike +241,EMP +243,HiveTrain +245,Transfusion +254,AttackRedirect +255,StimpackRedirect +256,StimpackMarauderRedirect +258,StopRedirect +259,GenerateCreep +260,QueenBuild +261,SpineCrawlerUproot +262,SporeCrawlerUproot +263,SpineCrawlerRoot +264,SporeCrawlerRoot +265,CreepTumorBurrowedBuild +266,BuildAutoTurret +267,ArchonWarp +268,NydusNetworkBuild +270,Charge +274,Contaminate +277,que5Passive +278,que5PassiveCancelToSelection +281,RavagerCorrosiveBile +282,ShieldBatteryRechargeChanneled +303,BurrowLurkerMPDown +304,BurrowLurkerMPUp +307,BurrowRavagerDown +308,BurrowRavagerUp +309,MorphToRavager +310,MorphToTransportOverlord +312,ThorNormalMode +317,DigesterCreepSpray +321,MorphToMothership +346,XelNagaHealingShrine +355,MothershipCoreMassRecall +357,MorphToHellion +367,MorphToHellionTank +375,MorphToSwarmHostBurrowedMP +376,MorphToSwarmHostMP +378,attackProtossBuilding +380,stopProtossBuilding +381,BlindingCloud +383,Yoink +386,ViperConsumeStructure +389,TestZerg +390,VolatileBurstBuilding +397,WidowMineBurrow +398,WidowMineUnburrow +399,WidowMineAttack +400,TornadoMissile +403,HallucinationOracle +404,MedivacSpeedBoost +405,ExtendingBridgeNEWide8Out +406,ExtendingBridgeNEWide8 +407,ExtendingBridgeNWWide8Out +408,ExtendingBridgeNWWide8 +409,ExtendingBridgeNEWide10Out +410,ExtendingBridgeNEWide10 +411,ExtendingBridgeNWWide10Out +412,ExtendingBridgeNWWide10 +413,ExtendingBridgeNEWide12Out +414,ExtendingBridgeNEWide12 +415,ExtendingBridgeNWWide12Out +416,ExtendingBridgeNWWide12 +418,CritterFlee +419,OracleRevelation +427,MothershipCorePurifyNexus +428,XelNaga_Caverns_DoorE +429,XelNaga_Caverns_DoorEOpened +430,XelNaga_Caverns_DoorN +431,XelNaga_Caverns_DoorNE +432,XelNaga_Caverns_DoorNEOpened +433,XelNaga_Caverns_DoorNOpened +434,XelNaga_Caverns_DoorNW +435,XelNaga_Caverns_DoorNWOpened +436,XelNaga_Caverns_DoorS +437,XelNaga_Caverns_DoorSE +438,XelNaga_Caverns_DoorSEOpened +439,XelNaga_Caverns_DoorSOpened +440,XelNaga_Caverns_DoorSW +441,XelNaga_Caverns_DoorSWOpened +442,XelNaga_Caverns_DoorW +443,XelNaga_Caverns_DoorWOpened +444,XelNaga_Caverns_Floating_BridgeNE8Out +445,XelNaga_Caverns_Floating_BridgeNE8 +446,XelNaga_Caverns_Floating_BridgeNW8Out +447,XelNaga_Caverns_Floating_BridgeNW8 +448,XelNaga_Caverns_Floating_BridgeNE10Out +449,XelNaga_Caverns_Floating_BridgeNE10 +450,XelNaga_Caverns_Floating_BridgeNW10Out +451,XelNaga_Caverns_Floating_BridgeNW10 +452,XelNaga_Caverns_Floating_BridgeNE12Out +453,XelNaga_Caverns_Floating_BridgeNE12 +454,XelNaga_Caverns_Floating_BridgeNW12Out +455,XelNaga_Caverns_Floating_BridgeNW12 +456,XelNaga_Caverns_Floating_BridgeH8Out +457,XelNaga_Caverns_Floating_BridgeH8 +458,XelNaga_Caverns_Floating_BridgeV8Out +459,XelNaga_Caverns_Floating_BridgeV8 +460,XelNaga_Caverns_Floating_BridgeH10Out +461,XelNaga_Caverns_Floating_BridgeH10 +462,XelNaga_Caverns_Floating_BridgeV10Out +463,XelNaga_Caverns_Floating_BridgeV10 +464,XelNaga_Caverns_Floating_BridgeH12Out +465,XelNaga_Caverns_Floating_BridgeH12 +466,XelNaga_Caverns_Floating_BridgeV12Out +467,XelNaga_Caverns_Floating_BridgeV12 +468,TemporalField +494,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +495,SnowRefinery_Terran_ExtendingBridgeNEShort8 +496,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +497,SnowRefinery_Terran_ExtendingBridgeNWShort8 +519,CausticSpray +522,MorphToLurker +526,PurificationNovaTargeted +528,LockOn +530,LockOnCancel +532,Hyperjump +534,ThorAPMode +537,NydusWormTransport +538,OracleWeapon +544,LocustMPFlyingSwoop +545,HallucinationDisruptor +546,HallucinationAdept +547,VoidRaySwarmDamageBoost +548,SeekerDummyChannel +549,AiurLightBridgeNE8Out +550,AiurLightBridgeNE8 +551,AiurLightBridgeNE10Out +552,AiurLightBridgeNE10 +553,AiurLightBridgeNE12Out +554,AiurLightBridgeNE12 +555,AiurLightBridgeNW8Out +556,AiurLightBridgeNW8 +557,AiurLightBridgeNW10Out +558,AiurLightBridgeNW10 +559,AiurLightBridgeNW12Out +560,AiurLightBridgeNW12 +573,ShakurasLightBridgeNE8Out +574,ShakurasLightBridgeNE8 +575,ShakurasLightBridgeNE10Out +576,ShakurasLightBridgeNE10 +577,ShakurasLightBridgeNE12Out +578,ShakurasLightBridgeNE12 +579,ShakurasLightBridgeNW8Out +580,ShakurasLightBridgeNW8 +581,ShakurasLightBridgeNW10Out +582,ShakurasLightBridgeNW10 +583,ShakurasLightBridgeNW12Out +584,ShakurasLightBridgeNW12 +585,VoidMPImmortalReviveRebuild +587,ArbiterMPStasisField +588,ArbiterMPRecall +589,CorsairMPDisruptionWeb +590,MorphToGuardianMP +591,MorphToDevourerMP +592,DefilerMPConsume +593,DefilerMPDarkSwarm +594,DefilerMPPlague +595,DefilerMPBurrow +596,DefilerMPUnburrow +597,QueenMPEnsnare +598,QueenMPSpawnBroodlings +599,QueenMPInfestCommandCenter +603,OracleBuild +607,ParasiticBomb +608,AdeptPhaseShift +611,LurkerHoldFire +612,LurkerRemoveHoldFire +615,LiberatorAGTarget +616,LiberatorAATarget +618,AiurLightBridgeAbandonedNE8Out +619,AiurLightBridgeAbandonedNE8 +620,AiurLightBridgeAbandonedNE10Out +621,AiurLightBridgeAbandonedNE10 +622,AiurLightBridgeAbandonedNE12Out +623,AiurLightBridgeAbandonedNE12 +624,AiurLightBridgeAbandonedNW8Out +625,AiurLightBridgeAbandonedNW8 +626,AiurLightBridgeAbandonedNW10Out +627,AiurLightBridgeAbandonedNW10 +628,AiurLightBridgeAbandonedNW12Out +629,AiurLightBridgeAbandonedNW12 +630,KD8Charge +633,AdeptPhaseShiftCancel +634,AdeptShadePhaseShiftCancel +635,SlaynElementalGrab +637,PortCity_Bridge_UnitNE8Out +638,PortCity_Bridge_UnitNE8 +639,PortCity_Bridge_UnitSE8Out +640,PortCity_Bridge_UnitSE8 +641,PortCity_Bridge_UnitNW8Out +642,PortCity_Bridge_UnitNW8 +643,PortCity_Bridge_UnitSW8Out +644,PortCity_Bridge_UnitSW8 +645,PortCity_Bridge_UnitNE10Out +646,PortCity_Bridge_UnitNE10 +647,PortCity_Bridge_UnitSE10Out +648,PortCity_Bridge_UnitSE10 +649,PortCity_Bridge_UnitNW10Out +650,PortCity_Bridge_UnitNW10 +651,PortCity_Bridge_UnitSW10Out +652,PortCity_Bridge_UnitSW10 +653,PortCity_Bridge_UnitNE12Out +654,PortCity_Bridge_UnitNE12 +655,PortCity_Bridge_UnitSE12Out +656,PortCity_Bridge_UnitSE12 +657,PortCity_Bridge_UnitNW12Out +658,PortCity_Bridge_UnitNW12 +659,PortCity_Bridge_UnitSW12Out +660,PortCity_Bridge_UnitSW12 +661,PortCity_Bridge_UnitN8Out +662,PortCity_Bridge_UnitN8 +663,PortCity_Bridge_UnitS8Out +664,PortCity_Bridge_UnitS8 +665,PortCity_Bridge_UnitE8Out +666,PortCity_Bridge_UnitE8 +667,PortCity_Bridge_UnitW8Out +668,PortCity_Bridge_UnitW8 +669,PortCity_Bridge_UnitN10Out +670,PortCity_Bridge_UnitN10 +671,PortCity_Bridge_UnitS10Out +672,PortCity_Bridge_UnitS10 +673,PortCity_Bridge_UnitE10Out +674,PortCity_Bridge_UnitE10 +675,PortCity_Bridge_UnitW10Out +676,PortCity_Bridge_UnitW10 +677,PortCity_Bridge_UnitN12Out +678,PortCity_Bridge_UnitN12 +679,PortCity_Bridge_UnitS12Out +680,PortCity_Bridge_UnitS12 +681,PortCity_Bridge_UnitE12Out +682,PortCity_Bridge_UnitE12 +683,PortCity_Bridge_UnitW12Out +684,PortCity_Bridge_UnitW12 +687,DarkTemplarBlink +690,BattlecruiserAttack +692,BattlecruiserMove +694,BattlecruiserStop +696,SpawnLocustsTargeted +697,ViperParasiticBombRelay +698,ParasiticBombRelayDodge +699,VoidRaySwarmDamageBoostCancel +703,ChannelSnipe +706,DarkShrineResearch +707,LurkerDenMPResearch +708,ObserverSiegeMorphtoObserver +709,ObserverMorphtoObserverSiege +710,OverseerMorphtoOverseerSiegeMode +711,OverseerSiegeModeMorphtoOverseer +712,RavenScramblerMissile +714,RavenRepairDroneHeal +715,RavenShredderMissile +716,ChronoBoostEnergyCost +717,NexusMassRecall diff --git a/sc2reader/data/LotV/76114_units.csv b/sc2reader/data/LotV/76114_units.csv new file mode 100644 index 00000000..6c7af845 --- /dev/null +++ b/sc2reader/data/LotV/76114_units.csv @@ -0,0 +1,1023 @@ +3,System_Snapshot_Dummy +21,Ball +22,StereoscopicOptionsUnit +23,Colossus +24,TechLab +25,Reactor +27,InfestorTerran +28,BanelingCocoon +29,Baneling +30,Mothership +31,PointDefenseDrone +32,Changeling +33,ChangelingZealot +34,ChangelingMarineShield +35,ChangelingMarine +36,ChangelingZerglingWings +37,ChangelingZergling +39,CommandCenter +40,SupplyDepot +41,Refinery +42,Barracks +43,EngineeringBay +44,MissileTurret +45,Bunker +46,SensorTower +47,GhostAcademy +48,Factory +49,Starport +51,Armory +52,FusionCore +53,AutoTurret +54,SiegeTankSieged +55,SiegeTank +56,VikingAssault +57,VikingFighter +58,CommandCenterFlying +59,BarracksTechLab +60,BarracksReactor +61,FactoryTechLab +62,FactoryReactor +63,StarportTechLab +64,StarportReactor +65,FactoryFlying +66,StarportFlying +67,SCV +68,BarracksFlying +69,SupplyDepotLowered +70,Marine +71,Reaper +72,Ghost +73,Marauder +74,Thor +75,Hellion +76,Medivac +77,Banshee +78,Raven +79,Battlecruiser +80,Nuke +81,Nexus +82,Pylon +83,Assimilator +84,Gateway +85,Forge +86,FleetBeacon +87,TwilightCouncil +88,PhotonCannon +89,Stargate +90,TemplarArchive +91,DarkShrine +92,RoboticsBay +93,RoboticsFacility +94,CyberneticsCore +95,Zealot +96,Stalker +97,HighTemplar +98,DarkTemplar +99,Sentry +100,Phoenix +101,Carrier +102,VoidRay +103,WarpPrism +104,Observer +105,Immortal +106,Probe +107,Interceptor +108,Hatchery +109,CreepTumor +110,Extractor +111,SpawningPool +112,EvolutionChamber +113,HydraliskDen +114,Spire +115,UltraliskCavern +116,InfestationPit +117,NydusNetwork +118,BanelingNest +119,RoachWarren +120,SpineCrawler +121,SporeCrawler +122,Lair +123,Hive +124,GreaterSpire +125,Egg +126,Drone +127,Zergling +128,Overlord +129,Hydralisk +130,Mutalisk +131,Ultralisk +132,Roach +133,Infestor +134,Corruptor +135,BroodLordCocoon +136,BroodLord +137,BanelingBurrowed +138,DroneBurrowed +139,HydraliskBurrowed +140,RoachBurrowed +141,ZerglingBurrowed +142,InfestorTerranBurrowed +143,RedstoneLavaCritterBurrowed +144,RedstoneLavaCritterInjuredBurrowed +145,RedstoneLavaCritter +146,RedstoneLavaCritterInjured +147,QueenBurrowed +148,Queen +149,InfestorBurrowed +150,OverlordCocoon +151,Overseer +152,PlanetaryFortress +153,UltraliskBurrowed +154,OrbitalCommand +155,WarpGate +156,OrbitalCommandFlying +157,ForceField +158,WarpPrismPhasing +159,CreepTumorBurrowed +160,CreepTumorQueen +161,SpineCrawlerUprooted +162,SporeCrawlerUprooted +163,Archon +164,NydusCanal +165,BroodlingEscort +166,GhostAlternate +167,GhostNova +168,RichMineralField +169,RichMineralField750 +170,Ursadon +172,LurkerMPBurrowed +173,LurkerMP +174,LurkerDenMP +175,LurkerMPEgg +176,NydusCanalAttacker +177,OverlordTransport +178,Ravager +179,RavagerBurrowed +180,RavagerCocoon +181,TransportOverlordCocoon +182,XelNagaTower +184,Oracle +185,Tempest +187,InfestedTerransEgg +188,Larva +189,OverseerSiegeMode +191,ReaperPlaceholder +192,MarineACGluescreenDummy +193,FirebatACGluescreenDummy +194,MedicACGluescreenDummy +195,MarauderACGluescreenDummy +196,VultureACGluescreenDummy +197,SiegeTankACGluescreenDummy +198,VikingACGluescreenDummy +199,BansheeACGluescreenDummy +200,BattlecruiserACGluescreenDummy +201,OrbitalCommandACGluescreenDummy +202,BunkerACGluescreenDummy +203,BunkerUpgradedACGluescreenDummy +204,MissileTurretACGluescreenDummy +205,HellbatACGluescreenDummy +206,GoliathACGluescreenDummy +207,CycloneACGluescreenDummy +208,WraithACGluescreenDummy +209,ScienceVesselACGluescreenDummy +210,HerculesACGluescreenDummy +211,ThorACGluescreenDummy +212,PerditionTurretACGluescreenDummy +213,FlamingBettyACGluescreenDummy +214,DevastationTurretACGluescreenDummy +215,BlasterBillyACGluescreenDummy +216,SpinningDizzyACGluescreenDummy +217,ZerglingKerriganACGluescreenDummy +218,RaptorACGluescreenDummy +219,QueenCoopACGluescreenDummy +220,HydraliskACGluescreenDummy +221,HydraliskLurkerACGluescreenDummy +222,MutaliskBroodlordACGluescreenDummy +223,BroodLordACGluescreenDummy +224,UltraliskACGluescreenDummy +225,TorrasqueACGluescreenDummy +226,OverseerACGluescreenDummy +227,LurkerACGluescreenDummy +228,SpineCrawlerACGluescreenDummy +229,SporeCrawlerACGluescreenDummy +230,NydusNetworkACGluescreenDummy +231,OmegaNetworkACGluescreenDummy +232,ZerglingZagaraACGluescreenDummy +233,SwarmlingACGluescreenDummy +234,QueenZagaraACGluescreenDummy +235,BanelingACGluescreenDummy +236,SplitterlingACGluescreenDummy +237,AberrationACGluescreenDummy +238,ScourgeACGluescreenDummy +239,CorruptorACGluescreenDummy +240,OverseerZagaraACGluescreenDummy +241,BileLauncherACGluescreenDummy +242,SwarmQueenACGluescreenDummy +243,RoachACGluescreenDummy +244,RoachVileACGluescreenDummy +245,RavagerACGluescreenDummy +246,SwarmHostACGluescreenDummy +247,MutaliskACGluescreenDummy +248,GuardianACGluescreenDummy +249,DevourerACGluescreenDummy +250,ViperACGluescreenDummy +251,BrutaliskACGluescreenDummy +252,LeviathanACGluescreenDummy +253,ZealotACGluescreenDummy +254,ZealotAiurACGluescreenDummy +255,DragoonACGluescreenDummy +256,HighTemplarACGluescreenDummy +257,ArchonACGluescreenDummy +258,ImmortalACGluescreenDummy +259,ObserverACGluescreenDummy +260,PhoenixAiurACGluescreenDummy +261,ReaverACGluescreenDummy +262,TempestACGluescreenDummy +263,PhotonCannonACGluescreenDummy +264,ZealotVorazunACGluescreenDummy +265,ZealotShakurasACGluescreenDummy +266,StalkerShakurasACGluescreenDummy +267,DarkTemplarShakurasACGluescreenDummy +268,CorsairACGluescreenDummy +269,VoidRayACGluescreenDummy +270,VoidRayShakurasACGluescreenDummy +271,OracleACGluescreenDummy +272,DarkArchonACGluescreenDummy +273,DarkPylonACGluescreenDummy +274,ZealotPurifierACGluescreenDummy +275,SentryPurifierACGluescreenDummy +276,ImmortalKaraxACGluescreenDummy +277,ColossusACGluescreenDummy +278,ColossusPurifierACGluescreenDummy +279,PhoenixPurifierACGluescreenDummy +280,CarrierACGluescreenDummy +281,CarrierAiurACGluescreenDummy +282,KhaydarinMonolithACGluescreenDummy +283,ShieldBatteryACGluescreenDummy +284,EliteMarineACGluescreenDummy +285,MarauderCommandoACGluescreenDummy +286,SpecOpsGhostACGluescreenDummy +287,HellbatRangerACGluescreenDummy +288,StrikeGoliathACGluescreenDummy +289,HeavySiegeTankACGluescreenDummy +290,RaidLiberatorACGluescreenDummy +291,RavenTypeIIACGluescreenDummy +292,CovertBansheeACGluescreenDummy +293,RailgunTurretACGluescreenDummy +294,BlackOpsMissileTurretACGluescreenDummy +295,SupplicantACGluescreenDummy +296,StalkerTaldarimACGluescreenDummy +297,SentryTaldarimACGluescreenDummy +298,HighTemplarTaldarimACGluescreenDummy +299,ImmortalTaldarimACGluescreenDummy +300,ColossusTaldarimACGluescreenDummy +301,WarpPrismTaldarimACGluescreenDummy +302,PhotonCannonTaldarimACGluescreenDummy +303,StukovInfestedCivilianACGluescreenDummy +304,StukovInfestedMarineACGluescreenDummy +305,StukovInfestedSiegeTankACGluescreenDummy +306,StukovInfestedDiamondbackACGluescreenDummy +307,StukovInfestedBansheeACGluescreenDummy +308,SILiberatorACGluescreenDummy +309,StukovInfestedBunkerACGluescreenDummy +310,StukovInfestedMissileTurretACGluescreenDummy +311,StukovBroodQueenACGluescreenDummy +312,ZealotFenixACGluescreenDummy +313,SentryFenixACGluescreenDummy +314,AdeptFenixACGluescreenDummy +315,ImmortalFenixACGluescreenDummy +316,ColossusFenixACGluescreenDummy +317,DisruptorACGluescreenDummy +318,ObserverFenixACGluescreenDummy +319,ScoutACGluescreenDummy +320,CarrierFenixACGluescreenDummy +321,PhotonCannonFenixACGluescreenDummy +322,PrimalZerglingACGluescreenDummy +323,RavasaurACGluescreenDummy +324,PrimalRoachACGluescreenDummy +325,FireRoachACGluescreenDummy +326,PrimalGuardianACGluescreenDummy +327,PrimalHydraliskACGluescreenDummy +328,PrimalMutaliskACGluescreenDummy +329,PrimalImpalerACGluescreenDummy +330,PrimalSwarmHostACGluescreenDummy +331,CreeperHostACGluescreenDummy +332,PrimalUltraliskACGluescreenDummy +333,TyrannozorACGluescreenDummy +334,PrimalWurmACGluescreenDummy +335,HHReaperACGluescreenDummy +336,HHWidowMineACGluescreenDummy +337,HHHellionTankACGluescreenDummy +338,HHWraithACGluescreenDummy +339,HHVikingACGluescreenDummy +340,HHBattlecruiserACGluescreenDummy +341,HHRavenACGluescreenDummy +342,HHBomberPlatformACGluescreenDummy +343,HHMercStarportACGluescreenDummy +344,HHMissileTurretACGluescreenDummy +345,TychusReaperACGluescreenDummy +346,TychusFirebatACGluescreenDummy +347,TychusSpectreACGluescreenDummy +348,TychusMedicACGluescreenDummy +349,TychusMarauderACGluescreenDummy +350,TychusWarhoundACGluescreenDummy +351,TychusHERCACGluescreenDummy +352,TychusGhostACGluescreenDummy +353,TychusSCVAutoTurretACGluescreenDummy +354,ZeratulStalkerACGluescreenDummy +355,ZeratulSentryACGluescreenDummy +356,ZeratulDarkTemplarACGluescreenDummy +357,ZeratulImmortalACGluescreenDummy +358,ZeratulObserverACGluescreenDummy +359,ZeratulDisruptorACGluescreenDummy +360,ZeratulWarpPrismACGluescreenDummy +361,ZeratulPhotonCannonACGluescreenDummy +362,MechaZerglingACGluescreenDummy +363,MechaBanelingACGluescreenDummy +364,MechaHydraliskACGluescreenDummy +365,MechaInfestorACGluescreenDummy +366,MechaCorruptorACGluescreenDummy +367,MechaUltraliskACGluescreenDummy +368,MechaOverseerACGluescreenDummy +369,MechaLurkerACGluescreenDummy +370,MechaBattlecarrierLordACGluescreenDummy +371,MechaSpineCrawlerACGluescreenDummy +372,MechaSporeCrawlerACGluescreenDummy +374,RenegadeLongboltMissileWeapon +375,NeedleSpinesWeapon +376,CorruptionWeapon +377,InfestedTerransWeapon +378,NeuralParasiteWeapon +379,PointDefenseDroneReleaseWeapon +380,HunterSeekerWeapon +381,MULE +383,ThorAAWeapon +384,PunisherGrenadesLMWeapon +385,VikingFighterWeapon +386,ATALaserBatteryLMWeapon +387,ATSLaserBatteryLMWeapon +388,LongboltMissileWeapon +389,D8ChargeWeapon +390,YamatoWeapon +391,IonCannonsWeapon +392,AcidSalivaWeapon +393,SpineCrawlerWeapon +394,SporeCrawlerWeapon +395,GlaiveWurmWeapon +396,GlaiveWurmM2Weapon +397,GlaiveWurmM3Weapon +398,StalkerWeapon +399,EMP2Weapon +400,BacklashRocketsLMWeapon +401,PhotonCannonWeapon +402,ParasiteSporeWeapon +404,Broodling +405,BroodLordBWeapon +408,AutoTurretReleaseWeapon +409,LarvaReleaseMissile +410,AcidSpinesWeapon +411,FrenzyWeapon +412,ContaminateWeapon +424,BeaconArmy +425,BeaconDefend +426,BeaconAttack +427,BeaconHarass +428,BeaconIdle +429,BeaconAuto +430,BeaconDetect +431,BeaconScout +432,BeaconClaim +433,BeaconExpand +434,BeaconRally +435,BeaconCustom1 +436,BeaconCustom2 +437,BeaconCustom3 +438,BeaconCustom4 +443,LiberatorAG +445,PreviewBunkerUpgraded +446,HellionTank +447,Cyclone +448,WidowMine +449,Liberator +451,Adept +452,Disruptor +453,SwarmHostMP +454,Viper +455,ShieldBattery +456,HighTemplarSkinPreview +457,MothershipCore +458,Viking +467,AssimilatorRich +468,RichVespeneGeyser +469,ExtractorRich +470,InhibitorZoneSmall +471,InhibitorZoneMedium +472,InhibitorZoneLarge +473,RavagerCorrosiveBileMissile +474,RavagerWeaponMissile +475,RefineryRich +476,RenegadeMissileTurret +477,Rocks2x2NonConjoined +478,FungalGrowthMissile +479,NeuralParasiteTentacleMissile +480,Beacon_Protoss +481,Beacon_ProtossSmall +482,Beacon_Terran +483,Beacon_TerranSmall +484,Beacon_Zerg +485,Beacon_ZergSmall +486,Lyote +487,CarrionBird +488,KarakMale +489,KarakFemale +490,UrsadakFemaleExotic +491,UrsadakMale +492,UrsadakFemale +493,UrsadakCalf +494,UrsadakMaleExotic +495,UtilityBot +496,CommentatorBot1 +497,CommentatorBot2 +498,CommentatorBot3 +499,CommentatorBot4 +500,Scantipede +501,Dog +502,Sheep +503,Cow +504,InfestedTerransEggPlacement +505,InfestorTerransWeapon +506,MineralField +507,MineralField450 +508,MineralField750 +509,MineralFieldOpaque +510,MineralFieldOpaque900 +511,VespeneGeyser +512,SpacePlatformGeyser +513,DestructibleSearchlight +514,DestructibleBullhornLights +515,DestructibleStreetlight +516,DestructibleSpacePlatformSign +517,DestructibleStoreFrontCityProps +518,DestructibleBillboardTall +519,DestructibleBillboardScrollingText +520,DestructibleSpacePlatformBarrier +521,DestructibleSignsDirectional +522,DestructibleSignsConstruction +523,DestructibleSignsFunny +524,DestructibleSignsIcons +525,DestructibleSignsWarning +526,DestructibleGarage +527,DestructibleGarageLarge +528,DestructibleTrafficSignal +529,TrafficSignal +530,BraxisAlphaDestructible1x1 +531,BraxisAlphaDestructible2x2 +532,DestructibleDebris4x4 +533,DestructibleDebris6x6 +534,DestructibleRock2x4Vertical +535,DestructibleRock2x4Horizontal +536,DestructibleRock2x6Vertical +537,DestructibleRock2x6Horizontal +538,DestructibleRock4x4 +539,DestructibleRock6x6 +540,DestructibleRampDiagonalHugeULBR +541,DestructibleRampDiagonalHugeBLUR +542,DestructibleRampVerticalHuge +543,DestructibleRampHorizontalHuge +544,DestructibleDebrisRampDiagonalHugeULBR +545,DestructibleDebrisRampDiagonalHugeBLUR +546,WarpPrismSkinPreview +547,SiegeTankSkinPreview +548,ThorAP +549,ThorAALance +550,LiberatorSkinPreview +551,OverlordGenerateCreepKeybind +552,MengskStatueAlone +553,MengskStatue +554,WolfStatue +555,GlobeStatue +556,Weapon +557,GlaiveWurmBounceWeapon +558,BroodLordWeapon +559,BroodLordAWeapon +560,CreepBlocker1x1 +561,PermanentCreepBlocker1x1 +562,PathingBlocker1x1 +563,PathingBlocker2x2 +564,AutoTestAttackTargetGround +565,AutoTestAttackTargetAir +566,AutoTestAttacker +567,HelperEmitterSelectionArrow +568,MultiKillObject +569,ShapeGolfball +570,ShapeCone +571,ShapeCube +572,ShapeCylinder +573,ShapeDodecahedron +574,ShapeIcosahedron +575,ShapeOctahedron +576,ShapePyramid +577,ShapeRoundedCube +578,ShapeSphere +579,ShapeTetrahedron +580,ShapeThickTorus +581,ShapeThinTorus +582,ShapeTorus +583,Shape4PointStar +584,Shape5PointStar +585,Shape6PointStar +586,Shape8PointStar +587,ShapeArrowPointer +588,ShapeBowl +589,ShapeBox +590,ShapeCapsule +591,ShapeCrescentMoon +592,ShapeDecahedron +593,ShapeDiamond +594,ShapeFootball +595,ShapeGemstone +596,ShapeHeart +597,ShapeJack +598,ShapePlusSign +599,ShapeShamrock +600,ShapeSpade +601,ShapeTube +602,ShapeEgg +603,ShapeYenSign +604,ShapeX +605,ShapeWatermelon +606,ShapeWonSign +607,ShapeTennisball +608,ShapeStrawberry +609,ShapeSmileyFace +610,ShapeSoccerball +611,ShapeRainbow +612,ShapeSadFace +613,ShapePoundSign +614,ShapePear +615,ShapePineapple +616,ShapeOrange +617,ShapePeanut +618,ShapeO +619,ShapeLemon +620,ShapeMoneyBag +621,ShapeHorseshoe +622,ShapeHockeyStick +623,ShapeHockeyPuck +624,ShapeHand +625,ShapeGolfClub +626,ShapeGrape +627,ShapeEuroSign +628,ShapeDollarSign +629,ShapeBasketball +630,ShapeCarrot +631,ShapeCherry +632,ShapeBaseball +633,ShapeBaseballBat +634,ShapeBanana +635,ShapeApple +636,ShapeCashLarge +637,ShapeCashMedium +638,ShapeCashSmall +639,ShapeFootballColored +640,ShapeLemonSmall +641,ShapeOrangeSmall +642,ShapeTreasureChestOpen +643,ShapeTreasureChestClosed +644,ShapeWatermelonSmall +645,UnbuildableRocksDestructible +646,UnbuildableBricksDestructible +647,UnbuildablePlatesDestructible +648,Debris2x2NonConjoined +649,EnemyPathingBlocker1x1 +650,EnemyPathingBlocker2x2 +651,EnemyPathingBlocker4x4 +652,EnemyPathingBlocker8x8 +653,EnemyPathingBlocker16x16 +654,ScopeTest +655,SentryACGluescreenDummy +656,StukovInfestedTrooperACGluescreenDummy +672,CollapsibleTerranTowerDebris +673,DebrisRampLeft +674,DebrisRampRight +678,LocustMP +679,CollapsibleRockTowerDebris +680,NydusCanalCreeper +681,SwarmHostBurrowedMP +682,WarHound +683,WidowMineBurrowed +684,ExtendingBridgeNEWide8Out +685,ExtendingBridgeNEWide8 +686,ExtendingBridgeNWWide8Out +687,ExtendingBridgeNWWide8 +688,ExtendingBridgeNEWide10Out +689,ExtendingBridgeNEWide10 +690,ExtendingBridgeNWWide10Out +691,ExtendingBridgeNWWide10 +692,ExtendingBridgeNEWide12Out +693,ExtendingBridgeNEWide12 +694,ExtendingBridgeNWWide12Out +695,ExtendingBridgeNWWide12 +697,CollapsibleRockTowerDebrisRampRight +698,CollapsibleRockTowerDebrisRampLeft +699,XelNaga_Caverns_DoorE +700,XelNaga_Caverns_DoorEOpened +701,XelNaga_Caverns_DoorN +702,XelNaga_Caverns_DoorNE +703,XelNaga_Caverns_DoorNEOpened +704,XelNaga_Caverns_DoorNOpened +705,XelNaga_Caverns_DoorNW +706,XelNaga_Caverns_DoorNWOpened +707,XelNaga_Caverns_DoorS +708,XelNaga_Caverns_DoorSE +709,XelNaga_Caverns_DoorSEOpened +710,XelNaga_Caverns_DoorSOpened +711,XelNaga_Caverns_DoorSW +712,XelNaga_Caverns_DoorSWOpened +713,XelNaga_Caverns_DoorW +714,XelNaga_Caverns_DoorWOpened +715,XelNaga_Caverns_Floating_BridgeNE8Out +716,XelNaga_Caverns_Floating_BridgeNE8 +717,XelNaga_Caverns_Floating_BridgeNW8Out +718,XelNaga_Caverns_Floating_BridgeNW8 +719,XelNaga_Caverns_Floating_BridgeNE10Out +720,XelNaga_Caverns_Floating_BridgeNE10 +721,XelNaga_Caverns_Floating_BridgeNW10Out +722,XelNaga_Caverns_Floating_BridgeNW10 +723,XelNaga_Caverns_Floating_BridgeNE12Out +724,XelNaga_Caverns_Floating_BridgeNE12 +725,XelNaga_Caverns_Floating_BridgeNW12Out +726,XelNaga_Caverns_Floating_BridgeNW12 +727,XelNaga_Caverns_Floating_BridgeH8Out +728,XelNaga_Caverns_Floating_BridgeH8 +729,XelNaga_Caverns_Floating_BridgeV8Out +730,XelNaga_Caverns_Floating_BridgeV8 +731,XelNaga_Caverns_Floating_BridgeH10Out +732,XelNaga_Caverns_Floating_BridgeH10 +733,XelNaga_Caverns_Floating_BridgeV10Out +734,XelNaga_Caverns_Floating_BridgeV10 +735,XelNaga_Caverns_Floating_BridgeH12Out +736,XelNaga_Caverns_Floating_BridgeH12 +737,XelNaga_Caverns_Floating_BridgeV12Out +738,XelNaga_Caverns_Floating_BridgeV12 +741,CollapsibleTerranTowerPushUnitRampLeft +742,CollapsibleTerranTowerPushUnitRampRight +745,CollapsibleRockTowerPushUnit +746,CollapsibleTerranTowerPushUnit +747,CollapsibleRockTowerPushUnitRampRight +748,CollapsibleRockTowerPushUnitRampLeft +749,DigesterCreepSprayTargetUnit +750,DigesterCreepSprayUnit +751,NydusCanalAttackerWeapon +752,ViperConsumeStructureWeapon +755,ResourceBlocker +756,TempestWeapon +757,YoinkMissile +761,YoinkVikingAirMissile +763,YoinkVikingGroundMissile +765,YoinkSiegeTankMissile +767,WarHoundWeapon +769,EyeStalkWeapon +772,WidowMineWeapon +773,WidowMineAirWeapon +774,MothershipCoreWeaponWeapon +775,TornadoMissileWeapon +776,TornadoMissileDummyWeapon +777,TalonsMissileWeapon +778,CreepTumorMissile +779,LocustMPEggAMissileWeapon +780,LocustMPEggBMissileWeapon +781,LocustMPWeapon +783,RepulsorCannonWeapon +787,CollapsibleRockTowerDiagonal +788,CollapsibleTerranTowerDiagonal +789,CollapsibleTerranTowerRampLeft +790,CollapsibleTerranTowerRampRight +791,Ice2x2NonConjoined +792,IceProtossCrates +793,ProtossCrates +794,TowerMine +795,PickupPalletGas +796,PickupPalletMinerals +797,PickupScrapSalvage1x1 +798,PickupScrapSalvage2x2 +799,PickupScrapSalvage3x3 +800,RoughTerrain +801,UnbuildableBricksSmallUnit +802,UnbuildablePlatesSmallUnit +803,UnbuildablePlatesUnit +804,UnbuildableRocksSmallUnit +805,XelNagaHealingShrine +806,InvisibleTargetDummy +807,ProtossVespeneGeyser +808,CollapsibleRockTower +809,CollapsibleTerranTower +810,ThornLizard +811,CleaningBot +812,DestructibleRock6x6Weak +813,ProtossSnakeSegmentDemo +814,PhysicsCapsule +815,PhysicsCube +816,PhysicsCylinder +817,PhysicsKnot +818,PhysicsL +819,PhysicsPrimitives +820,PhysicsSphere +821,PhysicsStar +822,CreepBlocker4x4 +823,DestructibleCityDebris2x4Vertical +824,DestructibleCityDebris2x4Horizontal +825,DestructibleCityDebris2x6Vertical +826,DestructibleCityDebris2x6Horizontal +827,DestructibleCityDebris4x4 +828,DestructibleCityDebris6x6 +829,DestructibleCityDebrisHugeDiagonalBLUR +830,DestructibleCityDebrisHugeDiagonalULBR +831,TestZerg +832,PathingBlockerRadius1 +833,DestructibleRockEx12x4Vertical +834,DestructibleRockEx12x4Horizontal +835,DestructibleRockEx12x6Vertical +836,DestructibleRockEx12x6Horizontal +837,DestructibleRockEx14x4 +838,DestructibleRockEx16x6 +839,DestructibleRockEx1DiagonalHugeULBR +840,DestructibleRockEx1DiagonalHugeBLUR +841,DestructibleRockEx1VerticalHuge +842,DestructibleRockEx1HorizontalHuge +843,DestructibleIce2x4Vertical +844,DestructibleIce2x4Horizontal +845,DestructibleIce2x6Vertical +846,DestructibleIce2x6Horizontal +847,DestructibleIce4x4 +848,DestructibleIce6x6 +849,DestructibleIceDiagonalHugeULBR +850,DestructibleIceDiagonalHugeBLUR +851,DestructibleIceVerticalHuge +852,DestructibleIceHorizontalHuge +853,DesertPlanetSearchlight +854,DesertPlanetStreetlight +855,UnbuildableBricksUnit +856,UnbuildableRocksUnit +857,ZerusDestructibleArch +858,Artosilope +859,Anteplott +860,LabBot +861,Crabeetle +862,CollapsibleRockTowerRampRight +863,CollapsibleRockTowerRampLeft +864,LabMineralField +865,LabMineralField750 +880,CollapsibleRockTowerDebrisRampLeftGreen +881,CollapsibleRockTowerDebrisRampRightGreen +882,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +883,SnowRefinery_Terran_ExtendingBridgeNEShort8 +884,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +885,SnowRefinery_Terran_ExtendingBridgeNWShort8 +890,Tarsonis_DoorN +891,Tarsonis_DoorNLowered +892,Tarsonis_DoorNE +893,Tarsonis_DoorNELowered +894,Tarsonis_DoorE +895,Tarsonis_DoorELowered +896,Tarsonis_DoorNW +897,Tarsonis_DoorNWLowered +898,CompoundMansion_DoorN +899,CompoundMansion_DoorNLowered +900,CompoundMansion_DoorNE +901,CompoundMansion_DoorNELowered +902,CompoundMansion_DoorE +903,CompoundMansion_DoorELowered +904,CompoundMansion_DoorNW +905,CompoundMansion_DoorNWLowered +907,LocustMPFlying +908,AiurLightBridgeNE8Out +909,AiurLightBridgeNE8 +910,AiurLightBridgeNE10Out +911,AiurLightBridgeNE10 +912,AiurLightBridgeNE12Out +913,AiurLightBridgeNE12 +914,AiurLightBridgeNW8Out +915,AiurLightBridgeNW8 +916,AiurLightBridgeNW10Out +917,AiurLightBridgeNW10 +918,AiurLightBridgeNW12Out +919,AiurLightBridgeNW12 +920,AiurTempleBridgeNE8Out +922,AiurTempleBridgeNE10Out +924,AiurTempleBridgeNE12Out +926,AiurTempleBridgeNW8Out +928,AiurTempleBridgeNW10Out +930,AiurTempleBridgeNW12Out +932,ShakurasLightBridgeNE8Out +933,ShakurasLightBridgeNE8 +934,ShakurasLightBridgeNE10Out +935,ShakurasLightBridgeNE10 +936,ShakurasLightBridgeNE12Out +937,ShakurasLightBridgeNE12 +938,ShakurasLightBridgeNW8Out +939,ShakurasLightBridgeNW8 +940,ShakurasLightBridgeNW10Out +941,ShakurasLightBridgeNW10 +942,ShakurasLightBridgeNW12Out +943,ShakurasLightBridgeNW12 +944,VoidMPImmortalReviveCorpse +945,GuardianCocoonMP +946,GuardianMP +947,DevourerCocoonMP +948,DevourerMP +949,DefilerMPBurrowed +950,DefilerMP +951,OracleStasisTrap +952,DisruptorPhased +953,AiurLightBridgeAbandonedNE8Out +954,AiurLightBridgeAbandonedNE8 +955,AiurLightBridgeAbandonedNE10Out +956,AiurLightBridgeAbandonedNE10 +957,AiurLightBridgeAbandonedNE12Out +958,AiurLightBridgeAbandonedNE12 +959,AiurLightBridgeAbandonedNW8Out +960,AiurLightBridgeAbandonedNW8 +961,AiurLightBridgeAbandonedNW10Out +962,AiurLightBridgeAbandonedNW10 +963,AiurLightBridgeAbandonedNW12Out +964,AiurLightBridgeAbandonedNW12 +965,CollapsiblePurifierTowerDebris +966,PortCity_Bridge_UnitNE8Out +967,PortCity_Bridge_UnitNE8 +968,PortCity_Bridge_UnitSE8Out +969,PortCity_Bridge_UnitSE8 +970,PortCity_Bridge_UnitNW8Out +971,PortCity_Bridge_UnitNW8 +972,PortCity_Bridge_UnitSW8Out +973,PortCity_Bridge_UnitSW8 +974,PortCity_Bridge_UnitNE10Out +975,PortCity_Bridge_UnitNE10 +976,PortCity_Bridge_UnitSE10Out +977,PortCity_Bridge_UnitSE10 +978,PortCity_Bridge_UnitNW10Out +979,PortCity_Bridge_UnitNW10 +980,PortCity_Bridge_UnitSW10Out +981,PortCity_Bridge_UnitSW10 +982,PortCity_Bridge_UnitNE12Out +983,PortCity_Bridge_UnitNE12 +984,PortCity_Bridge_UnitSE12Out +985,PortCity_Bridge_UnitSE12 +986,PortCity_Bridge_UnitNW12Out +987,PortCity_Bridge_UnitNW12 +988,PortCity_Bridge_UnitSW12Out +989,PortCity_Bridge_UnitSW12 +990,PortCity_Bridge_UnitN8Out +991,PortCity_Bridge_UnitN8 +992,PortCity_Bridge_UnitS8Out +993,PortCity_Bridge_UnitS8 +994,PortCity_Bridge_UnitE8Out +995,PortCity_Bridge_UnitE8 +996,PortCity_Bridge_UnitW8Out +997,PortCity_Bridge_UnitW8 +998,PortCity_Bridge_UnitN10Out +999,PortCity_Bridge_UnitN10 +1000,PortCity_Bridge_UnitS10Out +1001,PortCity_Bridge_UnitS10 +1002,PortCity_Bridge_UnitE10Out +1003,PortCity_Bridge_UnitE10 +1004,PortCity_Bridge_UnitW10Out +1005,PortCity_Bridge_UnitW10 +1006,PortCity_Bridge_UnitN12Out +1007,PortCity_Bridge_UnitN12 +1008,PortCity_Bridge_UnitS12Out +1009,PortCity_Bridge_UnitS12 +1010,PortCity_Bridge_UnitE12Out +1011,PortCity_Bridge_UnitE12 +1012,PortCity_Bridge_UnitW12Out +1013,PortCity_Bridge_UnitW12 +1014,PurifierRichMineralField +1015,PurifierRichMineralField750 +1016,CollapsibleRockTowerPushUnitRampLeftGreen +1017,CollapsibleRockTowerPushUnitRampRightGreen +1032,CollapsiblePurifierTowerPushUnit +1034,LocustMPPrecursor +1035,ReleaseInterceptorsBeacon +1036,AdeptPhaseShift +1037,HydraliskImpaleMissile +1038,CycloneMissileLargeAir +1039,CycloneMissile +1040,CycloneMissileLarge +1041,OracleWeapon +1042,TempestWeaponGround +1043,ScoutMPAirWeaponLeft +1044,ScoutMPAirWeaponRight +1045,ArbiterMPWeaponMissile +1046,GuardianMPWeapon +1047,DevourerMPWeaponMissile +1048,DefilerMPDarkSwarmWeapon +1049,QueenMPEnsnareMissile +1050,QueenMPSpawnBroodlingsMissile +1051,LightningBombWeapon +1052,HERCPlacement +1053,GrappleWeapon +1056,CausticSprayMissile +1057,ParasiticBombMissile +1058,ParasiticBombDummy +1059,AdeptWeapon +1060,AdeptUpgradeWeapon +1061,LiberatorMissile +1062,LiberatorDamageMissile +1063,LiberatorAGMissile +1064,KD8Charge +1065,KD8ChargeWeapon +1067,SlaynElementalGrabWeapon +1068,SlaynElementalGrabAirUnit +1069,SlaynElementalGrabGroundUnit +1070,SlaynElementalWeapon +1075,CollapsibleRockTowerRampLeftGreen +1076,CollapsibleRockTowerRampRightGreen +1077,DestructibleExpeditionGate6x6 +1078,DestructibleZergInfestation3x3 +1079,HERC +1080,Moopy +1081,Replicant +1082,SeekerMissile +1083,AiurTempleBridgeDestructibleNE8Out +1084,AiurTempleBridgeDestructibleNE10Out +1085,AiurTempleBridgeDestructibleNE12Out +1086,AiurTempleBridgeDestructibleNW8Out +1087,AiurTempleBridgeDestructibleNW10Out +1088,AiurTempleBridgeDestructibleNW12Out +1089,AiurTempleBridgeDestructibleSW8Out +1090,AiurTempleBridgeDestructibleSW10Out +1091,AiurTempleBridgeDestructibleSW12Out +1092,AiurTempleBridgeDestructibleSE8Out +1093,AiurTempleBridgeDestructibleSE10Out +1094,AiurTempleBridgeDestructibleSE12Out +1096,FlyoverUnit +1097,CorsairMP +1098,ScoutMP +1100,ArbiterMP +1101,ScourgeMP +1102,DefilerMPPlagueWeapon +1103,QueenMP +1104,XelNagaDestructibleRampBlocker6S +1105,XelNagaDestructibleRampBlocker6SE +1106,XelNagaDestructibleRampBlocker6E +1107,XelNagaDestructibleRampBlocker6NE +1108,XelNagaDestructibleRampBlocker6N +1109,XelNagaDestructibleRampBlocker6NW +1110,XelNagaDestructibleRampBlocker6W +1111,XelNagaDestructibleRampBlocker6SW +1112,XelNagaDestructibleRampBlocker8S +1113,XelNagaDestructibleRampBlocker8SE +1114,XelNagaDestructibleRampBlocker8E +1115,XelNagaDestructibleRampBlocker8NE +1116,XelNagaDestructibleRampBlocker8N +1117,XelNagaDestructibleRampBlocker8NW +1118,XelNagaDestructibleRampBlocker8W +1119,XelNagaDestructibleRampBlocker8SW +1120,XelNagaDestructibleBlocker6S +1121,XelNagaDestructibleBlocker6SE +1122,XelNagaDestructibleBlocker6E +1123,XelNagaDestructibleBlocker6NE +1124,XelNagaDestructibleBlocker6N +1125,XelNagaDestructibleBlocker6NW +1126,XelNagaDestructibleBlocker6W +1127,XelNagaDestructibleBlocker6SW +1128,XelNagaDestructibleBlocker8S +1129,XelNagaDestructibleBlocker8SE +1130,XelNagaDestructibleBlocker8E +1131,XelNagaDestructibleBlocker8NE +1132,XelNagaDestructibleBlocker8N +1133,XelNagaDestructibleBlocker8NW +1134,XelNagaDestructibleBlocker8W +1135,XelNagaDestructibleBlocker8SW +1136,ReptileCrate +1137,SlaynSwarmHostSpawnFlyer +1138,SlaynElemental +1139,PurifierVespeneGeyser +1140,ShakurasVespeneGeyser +1141,CollapsiblePurifierTowerDiagonal +1142,CreepOnlyBlocker4x4 +1143,BattleStationMineralField +1144,BattleStationMineralField750 +1145,PurifierMineralField +1146,PurifierMineralField750 +1147,Beacon_Nova +1148,Beacon_NovaSmall +1149,Ursula +1150,Elsecaro_Colonist_Hut +1151,SnowGlazeStarterMP +1152,PylonOvercharged +1153,ObserverSiegeMode +1154,RavenRepairDrone +1156,ParasiticBombRelayDummy +1157,BypassArmorDrone +1158,AdeptPiercingWeapon +1159,HighTemplarWeaponMissile +1160,CycloneMissileLargeAirAlternative +1161,RavenScramblerMissile +1162,RavenRepairDroneReleaseWeapon +1163,RavenShredderMissileWeapon +1164,InfestedAcidSpinesWeapon +1165,InfestorEnsnareAttackMissile +1166,SNARE_PLACEHOLDER +1169,CorrosiveParasiteWeapon diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index dd5ff207..f866284c 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -469,7 +469,7 @@ def load_build(expansion, version): # Load LotV Data lotv_builds = dict() -for version in ("base", "44401", "47185", "48258", "53644", "54724", "59587", "70154"): +for version in ("base", "44401", "47185", "48258", "53644", "54724", "59587", "70154", "76114"): lotv_builds[version] = load_build("LotV", version) datapacks = builds = {"WoL": wol_builds, "HotS": hots_builds, "LotV": lotv_builds} diff --git a/sc2reader/data/ability_lookup.csv b/sc2reader/data/ability_lookup.csv index 09b69177..5cfe9440 100755 --- a/sc2reader/data/ability_lookup.csv +++ b/sc2reader/data/ability_lookup.csv @@ -127,7 +127,7 @@ FactoryLand,LandFactory,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FactoryLiftOff,LiftFactory,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FactoryReactorMorph,BuildReactorFactory,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FactoryTechLabMorph,BuildTechLabFactory,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -FactoryTechLabResearch,ResearchSiegeTech,ResearchInfernalPreIgniter,Research250mmStrikeCannons,ResearchTransformationServos,ResearchDrillingClaws,,ResearchSmartServos,,ResearchCycloneRapidFireLaunchers,,,,,,,,,,,,,,,,,,,,,,CancelFactoryTechLabResearch, +FactoryTechLabResearch,ResearchSiegeTech,ResearchInfernalPreIgniter,Research250mmStrikeCannons,ResearchTransformationServos,ResearchDrillingClaws,,ResearchSmartServos,,ResearchCycloneRapidFireLaunchers,ResearchCycloneLockOnDamageUpgrade,,,,,,,,,,,,,,,,,,,,,CancelFactoryTechLabResearch, FactoryTrain,,BuildSiegeTank,,,BuildThor,BuildHellion,BuildBattleHellion,TrainCyclone,,,,,BuildWarHound,,,,,,,,,,,,BuildWidowMine,,,,,,CancelFactoryTrain, Feedback,Feedback,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FighterMode,FighterMode,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -140,7 +140,7 @@ FungalGrowth,FungalGrowth,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FusionCoreResearch,ResearchWeaponRefit,ResearchBehemothReactor,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelFusionCoreResearch, GatewayTrain,TrainZealot,TrainStalker,,TrainHighTemplar,TrainDarkTemplar,TrainSentry,TrainAdept,,,,,,,,,,,,,,,,,,,,,,,,CancelGatewayTrain, GenerateCreep,GenerateCreep,StopGenerateCreep,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -GhostAcademyResearch,ResearchPersonalCloaking,ResearchMoebiusReactor,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelGhostAcademyResearch, +GhostAcademyResearch,ResearchPersonalCloaking,ResearchMoebiusReactor,ResearchEnhancedShockwaves,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelGhostAcademyResearch, GhostCloak,CloakGhost,DecloakGhost,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, GhostHoldFire,HoldFireGhost,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, GhostWeaponsFree,GWeaponsFreeGhost,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -329,7 +329,7 @@ TrainQueen,TrainQueen,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelTrainQueen, Transfusion,QueenTransfusion,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, TransportMode,TransportMode,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, TwilightCouncilResearch,ResearchCharge,ResearchBlink,ResearchAdeptPiercingAttack,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelTwilightCouncilResearch, -UltraliskCavernResearch,,,EvolveChitinousPlating,EvolveBurrowCharge,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelUltraliskCavernResearch, +UltraliskCavernResearch,ResearchAnabolicSynthesis,,EvolveChitinousPlating,EvolveBurrowCharge,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelUltraliskCavernResearch, UltraliskWeaponCooldown,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Unsiege,TankMode,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, UpgradeToGreaterSpire,MorphToGreaterSpire,CancelMorphToGreaterSpire,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -861,3 +861,6 @@ NexusMassRecall,NexusMassRecall OverlordSingleTransport,Load,,UnloadAt ParasiticBombRelayDodge,ParasiticBombRelayDodge ViperParasiticBombRelay,ViperParasiticBombRelay +BattlecruiserStop,Stop +BattlecruiserAttack,BattlecruiserAttack +BattlecruiserMove,Move,Patrol,HoldPos diff --git a/sc2reader/data/unit_lookup.csv b/sc2reader/data/unit_lookup.csv index 7327ef0d..cb76e291 100755 --- a/sc2reader/data/unit_lookup.csv +++ b/sc2reader/data/unit_lookup.csv @@ -1020,3 +1020,30 @@ ZeratulStalkerACGluescreenDummy,ZeratulStalkerACGluescreenDummy ZeratulPhotonCannonACGluescreenDummy,ZeratulPhotonCannonACGluescreenDummy Viking,Viking TychusReaperACGluescreenDummy,TychusReaperACGluescreenDummy +MechaZerglingACGluescreenDummy,MechaZerglingACGluescreenDummy +MechaBanelingACGluescreenDummy,MechaBanelingACGluescreenDummy +MechaHydraliskACGluescreenDummy,MechaHydraliskACGluescreenDummy +MechaInfestorACGluescreenDummy,MechaInfestorACGluescreenDummy +MechaCorruptorACGluescreenDummy,MechaCorruptorACGluescreenDummy +MechaUltraliskACGluescreenDummy,MechaUltraliskACGluescreenDummy +MechaOverseerACGluescreenDummy,MechaOverseerACGluescreenDummy +MechaLurkerACGluescreenDummy,MechaLurkerACGluescreenDummy +MechaBattlecarrierLordACGluescreenDummy,MechaBattlecarrierLordACGluescreenDummy +MechaSpineCrawlerACGluescreenDummy,MechaSpineCrawlerACGluescreenDummy +MechaSporeCrawlerACGluescreenDummy,MechaSporeCrawlerACGluescreenDummy +PreviewBunkerUpgraded,PreviewBunkerUpgraded +AssimilatorRich,AssimilatorRich +ExtractorRich,ExtractorRich +InhibitorZoneSmall,InhibitorZoneSmall +InhibitorZoneMedium,InhibitorZoneMedium +InhibitorZoneLarge,InhibitorZoneLarge +RefineryRich,RefineryRich +MineralField450,MineralField450 +MineralFieldOpaque,MineralFieldOpaque +MineralFieldOpaque900,MineralFieldOpaque900 +CollapsibleRockTowerDebrisRampLeftGreen,CollapsibleRockTowerDebrisRampLeftGreen +CollapsibleRockTowerDebrisRampRightGreen,CollapsibleRockTowerDebrisRampRightGreen +CollapsibleRockTowerPushUnitRampLeftGreen,CollapsibleRockTowerPushUnitRampLeftGreen +CollapsibleRockTowerPushUnitRampRightGreen,CollapsibleRockTowerPushUnitRampRightGreen +CollapsibleRockTowerRampLeftGreen,CollapsibleRockTowerRampLeftGreen +CollapsibleRockTowerRampRightGreen,CollapsibleRockTowerRampRightGreen diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 8c1d7110..22de4adf 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -858,7 +858,11 @@ def register_default_datapacks(self): ) self.register_datapack( datapacks["LotV"]["70154"], - lambda r: r.expansion == "LotV" and 70154 <= r.build, + lambda r: r.expansion == "LotV" and 70154 <= r.build < 76114, + ) + self.register_datapack( + datapacks["LotV"]["76114"], + lambda r: r.expansion == "LotV" and 76114 <= r.build, ) # Internal Methods From 3989bd3ac3f88278bbe26867a1a31b4b96782bc5 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Fri, 18 Oct 2019 23:38:57 -0700 Subject: [PATCH 026/136] update HOWTO and black --- sc2reader/data/HOWTO.md | 3 +++ sc2reader/data/__init__.py | 12 +++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/sc2reader/data/HOWTO.md b/sc2reader/data/HOWTO.md index ab67031d..828e5e28 100644 --- a/sc2reader/data/HOWTO.md +++ b/sc2reader/data/HOWTO.md @@ -14,3 +14,6 @@ At the time of writing, the latest build version is 53644. e.g. `python3 sc2reader/generate_build_data.py LotV 53644 balance_data/ sc2reader/` This will generate the necessary data files to support the new build version (namely, `53644_abilities.csv`, `53644_units.csv`, and updated versions of `ability_lookup.csv` and `unit_lookup.csv`). 4. Finally, modify `sc2reader/data/__init__.py` and `sc2reader/resources.py` to register support for the new build version. + +If you are not able to see the correct expansion for the balance data, you may need to authenticate. See the instructions at +https://github.com/ggtracker/sc2reader/issues/98#issuecomment-542554588 on how to do that diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index f866284c..f8340c2b 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -469,7 +469,17 @@ def load_build(expansion, version): # Load LotV Data lotv_builds = dict() -for version in ("base", "44401", "47185", "48258", "53644", "54724", "59587", "70154", "76114"): +for version in ( + "base", + "44401", + "47185", + "48258", + "53644", + "54724", + "59587", + "70154", + "76114", +): lotv_builds[version] = load_build("LotV", version) datapacks = builds = {"WoL": wol_builds, "HotS": hots_builds, "LotV": lotv_builds} From 763fc35746f5f153866edf286c5ad322464637b3 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 4 Nov 2019 05:56:29 +0100 Subject: [PATCH 027/136] Workaround for missing speed in GAME_SPEED_FACTOR Fixes #101 --- sc2reader/resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 22de4adf..f2b18e6c 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -431,7 +431,7 @@ def load_details(self): self.game_length = self.length self.real_length = utils.Length( seconds=int( - self.length.seconds / GAME_SPEED_FACTOR[self.expansion][self.speed] + self.length.seconds / GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) ) ) self.start_time = datetime.utcfromtimestamp( From 27e79f174f37e6d2789e38deef2748f5e2339f73 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 4 Nov 2019 12:26:30 +0100 Subject: [PATCH 028/136] fixup: Placate psf/black --- sc2reader/resources.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index f2b18e6c..5a7872ce 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -431,7 +431,8 @@ def load_details(self): self.game_length = self.length self.real_length = utils.Length( seconds=int( - self.length.seconds / GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) + self.length.seconds + / GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) ) ) self.start_time = datetime.utcfromtimestamp( From c01974330b664cee59c9bf4ae7f4c7b45aaa9e6c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Mon, 4 Nov 2019 12:32:50 +0100 Subject: [PATCH 029/136] Use integer division // --- sc2reader/resources.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 5a7872ce..954efb09 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -430,10 +430,8 @@ def load_details(self): self.game_length = self.length self.real_length = utils.Length( - seconds=int( - self.length.seconds - / GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) - ) + seconds=self.length.seconds + // GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) ) self.start_time = datetime.utcfromtimestamp( self.unix_timestamp - self.real_length.seconds From 10576b62c3330e3dc5711c1010ea8d531ad6563b Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 5 Nov 2019 06:30:54 +0100 Subject: [PATCH 030/136] =?UTF-8?q?Use=20the=20=E2=80=9Cnormal=E2=80=9D=20?= =?UTF-8?q?speed=20(0.6)=20as=20the=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sc2reader/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 954efb09..09275ac9 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -431,8 +431,8 @@ def load_details(self): self.game_length = self.length self.real_length = utils.Length( seconds=self.length.seconds - // GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1) - ) + // GAME_SPEED_FACTOR[self.expansion].get(self.speed, 0.6) + ) # 0.6 is "normal" speed self.start_time = datetime.utcfromtimestamp( self.unix_timestamp - self.real_length.seconds ) From eff2924c9a06d6f26e36e15c9a2272c83c5ed9d0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 5 Nov 2019 09:07:57 +0100 Subject: [PATCH 031/136] Let's go faster... --- sc2reader/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 09275ac9..a74445fc 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -431,8 +431,8 @@ def load_details(self): self.game_length = self.length self.real_length = utils.Length( seconds=self.length.seconds - // GAME_SPEED_FACTOR[self.expansion].get(self.speed, 0.6) - ) # 0.6 is "normal" speed + // GAME_SPEED_FACTOR[self.expansion].get(self.speed, 1.0) + ) self.start_time = datetime.utcfromtimestamp( self.unix_timestamp - self.real_length.seconds ) From 77fa3580c9c9b1b6cc08fc23aaeb30c2009c0609 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 24 Nov 2019 15:32:37 +0100 Subject: [PATCH 032/136] Set defaults when parsing editor replays Fixes #103 --- sc2reader/resources.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index a74445fc..4898be33 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -371,9 +371,9 @@ def load_attribute_events(self): self.attributes[attr.player][attr.name] = attr.value # Populate replay with attributes - self.speed = self.attributes[16]["Game Speed"] - self.category = self.attributes[16]["Game Mode"] - self.type = self.game_type = self.attributes[16]["Teams"] + self.speed = self.attributes[16].get("Game Speed", 1.0) + self.category = self.attributes[16].get("Game Mode", "") + self.type = self.game_type = self.attributes[16].get("Teams", "") self.is_ladder = self.category == "Ladder" self.is_private = self.category == "Private" From 5b203c8e3c17f1d8b2eb78062842f1ffd3f8a080 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 26 Nov 2019 06:54:23 +0100 Subject: [PATCH 033/136] CircleCI: Add more flake8 tests --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d94d8e65..8e96bb5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,9 +2,9 @@ version: 2.0 build_and_test: &build_and_test_steps - checkout - - run: sudo pip install -r requirements.txt + - run: sudo pip install --upgrade pip + - run: sudo pip install pytest -r requirements.txt - run: pip install --user . - - run: sudo pip install pytest - run: python --version ; pip --version ; pwd ; ls -l - run: pytest @@ -18,7 +18,7 @@ jobs: - run: sudo pip install flake8 black - run: python --version ; pip --version ; pwd ; ls -l # stop the build if there are Python syntax errors or undefined names - - run: flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics + - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - run: flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - run: black . --check From 72647496aa3ca229e79480bdc2eef14abc646d37 Mon Sep 17 00:00:00 2001 From: Talv Date: Wed, 27 Nov 2019 12:09:26 +0100 Subject: [PATCH 034/136] Add balance data for 77379 --- sc2reader/data/LotV/77379_abilities.csv | 411 +++++++++ sc2reader/data/LotV/77379_units.csv | 1038 +++++++++++++++++++++++ sc2reader/data/__init__.py | 1 + sc2reader/data/ability_lookup.csv | 5 +- sc2reader/data/unit_lookup.csv | 15 + sc2reader/resources.py | 6 +- 6 files changed, 1473 insertions(+), 3 deletions(-) create mode 100644 sc2reader/data/LotV/77379_abilities.csv create mode 100644 sc2reader/data/LotV/77379_units.csv diff --git a/sc2reader/data/LotV/77379_abilities.csv b/sc2reader/data/LotV/77379_abilities.csv new file mode 100644 index 00000000..b06e856c --- /dev/null +++ b/sc2reader/data/LotV/77379_abilities.csv @@ -0,0 +1,411 @@ +39,Taunt +40,stop +42,move +45,attack +60,SprayTerran +61,SprayZerg +62,SprayProtoss +63,SalvageShared +65,GhostHoldFire +66,GhostWeaponsFree +68,Explode +69,FleetBeaconResearch +70,FungalGrowth +71,GuardianShield +72,MULERepair +73,ZerglingTrain +74,NexusTrainMothership +75,Feedback +76,MassRecall +78,HallucinationArchon +79,HallucinationColossus +80,HallucinationHighTemplar +81,HallucinationImmortal +82,HallucinationPhoenix +83,HallucinationProbe +84,HallucinationStalker +85,HallucinationVoidRay +86,HallucinationWarpPrism +87,HallucinationZealot +88,MULEGather +90,CalldownMULE +91,GravitonBeam +95,SpawnChangeling +102,Rally +103,ProgressRally +104,RallyCommand +105,RallyNexus +106,RallyHatchery +107,RoachWarrenResearch +110,NeuralParasite +111,SpawnLarva +112,StimpackMarauder +113,SupplyDrop +117,UltraliskCavernResearch +119,SCVHarvest +120,ProbeHarvest +122,que1 +123,que5 +124,que5CancelToSelection +126,que5Addon +127,BuildInProgress +128,Repair +129,TerranBuild +131,Stimpack +132,GhostCloak +134,MedivacHeal +135,SiegeMode +136,Unsiege +137,BansheeCloak +138,MedivacTransport +139,ScannerSweep +140,Yamato +141,AssaultMode +142,FighterMode +143,BunkerTransport +144,CommandCenterTransport +145,CommandCenterLiftOff +146,CommandCenterLand +147,BarracksFlyingBuild +148,BarracksLiftOff +149,FactoryFlyingBuild +150,FactoryLiftOff +151,StarportFlyingBuild +152,StarportLiftOff +153,FactoryLand +154,StarportLand +155,CommandCenterTrain +156,BarracksLand +157,SupplyDepotLower +158,SupplyDepotRaise +159,BarracksTrain +160,FactoryTrain +161,StarportTrain +162,EngineeringBayResearch +164,GhostAcademyTrain +165,BarracksTechLabResearch +166,FactoryTechLabResearch +167,StarportTechLabResearch +168,GhostAcademyResearch +169,ArmoryResearch +170,ProtossBuild +171,WarpPrismTransport +172,GatewayTrain +173,StargateTrain +174,RoboticsFacilityTrain +175,NexusTrain +176,PsiStorm +177,HangarQueue5 +179,CarrierTrain +180,ForgeResearch +181,RoboticsBayResearch +182,TemplarArchiveResearch +183,ZergBuild +184,DroneHarvest +185,EvolutionChamberResearch +186,UpgradeToLair +187,UpgradeToHive +188,UpgradeToGreaterSpire +189,HiveResearch +190,SpawningPoolResearch +191,HydraliskDenResearch +192,GreaterSpireResearch +193,LarvaTrain +194,MorphToBroodLord +195,BurrowBanelingDown +196,BurrowBanelingUp +197,BurrowDroneDown +198,BurrowDroneUp +199,BurrowHydraliskDown +200,BurrowHydraliskUp +201,BurrowRoachDown +202,BurrowRoachUp +203,BurrowZerglingDown +204,BurrowZerglingUp +205,BurrowInfestorTerranDown +206,BurrowInfestorTerranUp +207,RedstoneLavaCritterBurrow +208,RedstoneLavaCritterInjuredBurrow +209,RedstoneLavaCritterUnburrow +210,RedstoneLavaCritterInjuredUnburrow +211,OverlordTransport +214,WarpGateTrain +215,BurrowQueenDown +216,BurrowQueenUp +217,NydusCanalTransport +218,Blink +219,BurrowInfestorDown +220,BurrowInfestorUp +221,MorphToOverseer +222,UpgradeToPlanetaryFortress +223,InfestationPitResearch +224,BanelingNestResearch +225,BurrowUltraliskDown +226,BurrowUltraliskUp +227,UpgradeToOrbital +228,UpgradeToWarpGate +229,MorphBackToGateway +230,OrbitalLiftOff +231,OrbitalCommandLand +232,ForceField +233,PhasingMode +234,TransportMode +235,FusionCoreResearch +236,CyberneticsCoreResearch +237,TwilightCouncilResearch +238,TacNukeStrike +241,EMP +243,HiveTrain +245,Transfusion +254,AttackRedirect +255,StimpackRedirect +256,StimpackMarauderRedirect +258,StopRedirect +259,GenerateCreep +260,QueenBuild +261,SpineCrawlerUproot +262,SporeCrawlerUproot +263,SpineCrawlerRoot +264,SporeCrawlerRoot +265,CreepTumorBurrowedBuild +266,BuildAutoTurret +267,ArchonWarp +268,NydusNetworkBuild +270,Charge +274,Contaminate +277,que5Passive +278,que5PassiveCancelToSelection +281,RavagerCorrosiveBile +282,ShieldBatteryRechargeChanneled +303,BurrowLurkerMPDown +304,BurrowLurkerMPUp +307,BurrowRavagerDown +308,BurrowRavagerUp +309,MorphToRavager +310,MorphToTransportOverlord +312,ThorNormalMode +317,DigesterCreepSpray +321,MorphToMothership +346,XelNagaHealingShrine +355,MothershipCoreMassRecall +357,MorphToHellion +367,MorphToHellionTank +375,MorphToSwarmHostBurrowedMP +376,MorphToSwarmHostMP +378,attackProtossBuilding +380,stopProtossBuilding +381,BlindingCloud +383,Yoink +386,ViperConsumeStructure +389,TestZerg +390,VolatileBurstBuilding +397,WidowMineBurrow +398,WidowMineUnburrow +399,WidowMineAttack +400,TornadoMissile +403,HallucinationOracle +404,MedivacSpeedBoost +405,ExtendingBridgeNEWide8Out +406,ExtendingBridgeNEWide8 +407,ExtendingBridgeNWWide8Out +408,ExtendingBridgeNWWide8 +409,ExtendingBridgeNEWide10Out +410,ExtendingBridgeNEWide10 +411,ExtendingBridgeNWWide10Out +412,ExtendingBridgeNWWide10 +413,ExtendingBridgeNEWide12Out +414,ExtendingBridgeNEWide12 +415,ExtendingBridgeNWWide12Out +416,ExtendingBridgeNWWide12 +418,CritterFlee +419,OracleRevelation +427,MothershipCorePurifyNexus +428,XelNaga_Caverns_DoorE +429,XelNaga_Caverns_DoorEOpened +430,XelNaga_Caverns_DoorN +431,XelNaga_Caverns_DoorNE +432,XelNaga_Caverns_DoorNEOpened +433,XelNaga_Caverns_DoorNOpened +434,XelNaga_Caverns_DoorNW +435,XelNaga_Caverns_DoorNWOpened +436,XelNaga_Caverns_DoorS +437,XelNaga_Caverns_DoorSE +438,XelNaga_Caverns_DoorSEOpened +439,XelNaga_Caverns_DoorSOpened +440,XelNaga_Caverns_DoorSW +441,XelNaga_Caverns_DoorSWOpened +442,XelNaga_Caverns_DoorW +443,XelNaga_Caverns_DoorWOpened +444,XelNaga_Caverns_Floating_BridgeNE8Out +445,XelNaga_Caverns_Floating_BridgeNE8 +446,XelNaga_Caverns_Floating_BridgeNW8Out +447,XelNaga_Caverns_Floating_BridgeNW8 +448,XelNaga_Caverns_Floating_BridgeNE10Out +449,XelNaga_Caverns_Floating_BridgeNE10 +450,XelNaga_Caverns_Floating_BridgeNW10Out +451,XelNaga_Caverns_Floating_BridgeNW10 +452,XelNaga_Caverns_Floating_BridgeNE12Out +453,XelNaga_Caverns_Floating_BridgeNE12 +454,XelNaga_Caverns_Floating_BridgeNW12Out +455,XelNaga_Caverns_Floating_BridgeNW12 +456,XelNaga_Caverns_Floating_BridgeH8Out +457,XelNaga_Caverns_Floating_BridgeH8 +458,XelNaga_Caverns_Floating_BridgeV8Out +459,XelNaga_Caverns_Floating_BridgeV8 +460,XelNaga_Caverns_Floating_BridgeH10Out +461,XelNaga_Caverns_Floating_BridgeH10 +462,XelNaga_Caverns_Floating_BridgeV10Out +463,XelNaga_Caverns_Floating_BridgeV10 +464,XelNaga_Caverns_Floating_BridgeH12Out +465,XelNaga_Caverns_Floating_BridgeH12 +466,XelNaga_Caverns_Floating_BridgeV12Out +467,XelNaga_Caverns_Floating_BridgeV12 +468,TemporalField +494,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +495,SnowRefinery_Terran_ExtendingBridgeNEShort8 +496,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +497,SnowRefinery_Terran_ExtendingBridgeNWShort8 +519,CausticSpray +522,MorphToLurker +526,PurificationNovaTargeted +528,LockOn +530,LockOnCancel +532,Hyperjump +534,ThorAPMode +537,NydusWormTransport +538,OracleWeapon +544,LocustMPFlyingSwoop +545,HallucinationDisruptor +546,HallucinationAdept +547,VoidRaySwarmDamageBoost +548,SeekerDummyChannel +549,AiurLightBridgeNE8Out +550,AiurLightBridgeNE8 +551,AiurLightBridgeNE10Out +552,AiurLightBridgeNE10 +553,AiurLightBridgeNE12Out +554,AiurLightBridgeNE12 +555,AiurLightBridgeNW8Out +556,AiurLightBridgeNW8 +557,AiurLightBridgeNW10Out +558,AiurLightBridgeNW10 +559,AiurLightBridgeNW12Out +560,AiurLightBridgeNW12 +573,ShakurasLightBridgeNE8Out +574,ShakurasLightBridgeNE8 +575,ShakurasLightBridgeNE10Out +576,ShakurasLightBridgeNE10 +577,ShakurasLightBridgeNE12Out +578,ShakurasLightBridgeNE12 +579,ShakurasLightBridgeNW8Out +580,ShakurasLightBridgeNW8 +581,ShakurasLightBridgeNW10Out +582,ShakurasLightBridgeNW10 +583,ShakurasLightBridgeNW12Out +584,ShakurasLightBridgeNW12 +585,VoidMPImmortalReviveRebuild +587,ArbiterMPStasisField +588,ArbiterMPRecall +589,CorsairMPDisruptionWeb +590,MorphToGuardianMP +591,MorphToDevourerMP +592,DefilerMPConsume +593,DefilerMPDarkSwarm +594,DefilerMPPlague +595,DefilerMPBurrow +596,DefilerMPUnburrow +597,QueenMPEnsnare +598,QueenMPSpawnBroodlings +599,QueenMPInfestCommandCenter +603,OracleBuild +607,ParasiticBomb +608,AdeptPhaseShift +611,LurkerHoldFire +612,LurkerRemoveHoldFire +615,LiberatorAGTarget +616,LiberatorAATarget +618,AiurLightBridgeAbandonedNE8Out +619,AiurLightBridgeAbandonedNE8 +620,AiurLightBridgeAbandonedNE10Out +621,AiurLightBridgeAbandonedNE10 +622,AiurLightBridgeAbandonedNE12Out +623,AiurLightBridgeAbandonedNE12 +624,AiurLightBridgeAbandonedNW8Out +625,AiurLightBridgeAbandonedNW8 +626,AiurLightBridgeAbandonedNW10Out +627,AiurLightBridgeAbandonedNW10 +628,AiurLightBridgeAbandonedNW12Out +629,AiurLightBridgeAbandonedNW12 +630,KD8Charge +633,AdeptPhaseShiftCancel +634,AdeptShadePhaseShiftCancel +635,SlaynElementalGrab +637,PortCity_Bridge_UnitNE8Out +638,PortCity_Bridge_UnitNE8 +639,PortCity_Bridge_UnitSE8Out +640,PortCity_Bridge_UnitSE8 +641,PortCity_Bridge_UnitNW8Out +642,PortCity_Bridge_UnitNW8 +643,PortCity_Bridge_UnitSW8Out +644,PortCity_Bridge_UnitSW8 +645,PortCity_Bridge_UnitNE10Out +646,PortCity_Bridge_UnitNE10 +647,PortCity_Bridge_UnitSE10Out +648,PortCity_Bridge_UnitSE10 +649,PortCity_Bridge_UnitNW10Out +650,PortCity_Bridge_UnitNW10 +651,PortCity_Bridge_UnitSW10Out +652,PortCity_Bridge_UnitSW10 +653,PortCity_Bridge_UnitNE12Out +654,PortCity_Bridge_UnitNE12 +655,PortCity_Bridge_UnitSE12Out +656,PortCity_Bridge_UnitSE12 +657,PortCity_Bridge_UnitNW12Out +658,PortCity_Bridge_UnitNW12 +659,PortCity_Bridge_UnitSW12Out +660,PortCity_Bridge_UnitSW12 +661,PortCity_Bridge_UnitN8Out +662,PortCity_Bridge_UnitN8 +663,PortCity_Bridge_UnitS8Out +664,PortCity_Bridge_UnitS8 +665,PortCity_Bridge_UnitE8Out +666,PortCity_Bridge_UnitE8 +667,PortCity_Bridge_UnitW8Out +668,PortCity_Bridge_UnitW8 +669,PortCity_Bridge_UnitN10Out +670,PortCity_Bridge_UnitN10 +671,PortCity_Bridge_UnitS10Out +672,PortCity_Bridge_UnitS10 +673,PortCity_Bridge_UnitE10Out +674,PortCity_Bridge_UnitE10 +675,PortCity_Bridge_UnitW10Out +676,PortCity_Bridge_UnitW10 +677,PortCity_Bridge_UnitN12Out +678,PortCity_Bridge_UnitN12 +679,PortCity_Bridge_UnitS12Out +680,PortCity_Bridge_UnitS12 +681,PortCity_Bridge_UnitE12Out +682,PortCity_Bridge_UnitE12 +683,PortCity_Bridge_UnitW12Out +684,PortCity_Bridge_UnitW12 +687,DarkTemplarBlink +690,BattlecruiserAttack +692,BattlecruiserMove +694,BattlecruiserStop +696,AmorphousArmorcloud +697,SpawnLocustsTargeted +698,ViperParasiticBombRelay +699,ParasiticBombRelayDodge +700,VoidRaySwarmDamageBoostCancel +704,ChannelSnipe +707,DarkShrineResearch +708,LurkerDenMPResearch +709,ObserverSiegeMorphtoObserver +710,ObserverMorphtoObserverSiege +711,OverseerMorphtoOverseerSiegeMode +712,OverseerSiegeModeMorphtoOverseer +713,RavenScramblerMissile +715,RavenRepairDroneHeal +716,RavenShredderMissile +717,ChronoBoostEnergyCost +718,NexusMassRecall diff --git a/sc2reader/data/LotV/77379_units.csv b/sc2reader/data/LotV/77379_units.csv new file mode 100644 index 00000000..51c81189 --- /dev/null +++ b/sc2reader/data/LotV/77379_units.csv @@ -0,0 +1,1038 @@ +3,System_Snapshot_Dummy +21,Ball +22,StereoscopicOptionsUnit +23,Colossus +24,TechLab +25,Reactor +27,InfestorTerran +28,BanelingCocoon +29,Baneling +30,Mothership +31,PointDefenseDrone +32,Changeling +33,ChangelingZealot +34,ChangelingMarineShield +35,ChangelingMarine +36,ChangelingZerglingWings +37,ChangelingZergling +39,CommandCenter +40,SupplyDepot +41,Refinery +42,Barracks +43,EngineeringBay +44,MissileTurret +45,Bunker +46,RefineryRich +47,SensorTower +48,GhostAcademy +49,Factory +50,Starport +52,Armory +53,FusionCore +54,AutoTurret +55,SiegeTankSieged +56,SiegeTank +57,VikingAssault +58,VikingFighter +59,CommandCenterFlying +60,BarracksTechLab +61,BarracksReactor +62,FactoryTechLab +63,FactoryReactor +64,StarportTechLab +65,StarportReactor +66,FactoryFlying +67,StarportFlying +68,SCV +69,BarracksFlying +70,SupplyDepotLowered +71,Marine +72,Reaper +73,Ghost +74,Marauder +75,Thor +76,Hellion +77,Medivac +78,Banshee +79,Raven +80,Battlecruiser +81,Nuke +82,Nexus +83,Pylon +84,Assimilator +85,Gateway +86,Forge +87,FleetBeacon +88,TwilightCouncil +89,PhotonCannon +90,Stargate +91,TemplarArchive +92,DarkShrine +93,RoboticsBay +94,RoboticsFacility +95,CyberneticsCore +96,Zealot +97,Stalker +98,HighTemplar +99,DarkTemplar +100,Sentry +101,Phoenix +102,Carrier +103,VoidRay +104,WarpPrism +105,Observer +106,Immortal +107,Probe +108,Interceptor +109,Hatchery +110,CreepTumor +111,Extractor +112,SpawningPool +113,EvolutionChamber +114,HydraliskDen +115,Spire +116,UltraliskCavern +117,InfestationPit +118,NydusNetwork +119,BanelingNest +120,RoachWarren +121,SpineCrawler +122,SporeCrawler +123,Lair +124,Hive +125,GreaterSpire +126,Egg +127,Drone +128,Zergling +129,Overlord +130,Hydralisk +131,Mutalisk +132,Ultralisk +133,Roach +134,Infestor +135,Corruptor +136,BroodLordCocoon +137,BroodLord +138,BanelingBurrowed +139,DroneBurrowed +140,HydraliskBurrowed +141,RoachBurrowed +142,ZerglingBurrowed +143,InfestorTerranBurrowed +144,RedstoneLavaCritterBurrowed +145,RedstoneLavaCritterInjuredBurrowed +146,RedstoneLavaCritter +147,RedstoneLavaCritterInjured +148,QueenBurrowed +149,Queen +150,InfestorBurrowed +151,OverlordCocoon +152,Overseer +153,PlanetaryFortress +154,UltraliskBurrowed +155,OrbitalCommand +156,WarpGate +157,OrbitalCommandFlying +158,ForceField +159,WarpPrismPhasing +160,CreepTumorBurrowed +161,CreepTumorQueen +162,SpineCrawlerUprooted +163,SporeCrawlerUprooted +164,Archon +165,NydusCanal +166,BroodlingEscort +167,GhostAlternate +168,GhostNova +169,RichMineralField +170,RichMineralField750 +171,Ursadon +173,LurkerMPBurrowed +174,LurkerMP +175,LurkerDenMP +176,LurkerMPEgg +177,NydusCanalAttacker +178,OverlordTransport +179,Ravager +180,RavagerBurrowed +181,RavagerCocoon +182,TransportOverlordCocoon +183,XelNagaTower +185,Oracle +186,Tempest +188,InfestedTerransEgg +189,Larva +190,OverseerSiegeMode +192,ReaperPlaceholder +193,MarineACGluescreenDummy +194,FirebatACGluescreenDummy +195,MedicACGluescreenDummy +196,MarauderACGluescreenDummy +197,VultureACGluescreenDummy +198,SiegeTankACGluescreenDummy +199,VikingACGluescreenDummy +200,BansheeACGluescreenDummy +201,BattlecruiserACGluescreenDummy +202,OrbitalCommandACGluescreenDummy +203,BunkerACGluescreenDummy +204,BunkerUpgradedACGluescreenDummy +205,MissileTurretACGluescreenDummy +206,HellbatACGluescreenDummy +207,GoliathACGluescreenDummy +208,CycloneACGluescreenDummy +209,WraithACGluescreenDummy +210,ScienceVesselACGluescreenDummy +211,HerculesACGluescreenDummy +212,ThorACGluescreenDummy +213,PerditionTurretACGluescreenDummy +214,FlamingBettyACGluescreenDummy +215,DevastationTurretACGluescreenDummy +216,BlasterBillyACGluescreenDummy +217,SpinningDizzyACGluescreenDummy +218,ZerglingKerriganACGluescreenDummy +219,RaptorACGluescreenDummy +220,QueenCoopACGluescreenDummy +221,HydraliskACGluescreenDummy +222,HydraliskLurkerACGluescreenDummy +223,MutaliskBroodlordACGluescreenDummy +224,BroodLordACGluescreenDummy +225,UltraliskACGluescreenDummy +226,TorrasqueACGluescreenDummy +227,OverseerACGluescreenDummy +228,LurkerACGluescreenDummy +229,SpineCrawlerACGluescreenDummy +230,SporeCrawlerACGluescreenDummy +231,NydusNetworkACGluescreenDummy +232,OmegaNetworkACGluescreenDummy +233,ZerglingZagaraACGluescreenDummy +234,SwarmlingACGluescreenDummy +235,QueenZagaraACGluescreenDummy +236,BanelingACGluescreenDummy +237,SplitterlingACGluescreenDummy +238,AberrationACGluescreenDummy +239,ScourgeACGluescreenDummy +240,CorruptorACGluescreenDummy +241,OverseerZagaraACGluescreenDummy +242,BileLauncherACGluescreenDummy +243,SwarmQueenACGluescreenDummy +244,RoachACGluescreenDummy +245,RoachVileACGluescreenDummy +246,RavagerACGluescreenDummy +247,SwarmHostACGluescreenDummy +248,MutaliskACGluescreenDummy +249,GuardianACGluescreenDummy +250,DevourerACGluescreenDummy +251,ViperACGluescreenDummy +252,BrutaliskACGluescreenDummy +253,LeviathanACGluescreenDummy +254,ZealotACGluescreenDummy +255,ZealotAiurACGluescreenDummy +256,DragoonACGluescreenDummy +257,HighTemplarACGluescreenDummy +258,ArchonACGluescreenDummy +259,ImmortalACGluescreenDummy +260,ObserverACGluescreenDummy +261,PhoenixAiurACGluescreenDummy +262,ReaverACGluescreenDummy +263,TempestACGluescreenDummy +264,PhotonCannonACGluescreenDummy +265,ZealotVorazunACGluescreenDummy +266,ZealotShakurasACGluescreenDummy +267,StalkerShakurasACGluescreenDummy +268,DarkTemplarShakurasACGluescreenDummy +269,CorsairACGluescreenDummy +270,VoidRayACGluescreenDummy +271,VoidRayShakurasACGluescreenDummy +272,OracleACGluescreenDummy +273,DarkArchonACGluescreenDummy +274,DarkPylonACGluescreenDummy +275,ZealotPurifierACGluescreenDummy +276,SentryPurifierACGluescreenDummy +277,ImmortalKaraxACGluescreenDummy +278,ColossusACGluescreenDummy +279,ColossusPurifierACGluescreenDummy +280,PhoenixPurifierACGluescreenDummy +281,CarrierACGluescreenDummy +282,CarrierAiurACGluescreenDummy +283,KhaydarinMonolithACGluescreenDummy +284,ShieldBatteryACGluescreenDummy +285,EliteMarineACGluescreenDummy +286,MarauderCommandoACGluescreenDummy +287,SpecOpsGhostACGluescreenDummy +288,HellbatRangerACGluescreenDummy +289,StrikeGoliathACGluescreenDummy +290,HeavySiegeTankACGluescreenDummy +291,RaidLiberatorACGluescreenDummy +292,RavenTypeIIACGluescreenDummy +293,CovertBansheeACGluescreenDummy +294,RailgunTurretACGluescreenDummy +295,BlackOpsMissileTurretACGluescreenDummy +296,SupplicantACGluescreenDummy +297,StalkerTaldarimACGluescreenDummy +298,SentryTaldarimACGluescreenDummy +299,HighTemplarTaldarimACGluescreenDummy +300,ImmortalTaldarimACGluescreenDummy +301,ColossusTaldarimACGluescreenDummy +302,WarpPrismTaldarimACGluescreenDummy +303,PhotonCannonTaldarimACGluescreenDummy +304,StukovInfestedCivilianACGluescreenDummy +305,StukovInfestedMarineACGluescreenDummy +306,StukovInfestedSiegeTankACGluescreenDummy +307,StukovInfestedDiamondbackACGluescreenDummy +308,StukovInfestedBansheeACGluescreenDummy +309,SILiberatorACGluescreenDummy +310,StukovInfestedBunkerACGluescreenDummy +311,StukovInfestedMissileTurretACGluescreenDummy +312,StukovBroodQueenACGluescreenDummy +313,ZealotFenixACGluescreenDummy +314,SentryFenixACGluescreenDummy +315,AdeptFenixACGluescreenDummy +316,ImmortalFenixACGluescreenDummy +317,ColossusFenixACGluescreenDummy +318,DisruptorACGluescreenDummy +319,ObserverFenixACGluescreenDummy +320,ScoutACGluescreenDummy +321,CarrierFenixACGluescreenDummy +322,PhotonCannonFenixACGluescreenDummy +323,PrimalZerglingACGluescreenDummy +324,RavasaurACGluescreenDummy +325,PrimalRoachACGluescreenDummy +326,FireRoachACGluescreenDummy +327,PrimalGuardianACGluescreenDummy +328,PrimalHydraliskACGluescreenDummy +329,PrimalMutaliskACGluescreenDummy +330,PrimalImpalerACGluescreenDummy +331,PrimalSwarmHostACGluescreenDummy +332,CreeperHostACGluescreenDummy +333,PrimalUltraliskACGluescreenDummy +334,TyrannozorACGluescreenDummy +335,PrimalWurmACGluescreenDummy +336,HHReaperACGluescreenDummy +337,HHWidowMineACGluescreenDummy +338,HHHellionTankACGluescreenDummy +339,HHWraithACGluescreenDummy +340,HHVikingACGluescreenDummy +341,HHBattlecruiserACGluescreenDummy +342,HHRavenACGluescreenDummy +343,HHBomberPlatformACGluescreenDummy +344,HHMercStarportACGluescreenDummy +345,HHMissileTurretACGluescreenDummy +346,TychusReaperACGluescreenDummy +347,TychusFirebatACGluescreenDummy +348,TychusSpectreACGluescreenDummy +349,TychusMedicACGluescreenDummy +350,TychusMarauderACGluescreenDummy +351,TychusWarhoundACGluescreenDummy +352,TychusHERCACGluescreenDummy +353,TychusGhostACGluescreenDummy +354,TychusSCVAutoTurretACGluescreenDummy +355,ZeratulStalkerACGluescreenDummy +356,ZeratulSentryACGluescreenDummy +357,ZeratulDarkTemplarACGluescreenDummy +358,ZeratulImmortalACGluescreenDummy +359,ZeratulObserverACGluescreenDummy +360,ZeratulDisruptorACGluescreenDummy +361,ZeratulWarpPrismACGluescreenDummy +362,ZeratulPhotonCannonACGluescreenDummy +363,MechaZerglingACGluescreenDummy +364,MechaBanelingACGluescreenDummy +365,MechaHydraliskACGluescreenDummy +366,MechaInfestorACGluescreenDummy +367,MechaCorruptorACGluescreenDummy +368,MechaUltraliskACGluescreenDummy +369,MechaOverseerACGluescreenDummy +370,MechaLurkerACGluescreenDummy +371,MechaBattlecarrierLordACGluescreenDummy +372,MechaSpineCrawlerACGluescreenDummy +373,MechaSporeCrawlerACGluescreenDummy +374,TrooperMengskACGluescreenDummy +375,MedivacMengskACGluescreenDummy +376,BlimpMengskACGluescreenDummy +377,MarauderMengskACGluescreenDummy +378,GhostMengskACGluescreenDummy +379,SiegeTankMengskACGluescreenDummy +380,ThorMengskACGluescreenDummy +381,VikingMengskACGluescreenDummy +382,BattlecruiserMengskACGluescreenDummy +383,BunkerDepotMengskACGluescreenDummy +384,MissileTurretMengskACGluescreenDummy +385,ArtilleryMengskACGluescreenDummy +387,RenegadeLongboltMissileWeapon +388,NeedleSpinesWeapon +389,CorruptionWeapon +390,InfestedTerransWeapon +391,NeuralParasiteWeapon +392,PointDefenseDroneReleaseWeapon +393,HunterSeekerWeapon +394,MULE +396,ThorAAWeapon +397,PunisherGrenadesLMWeapon +398,VikingFighterWeapon +399,ATALaserBatteryLMWeapon +400,ATSLaserBatteryLMWeapon +401,LongboltMissileWeapon +402,D8ChargeWeapon +403,YamatoWeapon +404,IonCannonsWeapon +405,AcidSalivaWeapon +406,SpineCrawlerWeapon +407,SporeCrawlerWeapon +408,GlaiveWurmWeapon +409,GlaiveWurmM2Weapon +410,GlaiveWurmM3Weapon +411,StalkerWeapon +412,EMP2Weapon +413,BacklashRocketsLMWeapon +414,PhotonCannonWeapon +415,ParasiteSporeWeapon +417,Broodling +418,BroodLordBWeapon +421,AutoTurretReleaseWeapon +422,LarvaReleaseMissile +423,AcidSpinesWeapon +424,FrenzyWeapon +425,ContaminateWeapon +437,BeaconArmy +438,BeaconDefend +439,BeaconAttack +440,BeaconHarass +441,BeaconIdle +442,BeaconAuto +443,BeaconDetect +444,BeaconScout +445,BeaconClaim +446,BeaconExpand +447,BeaconRally +448,BeaconCustom1 +449,BeaconCustom2 +450,BeaconCustom3 +451,BeaconCustom4 +456,LiberatorAG +458,PreviewBunkerUpgraded +459,HellionTank +460,Cyclone +461,WidowMine +462,Liberator +464,Adept +465,Disruptor +466,SwarmHostMP +467,Viper +468,ShieldBattery +469,HighTemplarSkinPreview +470,MothershipCore +471,Viking +481,InhibitorZoneSmall +482,InhibitorZoneMedium +483,InhibitorZoneLarge +484,AccelerationZoneSmall +485,AccelerationZoneMedium +486,AccelerationZoneLarge +487,AssimilatorRich +488,RichVespeneGeyser +489,ExtractorRich +490,RavagerCorrosiveBileMissile +491,RavagerWeaponMissile +492,RenegadeMissileTurret +493,Rocks2x2NonConjoined +494,FungalGrowthMissile +495,NeuralParasiteTentacleMissile +496,Beacon_Protoss +497,Beacon_ProtossSmall +498,Beacon_Terran +499,Beacon_TerranSmall +500,Beacon_Zerg +501,Beacon_ZergSmall +502,Lyote +503,CarrionBird +504,KarakMale +505,KarakFemale +506,UrsadakFemaleExotic +507,UrsadakMale +508,UrsadakFemale +509,UrsadakCalf +510,UrsadakMaleExotic +511,UtilityBot +512,CommentatorBot1 +513,CommentatorBot2 +514,CommentatorBot3 +515,CommentatorBot4 +516,Scantipede +517,Dog +518,Sheep +519,Cow +520,InfestedTerransEggPlacement +521,InfestorTerransWeapon +522,MineralField +523,MineralField450 +524,MineralField750 +525,MineralFieldOpaque +526,MineralFieldOpaque900 +527,VespeneGeyser +528,SpacePlatformGeyser +529,DestructibleSearchlight +530,DestructibleBullhornLights +531,DestructibleStreetlight +532,DestructibleSpacePlatformSign +533,DestructibleStoreFrontCityProps +534,DestructibleBillboardTall +535,DestructibleBillboardScrollingText +536,DestructibleSpacePlatformBarrier +537,DestructibleSignsDirectional +538,DestructibleSignsConstruction +539,DestructibleSignsFunny +540,DestructibleSignsIcons +541,DestructibleSignsWarning +542,DestructibleGarage +543,DestructibleGarageLarge +544,DestructibleTrafficSignal +545,TrafficSignal +546,BraxisAlphaDestructible1x1 +547,BraxisAlphaDestructible2x2 +548,DestructibleDebris4x4 +549,DestructibleDebris6x6 +550,DestructibleRock2x4Vertical +551,DestructibleRock2x4Horizontal +552,DestructibleRock2x6Vertical +553,DestructibleRock2x6Horizontal +554,DestructibleRock4x4 +555,DestructibleRock6x6 +556,DestructibleRampDiagonalHugeULBR +557,DestructibleRampDiagonalHugeBLUR +558,DestructibleRampVerticalHuge +559,DestructibleRampHorizontalHuge +560,DestructibleDebrisRampDiagonalHugeULBR +561,DestructibleDebrisRampDiagonalHugeBLUR +562,WarpPrismSkinPreview +563,SiegeTankSkinPreview +564,ThorAP +565,ThorAALance +566,LiberatorSkinPreview +567,OverlordGenerateCreepKeybind +568,MengskStatueAlone +569,MengskStatue +570,WolfStatue +571,GlobeStatue +572,Weapon +573,GlaiveWurmBounceWeapon +574,BroodLordWeapon +575,BroodLordAWeapon +576,CreepBlocker1x1 +577,PermanentCreepBlocker1x1 +578,PathingBlocker1x1 +579,PathingBlocker2x2 +580,AutoTestAttackTargetGround +581,AutoTestAttackTargetAir +582,AutoTestAttacker +583,HelperEmitterSelectionArrow +584,MultiKillObject +585,ShapeGolfball +586,ShapeCone +587,ShapeCube +588,ShapeCylinder +589,ShapeDodecahedron +590,ShapeIcosahedron +591,ShapeOctahedron +592,ShapePyramid +593,ShapeRoundedCube +594,ShapeSphere +595,ShapeTetrahedron +596,ShapeThickTorus +597,ShapeThinTorus +598,ShapeTorus +599,Shape4PointStar +600,Shape5PointStar +601,Shape6PointStar +602,Shape8PointStar +603,ShapeArrowPointer +604,ShapeBowl +605,ShapeBox +606,ShapeCapsule +607,ShapeCrescentMoon +608,ShapeDecahedron +609,ShapeDiamond +610,ShapeFootball +611,ShapeGemstone +612,ShapeHeart +613,ShapeJack +614,ShapePlusSign +615,ShapeShamrock +616,ShapeSpade +617,ShapeTube +618,ShapeEgg +619,ShapeYenSign +620,ShapeX +621,ShapeWatermelon +622,ShapeWonSign +623,ShapeTennisball +624,ShapeStrawberry +625,ShapeSmileyFace +626,ShapeSoccerball +627,ShapeRainbow +628,ShapeSadFace +629,ShapePoundSign +630,ShapePear +631,ShapePineapple +632,ShapeOrange +633,ShapePeanut +634,ShapeO +635,ShapeLemon +636,ShapeMoneyBag +637,ShapeHorseshoe +638,ShapeHockeyStick +639,ShapeHockeyPuck +640,ShapeHand +641,ShapeGolfClub +642,ShapeGrape +643,ShapeEuroSign +644,ShapeDollarSign +645,ShapeBasketball +646,ShapeCarrot +647,ShapeCherry +648,ShapeBaseball +649,ShapeBaseballBat +650,ShapeBanana +651,ShapeApple +652,ShapeCashLarge +653,ShapeCashMedium +654,ShapeCashSmall +655,ShapeFootballColored +656,ShapeLemonSmall +657,ShapeOrangeSmall +658,ShapeTreasureChestOpen +659,ShapeTreasureChestClosed +660,ShapeWatermelonSmall +661,UnbuildableRocksDestructible +662,UnbuildableBricksDestructible +663,UnbuildablePlatesDestructible +664,Debris2x2NonConjoined +665,EnemyPathingBlocker1x1 +666,EnemyPathingBlocker2x2 +667,EnemyPathingBlocker4x4 +668,EnemyPathingBlocker8x8 +669,EnemyPathingBlocker16x16 +670,ScopeTest +671,SentryACGluescreenDummy +672,StukovInfestedTrooperACGluescreenDummy +688,CollapsibleTerranTowerDebris +689,DebrisRampLeft +690,DebrisRampRight +694,LocustMP +695,CollapsibleRockTowerDebris +696,NydusCanalCreeper +697,SwarmHostBurrowedMP +698,WarHound +699,WidowMineBurrowed +700,ExtendingBridgeNEWide8Out +701,ExtendingBridgeNEWide8 +702,ExtendingBridgeNWWide8Out +703,ExtendingBridgeNWWide8 +704,ExtendingBridgeNEWide10Out +705,ExtendingBridgeNEWide10 +706,ExtendingBridgeNWWide10Out +707,ExtendingBridgeNWWide10 +708,ExtendingBridgeNEWide12Out +709,ExtendingBridgeNEWide12 +710,ExtendingBridgeNWWide12Out +711,ExtendingBridgeNWWide12 +713,CollapsibleRockTowerDebrisRampRight +714,CollapsibleRockTowerDebrisRampLeft +715,XelNaga_Caverns_DoorE +716,XelNaga_Caverns_DoorEOpened +717,XelNaga_Caverns_DoorN +718,XelNaga_Caverns_DoorNE +719,XelNaga_Caverns_DoorNEOpened +720,XelNaga_Caverns_DoorNOpened +721,XelNaga_Caverns_DoorNW +722,XelNaga_Caverns_DoorNWOpened +723,XelNaga_Caverns_DoorS +724,XelNaga_Caverns_DoorSE +725,XelNaga_Caverns_DoorSEOpened +726,XelNaga_Caverns_DoorSOpened +727,XelNaga_Caverns_DoorSW +728,XelNaga_Caverns_DoorSWOpened +729,XelNaga_Caverns_DoorW +730,XelNaga_Caverns_DoorWOpened +731,XelNaga_Caverns_Floating_BridgeNE8Out +732,XelNaga_Caverns_Floating_BridgeNE8 +733,XelNaga_Caverns_Floating_BridgeNW8Out +734,XelNaga_Caverns_Floating_BridgeNW8 +735,XelNaga_Caverns_Floating_BridgeNE10Out +736,XelNaga_Caverns_Floating_BridgeNE10 +737,XelNaga_Caverns_Floating_BridgeNW10Out +738,XelNaga_Caverns_Floating_BridgeNW10 +739,XelNaga_Caverns_Floating_BridgeNE12Out +740,XelNaga_Caverns_Floating_BridgeNE12 +741,XelNaga_Caverns_Floating_BridgeNW12Out +742,XelNaga_Caverns_Floating_BridgeNW12 +743,XelNaga_Caverns_Floating_BridgeH8Out +744,XelNaga_Caverns_Floating_BridgeH8 +745,XelNaga_Caverns_Floating_BridgeV8Out +746,XelNaga_Caverns_Floating_BridgeV8 +747,XelNaga_Caverns_Floating_BridgeH10Out +748,XelNaga_Caverns_Floating_BridgeH10 +749,XelNaga_Caverns_Floating_BridgeV10Out +750,XelNaga_Caverns_Floating_BridgeV10 +751,XelNaga_Caverns_Floating_BridgeH12Out +752,XelNaga_Caverns_Floating_BridgeH12 +753,XelNaga_Caverns_Floating_BridgeV12Out +754,XelNaga_Caverns_Floating_BridgeV12 +757,CollapsibleTerranTowerPushUnitRampLeft +758,CollapsibleTerranTowerPushUnitRampRight +761,CollapsibleRockTowerPushUnit +762,CollapsibleTerranTowerPushUnit +763,CollapsibleRockTowerPushUnitRampRight +764,CollapsibleRockTowerPushUnitRampLeft +765,DigesterCreepSprayTargetUnit +766,DigesterCreepSprayUnit +767,NydusCanalAttackerWeapon +768,ViperConsumeStructureWeapon +771,ResourceBlocker +772,TempestWeapon +773,YoinkMissile +777,YoinkVikingAirMissile +779,YoinkVikingGroundMissile +781,YoinkSiegeTankMissile +783,WarHoundWeapon +785,EyeStalkWeapon +788,WidowMineWeapon +789,WidowMineAirWeapon +790,MothershipCoreWeaponWeapon +791,TornadoMissileWeapon +792,TornadoMissileDummyWeapon +793,TalonsMissileWeapon +794,CreepTumorMissile +795,LocustMPEggAMissileWeapon +796,LocustMPEggBMissileWeapon +797,LocustMPWeapon +799,RepulsorCannonWeapon +803,CollapsibleRockTowerDiagonal +804,CollapsibleTerranTowerDiagonal +805,CollapsibleTerranTowerRampLeft +806,CollapsibleTerranTowerRampRight +807,Ice2x2NonConjoined +808,IceProtossCrates +809,ProtossCrates +810,TowerMine +811,PickupPalletGas +812,PickupPalletMinerals +813,PickupScrapSalvage1x1 +814,PickupScrapSalvage2x2 +815,PickupScrapSalvage3x3 +816,RoughTerrain +817,UnbuildableBricksSmallUnit +818,UnbuildablePlatesSmallUnit +819,UnbuildablePlatesUnit +820,UnbuildableRocksSmallUnit +821,XelNagaHealingShrine +822,InvisibleTargetDummy +823,ProtossVespeneGeyser +824,CollapsibleRockTower +825,CollapsibleTerranTower +826,ThornLizard +827,CleaningBot +828,DestructibleRock6x6Weak +829,ProtossSnakeSegmentDemo +830,PhysicsCapsule +831,PhysicsCube +832,PhysicsCylinder +833,PhysicsKnot +834,PhysicsL +835,PhysicsPrimitives +836,PhysicsSphere +837,PhysicsStar +838,CreepBlocker4x4 +839,DestructibleCityDebris2x4Vertical +840,DestructibleCityDebris2x4Horizontal +841,DestructibleCityDebris2x6Vertical +842,DestructibleCityDebris2x6Horizontal +843,DestructibleCityDebris4x4 +844,DestructibleCityDebris6x6 +845,DestructibleCityDebrisHugeDiagonalBLUR +846,DestructibleCityDebrisHugeDiagonalULBR +847,TestZerg +848,PathingBlockerRadius1 +849,DestructibleRockEx12x4Vertical +850,DestructibleRockEx12x4Horizontal +851,DestructibleRockEx12x6Vertical +852,DestructibleRockEx12x6Horizontal +853,DestructibleRockEx14x4 +854,DestructibleRockEx16x6 +855,DestructibleRockEx1DiagonalHugeULBR +856,DestructibleRockEx1DiagonalHugeBLUR +857,DestructibleRockEx1VerticalHuge +858,DestructibleRockEx1HorizontalHuge +859,DestructibleIce2x4Vertical +860,DestructibleIce2x4Horizontal +861,DestructibleIce2x6Vertical +862,DestructibleIce2x6Horizontal +863,DestructibleIce4x4 +864,DestructibleIce6x6 +865,DestructibleIceDiagonalHugeULBR +866,DestructibleIceDiagonalHugeBLUR +867,DestructibleIceVerticalHuge +868,DestructibleIceHorizontalHuge +869,DesertPlanetSearchlight +870,DesertPlanetStreetlight +871,UnbuildableBricksUnit +872,UnbuildableRocksUnit +873,ZerusDestructibleArch +874,Artosilope +875,Anteplott +876,LabBot +877,Crabeetle +878,CollapsibleRockTowerRampRight +879,CollapsibleRockTowerRampLeft +880,LabMineralField +881,LabMineralField750 +896,CollapsibleRockTowerDebrisRampLeftGreen +897,CollapsibleRockTowerDebrisRampRightGreen +898,SnowRefinery_Terran_ExtendingBridgeNEShort8Out +899,SnowRefinery_Terran_ExtendingBridgeNEShort8 +900,SnowRefinery_Terran_ExtendingBridgeNWShort8Out +901,SnowRefinery_Terran_ExtendingBridgeNWShort8 +906,Tarsonis_DoorN +907,Tarsonis_DoorNLowered +908,Tarsonis_DoorNE +909,Tarsonis_DoorNELowered +910,Tarsonis_DoorE +911,Tarsonis_DoorELowered +912,Tarsonis_DoorNW +913,Tarsonis_DoorNWLowered +914,CompoundMansion_DoorN +915,CompoundMansion_DoorNLowered +916,CompoundMansion_DoorNE +917,CompoundMansion_DoorNELowered +918,CompoundMansion_DoorE +919,CompoundMansion_DoorELowered +920,CompoundMansion_DoorNW +921,CompoundMansion_DoorNWLowered +923,LocustMPFlying +924,AiurLightBridgeNE8Out +925,AiurLightBridgeNE8 +926,AiurLightBridgeNE10Out +927,AiurLightBridgeNE10 +928,AiurLightBridgeNE12Out +929,AiurLightBridgeNE12 +930,AiurLightBridgeNW8Out +931,AiurLightBridgeNW8 +932,AiurLightBridgeNW10Out +933,AiurLightBridgeNW10 +934,AiurLightBridgeNW12Out +935,AiurLightBridgeNW12 +936,AiurTempleBridgeNE8Out +938,AiurTempleBridgeNE10Out +940,AiurTempleBridgeNE12Out +942,AiurTempleBridgeNW8Out +944,AiurTempleBridgeNW10Out +946,AiurTempleBridgeNW12Out +948,ShakurasLightBridgeNE8Out +949,ShakurasLightBridgeNE8 +950,ShakurasLightBridgeNE10Out +951,ShakurasLightBridgeNE10 +952,ShakurasLightBridgeNE12Out +953,ShakurasLightBridgeNE12 +954,ShakurasLightBridgeNW8Out +955,ShakurasLightBridgeNW8 +956,ShakurasLightBridgeNW10Out +957,ShakurasLightBridgeNW10 +958,ShakurasLightBridgeNW12Out +959,ShakurasLightBridgeNW12 +960,VoidMPImmortalReviveCorpse +961,GuardianCocoonMP +962,GuardianMP +963,DevourerCocoonMP +964,DevourerMP +965,DefilerMPBurrowed +966,DefilerMP +967,OracleStasisTrap +968,DisruptorPhased +969,AiurLightBridgeAbandonedNE8Out +970,AiurLightBridgeAbandonedNE8 +971,AiurLightBridgeAbandonedNE10Out +972,AiurLightBridgeAbandonedNE10 +973,AiurLightBridgeAbandonedNE12Out +974,AiurLightBridgeAbandonedNE12 +975,AiurLightBridgeAbandonedNW8Out +976,AiurLightBridgeAbandonedNW8 +977,AiurLightBridgeAbandonedNW10Out +978,AiurLightBridgeAbandonedNW10 +979,AiurLightBridgeAbandonedNW12Out +980,AiurLightBridgeAbandonedNW12 +981,CollapsiblePurifierTowerDebris +982,PortCity_Bridge_UnitNE8Out +983,PortCity_Bridge_UnitNE8 +984,PortCity_Bridge_UnitSE8Out +985,PortCity_Bridge_UnitSE8 +986,PortCity_Bridge_UnitNW8Out +987,PortCity_Bridge_UnitNW8 +988,PortCity_Bridge_UnitSW8Out +989,PortCity_Bridge_UnitSW8 +990,PortCity_Bridge_UnitNE10Out +991,PortCity_Bridge_UnitNE10 +992,PortCity_Bridge_UnitSE10Out +993,PortCity_Bridge_UnitSE10 +994,PortCity_Bridge_UnitNW10Out +995,PortCity_Bridge_UnitNW10 +996,PortCity_Bridge_UnitSW10Out +997,PortCity_Bridge_UnitSW10 +998,PortCity_Bridge_UnitNE12Out +999,PortCity_Bridge_UnitNE12 +1000,PortCity_Bridge_UnitSE12Out +1001,PortCity_Bridge_UnitSE12 +1002,PortCity_Bridge_UnitNW12Out +1003,PortCity_Bridge_UnitNW12 +1004,PortCity_Bridge_UnitSW12Out +1005,PortCity_Bridge_UnitSW12 +1006,PortCity_Bridge_UnitN8Out +1007,PortCity_Bridge_UnitN8 +1008,PortCity_Bridge_UnitS8Out +1009,PortCity_Bridge_UnitS8 +1010,PortCity_Bridge_UnitE8Out +1011,PortCity_Bridge_UnitE8 +1012,PortCity_Bridge_UnitW8Out +1013,PortCity_Bridge_UnitW8 +1014,PortCity_Bridge_UnitN10Out +1015,PortCity_Bridge_UnitN10 +1016,PortCity_Bridge_UnitS10Out +1017,PortCity_Bridge_UnitS10 +1018,PortCity_Bridge_UnitE10Out +1019,PortCity_Bridge_UnitE10 +1020,PortCity_Bridge_UnitW10Out +1021,PortCity_Bridge_UnitW10 +1022,PortCity_Bridge_UnitN12Out +1023,PortCity_Bridge_UnitN12 +1024,PortCity_Bridge_UnitS12Out +1025,PortCity_Bridge_UnitS12 +1026,PortCity_Bridge_UnitE12Out +1027,PortCity_Bridge_UnitE12 +1028,PortCity_Bridge_UnitW12Out +1029,PortCity_Bridge_UnitW12 +1030,PurifierRichMineralField +1031,PurifierRichMineralField750 +1032,CollapsibleRockTowerPushUnitRampLeftGreen +1033,CollapsibleRockTowerPushUnitRampRightGreen +1048,CollapsiblePurifierTowerPushUnit +1050,LocustMPPrecursor +1051,ReleaseInterceptorsBeacon +1052,AdeptPhaseShift +1053,HydraliskImpaleMissile +1054,CycloneMissileLargeAir +1055,CycloneMissile +1056,CycloneMissileLarge +1057,OracleWeapon +1058,TempestWeaponGround +1059,ScoutMPAirWeaponLeft +1060,ScoutMPAirWeaponRight +1061,ArbiterMPWeaponMissile +1062,GuardianMPWeapon +1063,DevourerMPWeaponMissile +1064,DefilerMPDarkSwarmWeapon +1065,QueenMPEnsnareMissile +1066,QueenMPSpawnBroodlingsMissile +1067,LightningBombWeapon +1068,HERCPlacement +1069,GrappleWeapon +1072,CausticSprayMissile +1073,ParasiticBombMissile +1074,ParasiticBombDummy +1075,AdeptWeapon +1076,AdeptUpgradeWeapon +1077,LiberatorMissile +1078,LiberatorDamageMissile +1079,LiberatorAGMissile +1080,KD8Charge +1081,KD8ChargeWeapon +1083,SlaynElementalGrabWeapon +1084,SlaynElementalGrabAirUnit +1085,SlaynElementalGrabGroundUnit +1086,SlaynElementalWeapon +1091,CollapsibleRockTowerRampLeftGreen +1092,CollapsibleRockTowerRampRightGreen +1093,DestructibleExpeditionGate6x6 +1094,DestructibleZergInfestation3x3 +1095,HERC +1096,Moopy +1097,Replicant +1098,SeekerMissile +1099,AiurTempleBridgeDestructibleNE8Out +1100,AiurTempleBridgeDestructibleNE10Out +1101,AiurTempleBridgeDestructibleNE12Out +1102,AiurTempleBridgeDestructibleNW8Out +1103,AiurTempleBridgeDestructibleNW10Out +1104,AiurTempleBridgeDestructibleNW12Out +1105,AiurTempleBridgeDestructibleSW8Out +1106,AiurTempleBridgeDestructibleSW10Out +1107,AiurTempleBridgeDestructibleSW12Out +1108,AiurTempleBridgeDestructibleSE8Out +1109,AiurTempleBridgeDestructibleSE10Out +1110,AiurTempleBridgeDestructibleSE12Out +1112,FlyoverUnit +1113,CorsairMP +1114,ScoutMP +1116,ArbiterMP +1117,ScourgeMP +1118,DefilerMPPlagueWeapon +1119,QueenMP +1120,XelNagaDestructibleRampBlocker6S +1121,XelNagaDestructibleRampBlocker6SE +1122,XelNagaDestructibleRampBlocker6E +1123,XelNagaDestructibleRampBlocker6NE +1124,XelNagaDestructibleRampBlocker6N +1125,XelNagaDestructibleRampBlocker6NW +1126,XelNagaDestructibleRampBlocker6W +1127,XelNagaDestructibleRampBlocker6SW +1128,XelNagaDestructibleRampBlocker8S +1129,XelNagaDestructibleRampBlocker8SE +1130,XelNagaDestructibleRampBlocker8E +1131,XelNagaDestructibleRampBlocker8NE +1132,XelNagaDestructibleRampBlocker8N +1133,XelNagaDestructibleRampBlocker8NW +1134,XelNagaDestructibleRampBlocker8W +1135,XelNagaDestructibleRampBlocker8SW +1136,XelNagaDestructibleBlocker6S +1137,XelNagaDestructibleBlocker6SE +1138,XelNagaDestructibleBlocker6E +1139,XelNagaDestructibleBlocker6NE +1140,XelNagaDestructibleBlocker6N +1141,XelNagaDestructibleBlocker6NW +1142,XelNagaDestructibleBlocker6W +1143,XelNagaDestructibleBlocker6SW +1144,XelNagaDestructibleBlocker8S +1145,XelNagaDestructibleBlocker8SE +1146,XelNagaDestructibleBlocker8E +1147,XelNagaDestructibleBlocker8NE +1148,XelNagaDestructibleBlocker8N +1149,XelNagaDestructibleBlocker8NW +1150,XelNagaDestructibleBlocker8W +1151,XelNagaDestructibleBlocker8SW +1152,ReptileCrate +1153,SlaynSwarmHostSpawnFlyer +1154,SlaynElemental +1155,PurifierVespeneGeyser +1156,ShakurasVespeneGeyser +1157,CollapsiblePurifierTowerDiagonal +1158,CreepOnlyBlocker4x4 +1159,BattleStationMineralField +1160,BattleStationMineralField750 +1161,PurifierMineralField +1162,PurifierMineralField750 +1163,Beacon_Nova +1164,Beacon_NovaSmall +1165,Ursula +1166,Elsecaro_Colonist_Hut +1167,SnowGlazeStarterMP +1168,PylonOvercharged +1169,ObserverSiegeMode +1170,RavenRepairDrone +1172,ParasiticBombRelayDummy +1173,BypassArmorDrone +1174,AdeptPiercingWeapon +1175,HighTemplarWeaponMissile +1176,CycloneMissileLargeAirAlternative +1177,RavenScramblerMissile +1178,RavenRepairDroneReleaseWeapon +1179,RavenShredderMissileWeapon +1180,InfestedAcidSpinesWeapon +1181,InfestorEnsnareAttackMissile +1182,SNARE_PLACEHOLDER +1185,CorrosiveParasiteWeapon diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index f8340c2b..6f6fa1a7 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -479,6 +479,7 @@ def load_build(expansion, version): "59587", "70154", "76114", + "77379", ): lotv_builds[version] = load_build("LotV", version) diff --git a/sc2reader/data/ability_lookup.csv b/sc2reader/data/ability_lookup.csv index 5cfe9440..9d0d68fb 100755 --- a/sc2reader/data/ability_lookup.csv +++ b/sc2reader/data/ability_lookup.csv @@ -137,7 +137,7 @@ ForceField,ForceField,CancelForceField,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ForgeResearch,UpgradeGroundWeapons1,UpgradeGroundWeapons2,UpgradeGroundWeapons3,UpgradeGroundArmor1,UpgradeGroundArmor2,UpgradeGroundArmor3,UpgradeShields1,UpgradeShields2,UpgradesShields3,,,,,,,,,,,,,,,,,,,,,,CancelForgeResearch, Frenzy,Frenzy,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, FungalGrowth,FungalGrowth,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -FusionCoreResearch,ResearchWeaponRefit,ResearchBehemothReactor,,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelFusionCoreResearch, +FusionCoreResearch,ResearchWeaponRefit,ResearchBehemothReactor,ResearchMedivacIncreaseSpeedBoost,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelFusionCoreResearch, GatewayTrain,TrainZealot,TrainStalker,,TrainHighTemplar,TrainDarkTemplar,TrainSentry,TrainAdept,,,,,,,,,,,,,,,,,,,,,,,,CancelGatewayTrain, GenerateCreep,GenerateCreep,StopGenerateCreep,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, GhostAcademyResearch,ResearchPersonalCloaking,ResearchMoebiusReactor,ResearchEnhancedShockwaves,,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelGhostAcademyResearch, @@ -161,7 +161,7 @@ HangarQueue5,CancelLast,CancelSlot,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, HerdInteract,Herd,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, HoldFire,Stop,HoldFire,Cheer,Dance,,,,,,,,,,,,,,,,,,,,,,,,,,,, HydraliskDenResearch,ResearchEvolveGroovedSpines,ResearchEvolveMuscularAugments,EvolveGroovedSpines,EvolveMuscularAugments,,,,,,,,,,,,,,,,,,,,,,,,,,,CancelHydraliskDenResearch, -InfestationPitResearch,,,EvolvePathogenGlands,EvolveNeuralParasite,EvolveEnduringLocusts,,,,,,,,,,,,,,,,,,,,,,,,,,CancelInfestationPitResearch, +InfestationPitResearch,,,EvolvePathogenGlands,EvolveNeuralParasite,EvolveEnduringLocusts,ResearchMicrobialShroud,,,,,,,,,,,,,,,,,,,,,,,,,CancelInfestationPitResearch, InfestedTerrans,SpawnInfestedTerran,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, InfestedTerransLayEgg,SpawnInfestedTerran,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, InvulnerabilityShield,InvulnerabilityShield,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, @@ -864,3 +864,4 @@ ViperParasiticBombRelay,ViperParasiticBombRelay BattlecruiserStop,Stop BattlecruiserAttack,BattlecruiserAttack BattlecruiserMove,Move,Patrol,HoldPos +AmorphousArmorcloud,AmorphousArmorcloud diff --git a/sc2reader/data/unit_lookup.csv b/sc2reader/data/unit_lookup.csv index cb76e291..2912f77d 100755 --- a/sc2reader/data/unit_lookup.csv +++ b/sc2reader/data/unit_lookup.csv @@ -1047,3 +1047,18 @@ CollapsibleRockTowerPushUnitRampLeftGreen,CollapsibleRockTowerPushUnitRampLeftGr CollapsibleRockTowerPushUnitRampRightGreen,CollapsibleRockTowerPushUnitRampRightGreen CollapsibleRockTowerRampLeftGreen,CollapsibleRockTowerRampLeftGreen CollapsibleRockTowerRampRightGreen,CollapsibleRockTowerRampRightGreen +TrooperMengskACGluescreenDummy,TrooperMengskACGluescreenDummy +MedivacMengskACGluescreenDummy,MedivacMengskACGluescreenDummy +BlimpMengskACGluescreenDummy,BlimpMengskACGluescreenDummy +MarauderMengskACGluescreenDummy,MarauderMengskACGluescreenDummy +GhostMengskACGluescreenDummy,GhostMengskACGluescreenDummy +SiegeTankMengskACGluescreenDummy,SiegeTankMengskACGluescreenDummy +ThorMengskACGluescreenDummy,ThorMengskACGluescreenDummy +VikingMengskACGluescreenDummy,VikingMengskACGluescreenDummy +BattlecruiserMengskACGluescreenDummy,BattlecruiserMengskACGluescreenDummy +BunkerDepotMengskACGluescreenDummy,BunkerDepotMengskACGluescreenDummy +MissileTurretMengskACGluescreenDummy,MissileTurretMengskACGluescreenDummy +ArtilleryMengskACGluescreenDummy,ArtilleryMengskACGluescreenDummy +AccelerationZoneSmall,AccelerationZoneSmall +AccelerationZoneMedium,AccelerationZoneMedium +AccelerationZoneLarge,AccelerationZoneLarge diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 4898be33..dfcf9d75 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -861,7 +861,11 @@ def register_default_datapacks(self): ) self.register_datapack( datapacks["LotV"]["76114"], - lambda r: r.expansion == "LotV" and 76114 <= r.build, + lambda r: r.expansion == "LotV" and 76114 <= r.build < 77379, + ) + self.register_datapack( + datapacks["LotV"]["77379"], + lambda r: r.expansion == "LotV" and 77379 <= r.build, ) # Internal Methods From a31452715acb9fae1739703631f7abbc7afec30d Mon Sep 17 00:00:00 2001 From: Talv Date: Wed, 27 Nov 2019 11:58:48 +0100 Subject: [PATCH 035/136] Add support for protocol 77379 (v4.11.0) Based on https://github.com/Blizzard/s2protocol/commit/6c80b58ea7f286aad807ee975c44ed81f5c84e73 --- sc2reader/data/attributes.json | 1 + sc2reader/readers.py | 22 +++++++++++++++++- .../4.11.0.77379/Oblivion Express.SC2Replay | Bin 0 -> 38912 bytes test_replays/test_replays.py | 7 ++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test_replays/4.11.0.77379/Oblivion Express.SC2Replay diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index e0d541bf..53563b8e 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -708,6 +708,7 @@ "Horn": "Horner", "Kara": "Karax", "Kerr": "Kerrigan", + "Meng": "Mengsk", "Nova": "Nova", "Rayn": "Raynor", "Stet": "Stetmann", diff --git a/sc2reader/readers.py b/sc2reader/readers.py index a61a5934..c7bac539 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -210,7 +210,13 @@ def __call__(self, data, replay): licenses=[ data.read_uint32() for i in range( - data.read_bits(13 if replay.base_build >= 70154 else 9) + data.read_bits( + 16 + if replay.base_build >= 77379 + else 13 + if replay.base_build >= 70154 + else 9 + ) ) ] if replay.base_build >= 19132 @@ -250,6 +256,20 @@ def __call__(self, data, replay): ] if replay.base_build >= 47185 else None, + brutal_plus_difficulty=data.read_uint32() + if replay.base_build >= 77379 + else None, + retry_mutation_indexes=[ + data.read_uint32() for i in range(data.read_bits(3)) + ] + if replay.base_build >= 77379 + else None, + ac_enemy_race=data.read_uint32() + if replay.base_build >= 77379 + else None, + ac_enemy_wave_type=data.read_uint32() + if replay.base_build >= 77379 + else None, ) for i in range(data.read_bits(5)) ], diff --git a/test_replays/4.11.0.77379/Oblivion Express.SC2Replay b/test_replays/4.11.0.77379/Oblivion Express.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..c1646f4d5b3c032e7b0d139c5062f04cfe19ed05 GIT binary patch literal 38912 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 literal 0 HcmV?d00001 diff --git a/test_replays/test_replays.py b/test_replays/test_replays.py index 8d779eea..59500f69 100644 --- a/test_replays/test_replays.py +++ b/test_replays/test_replays.py @@ -701,6 +701,13 @@ def test_75689(self): replay = factory.load_replay(replayfilename) self.assertEqual(replay.players[0].trophy_id, 13) + def test_77379(self): + replay = sc2reader.load_replay( + "test_replays/4.11.0.77379/Oblivion Express.SC2Replay" + ) + self.assertEqual(replay.players[0].commander, "Mengsk") + self.assertEqual(replay.players[1].commander, "Stetmann") + def test_anonymous_replay(self): replayfilename = "test_replays/4.1.2.60604/1.SC2Replay" factory = sc2reader.factories.SC2Factory() From c0d7e2d92bad9acedc1552398b2aec705b2e31d0 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Sat, 18 Jan 2020 14:06:52 -0800 Subject: [PATCH 036/136] mark release version 1.5.0 --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 69d3c035..d4342fc3 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ CHANGELOG ============ +1.5.0 - January 18, 2019 +----------------------- +* Add support for protocol 77379 #106 #107 +* Workaround for missing data #102 #104 + 1.4.0 - August 19, 2019 ----------------------- * Add support for protocol 75689 #95 diff --git a/setup.py b/setup.py index 90c1d629..da473e7e 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version="1.4.0", + version="1.5.0", keywords=["starcraft 2", "sc2", "replay", "parser"], description="Utility for parsing Starcraft II replay files", long_description=open("README.rst").read() + "\n\n" + open("CHANGELOG.rst").read(), From 9fab19c1299c4254cc8fafc663996f078f7dc073 Mon Sep 17 00:00:00 2001 From: Kevin Leung Date: Sat, 18 Jan 2020 14:18:20 -0800 Subject: [PATCH 037/136] fix rst in 1.5.0 --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d4342fc3..49419793 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,8 @@ CHANGELOG ============ -1.5.0 - January 18, 2019 ------------------------ +1.5.0 - January 18, 2020 +------------------------ * Add support for protocol 77379 #106 #107 * Workaround for missing data #102 #104 From a8a2a70977e5c8cac3d2857a9446624896247ad8 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:24:18 +0100 Subject: [PATCH 038/136] CircleCI: Look for typos with codespell --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8e96bb5b..c6752cf4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,8 +15,9 @@ jobs: - image: circleci/python:3.8 steps: - checkout - - run: sudo pip install flake8 black + - run: sudo pip install black codespell flake8 - run: python --version ; pip --version ; pwd ; ls -l + - run: codespell # stop the build if there are Python syntax errors or undefined names - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide From 142a91e3ebd63f2c2f55e113f3c5d86c37813937 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:30:36 +0100 Subject: [PATCH 039/136] codespell -L queenland,uint --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c6752cf4..546b7a7b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ jobs: - checkout - run: sudo pip install black codespell flake8 - run: python --version ; pip --version ; pwd ; ls -l - - run: codespell + - run: codespell -L queenland,uint # stop the build if there are Python syntax errors or undefined names - run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide From 2356efb1b90dfe944ec6946a680481a9e5944a21 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:34:16 +0100 Subject: [PATCH 040/136] Typo: Properly --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 49419793..a6526d8c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -159,7 +159,7 @@ Changed Stuff (non-backwards compatible!): -------------------- * Fixes several game event parsing issues for older replays. -* Propperly maps ability ids for armory vehicle & ship armor upgrades. +* Properly maps ability ids for armory vehicle & ship armor upgrades. * Uses the US depot for SEA battle.net depot dependencies. * ``PlayerStatEvent.food_used`` and ``food_made`` are now properly divided by 4096 * ``AbilityEvent.flags`` are now processed into a dictionary mapping flag name to True/False (``AbilityEvent.flag``) From f787abcacadc4fd2d0e244bc24782fce754553eb Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:37:14 +0100 Subject: [PATCH 041/136] Fix typos found by codespell --- examples/sc2autosave.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/sc2autosave.py b/examples/sc2autosave.py index 74bb1567..f7091ba4 100755 --- a/examples/sc2autosave.py +++ b/examples/sc2autosave.py @@ -37,7 +37,7 @@ -------------------- The --rename option allows you to specify a renaming format string. The string -is constructed the pythonic (3.0) way with {:field} indicating the substition +is constructed the pythonic (3.0) way with {:field} indicating the substitution of a field. The forward slash (/) is a special character here which terminates a folder name and allows for organization into subdirectories. All other string characters form the template into which the fields are inserted. @@ -138,7 +138,7 @@ files every SECONDS seconds. --rename FORMAT :map - Inserts the map name. - :date - Inserts a string formated datetime object using --date-format. + :date - Inserts a string formatted datetime object using --date-format. :length - Inserts a string formatted time object using --length-format. :teams - Inserts a comma separated player list. Teams are separated with a ' vs ' string. Format the player with --player-format. @@ -602,7 +602,7 @@ def main(): try: run(parser.parse_args()) except KeyboardInterrupt: - print("\n\nScript Interupted. Process Aborting") + print("\n\nScript Interrupted. Process Aborting") if __name__ == "__main__": From 2841052a459ac9a3ddb49e43fbe7e537c3c7396c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:40:33 +0100 Subject: [PATCH 042/136] Fix typos found by codespell --- docs/source/tutorials/prettyprinter.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/tutorials/prettyprinter.rst b/docs/source/tutorials/prettyprinter.rst index a8584620..310e852a 100644 --- a/docs/source/tutorials/prettyprinter.rst +++ b/docs/source/tutorials/prettyprinter.rst @@ -93,7 +93,7 @@ Many of the replay attributes are nested data structures which are generally all Each of these nested structures can be found either on its own reference page or lumped together with the other minor structures on the Misc Structures page. -So now all we need to do is build the ouput using the available replay attributes. Lets start with the header portion. We'll use a block string formatting method that makes this clean and easy: +So now all we need to do is build the output using the available replay attributes. Lets start with the header portion. We'll use a block string formatting method that makes this clean and easy: :: @@ -199,7 +199,7 @@ With this in mind, lets make a slight change to our main function to support any Any time that you start dealing with directories or collections of files you run into dangers with recursion and annoyances of tedium. sc2reader provides options to mitigate these concerns. * directory: Default ''. The directory string when supplied, becomes the base of all the file paths sent into sc2reader and can save you the hassle of fully qualifying your file paths each time. -* depth: Default -1. When handling directory inputs, sc2reader searches the directory recursively until all .SC2Replay files have been loaded. By setting the maxium depth value this behavior can be mitigated. +* depth: Default -1. When handling directory inputs, sc2reader searches the directory recursively until all .SC2Replay files have been loaded. By setting the maximum depth value this behavior can be mitigated. * exclude: Default []. When recursing directories you can choose to exclude directories from the file search by directory name (not full path). * followlinks: Default false. When recursing directories, enables or disables the follow symlinks behavior. From a829efbdc998fd0cf12d4e94f2c8befbc6b78af6 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:42:15 +0100 Subject: [PATCH 043/136] Fix typos found by codespell --- docs/source/articles/whatsinareplay.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/articles/whatsinareplay.rst b/docs/source/articles/whatsinareplay.rst index 5acf6ea4..466f3bf0 100644 --- a/docs/source/articles/whatsinareplay.rst +++ b/docs/source/articles/whatsinareplay.rst @@ -35,7 +35,7 @@ The last file provides a record of important events from the game. * replay.tracker.events - Records important game events and game state updates. -This file was introduced in 2.0.4 and is unncessary for the Starcraft II to reproduce the game. Instead, it records interesting game events and game state for community developers to use when analyzing replays. +This file was introduced in 2.0.4 and is unnecessary for the Starcraft II to reproduce the game. Instead, it records interesting game events and game state for community developers to use when analyzing replays. What isn't in a replay? From 3b4a88c687a815a96258d028463cbee4f755355e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:49:55 +0100 Subject: [PATCH 044/136] Fix typos found by codespell --- sc2reader/objects.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sc2reader/objects.py b/sc2reader/objects.py index a49c3012..5d37f62a 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -18,7 +18,7 @@ class Team(object): objects with some metadata. As such, it implements iterable and can be looped over like a list. - :param interger number: The team number as recorded in the replay + :param integer number: The team number as recorded in the replay """ #: A unique hash identifying the team of players @@ -140,7 +140,7 @@ def __init__(self, sid, slot_data): #: The Battle.net subregion the entity is registered to self.subregion = int(parts[2]) - #: The Battle.net acount identifier. Used to construct the + #: The Battle.net account identifier. Used to construct the #: bnet profile url. This value can be zero for games #: played offline when a user was not logged in to battle.net. self.toon_id = int(parts[3]) @@ -240,7 +240,7 @@ def __init__(self, pid, slot_data, detail_data, attribute_data): #: The Battle.net subregion the entity is registered to self.subregion = detail_data["bnet"]["subregion"] - #: The Battle.net acount identifier. Used to construct the + #: The Battle.net account identifier. Used to construct the #: bnet profile url. This value can be zero for games #: played offline when a user was not logged in to battle.net. self.toon_id = detail_data["bnet"]["uid"] @@ -604,8 +604,8 @@ def __init__(self, contents): #: The map base height (what is that?). This value is 4096*Base Height in the editor (giving a decimal value). self.base_height = data.read_uint32() / 4096 - # Leave early so we dont barf. Turns out ggtracker doesnt need - # any of the map data thats loaded below. + # Leave early so we don't barf. Turns out ggtracker doesn't need + # any of the map data that is loaded below. return #: Load screen type: 0 = default, 1 = custom From 6c5687bf4edd2cc4fc12262c2fe8995a87d7a216 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:51:03 +0100 Subject: [PATCH 045/136] Fix typos found by codespell --- sc2reader/engine/plugins/supply.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/supply.py b/sc2reader/engine/plugins/supply.py index c106f757..8305e50b 100644 --- a/sc2reader/engine/plugins/supply.py +++ b/sc2reader/engine/plugins/supply.py @@ -87,7 +87,7 @@ def remove_from_units_alive(self, event, replay): ) def handleInitGame(self, event, replay): - ## This dictionary contains te supply of every unit + ## This dictionary contains the supply of every unit self.unit_name_to_supply = { # Zerg "Drone": (1, 17), From 8130c5d1c4127bfe6e24c4b6480131cafb7be90c Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:53:15 +0100 Subject: [PATCH 046/136] Fix typos found by codespell --- sc2reader/engine/plugins/creeptracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 98f0148f..db5e6ac8 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -117,7 +117,7 @@ def __init__(self, replay): self.creep_spread_image_by_minute = dict() ## This list contains all the active cgus in every time frame self.creep_gen_units = dict() - ## Thist list corresponds to creep_gen_units storing the time of each CGU + ## This list corresponds to creep_gen_units storing the time of each CGU self.creep_gen_units_times = dict() ## convert all possible cgu radii into a sets of coordinates centred around the origin, ## in order to use this with the CGUs, the centre point will be @@ -168,7 +168,7 @@ def radius_to_map_positions(self, radius): ## this function converts all radius into map coordinates ## centred around the origin that the creep can exist ## the cgu_radius_to_map_position function will simply - ## substract every coordinate with the centre point of the tumour + ## subtract every coordinate with the centre point of the tumour output_coordinates = list() # Sample a square area using the radius for x in range(-radius, radius): From 43160a72b6cbc2b32cf3d6f9686fdff17815c653 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:54:20 +0100 Subject: [PATCH 047/136] Fix typos found by codespell --- sc2reader/engine/plugins/selection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 9f741894..59d4ee02 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -21,7 +21,7 @@ class SelectionTracker(object): control_group_9 = selection[9] active_selection = selection[10] - # TODO: list a few error inducing sitations + # TODO: list a few error inducing situations """ name = "SelectionTracker" From e8c188a04db69978d486a61c8a3d18abac074400 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 04:57:27 +0100 Subject: [PATCH 048/136] Fix typos found by codespell --- sc2reader/events/game.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 9dda98b5..346adb96 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -324,7 +324,7 @@ def __init__(self, frame, pid, data): #: This id can be 0 when the target unit is shrouded by fog of war. self.target_unit_id = self.ability_type_data.get("unit_tag", None) - #: A reference to the targetted unit. When the :attr:`target_unit_id` is + #: A reference to the targeted unit. When the :attr:`target_unit_id` is #: 0 this target unit is a generic, reused fog of war unit of the :attr:`target_unit_type` #: with an id of zero. It should not be confused with a real unit. self.target_unit = None @@ -333,7 +333,7 @@ def __init__(self, frame, pid, data): self.target_unit_type = self.ability_type_data.get("unit_link", None) #: Integer player id of the controlling player. Available for TargetUnit type events starting in 19595. - #: When the targetted unit is under fog of war this id is zero. + #: When the targeted unit is under fog of war this id is zero. self.control_player_id = self.ability_type_data.get("control_player_id", None) #: Integer player id of the player paying upkeep. Available for TargetUnit type events. @@ -374,7 +374,7 @@ class UpdateTargetUnitCommandEvent(TargetUnitCommandEvent): from TargetUnitCommandEvent, but for flexibility, it will be treated differently. - One example of this event occuring is casting inject on a hatchery while + One example of this event occurring is casting inject on a hatchery while holding shift, and then shift clicking on a second hatchery. """ @@ -406,7 +406,7 @@ class SelectionEvent(GameEvent): player is updated. Unlike other game events, these events can also be generated by non-player actions like unit deaths or transformations. - Starting in Starcraft 2.0.0, selection events targetting control group + Starting in Starcraft 2.0.0, selection events targeting control group buffers are also generated when control group selections are modified by non-player actions. When a player action updates a control group a :class:`ControlGroupEvent` is generated. @@ -529,7 +529,7 @@ class ControlGroupEvent(GameEvent): * :class:`GetControlGroup` - Recorded when a user retrieves a control group (#). * :class:`AddToControlGroup` - Recorded when a user adds to a control group (shift+ctrl+#) - All three events have the same set of data (shown below) but are interpretted differently. + All three events have the same set of data (shown below) but are interpreted differently. See the class entry for details. """ From 52c812ac0effb69c553ab015d91f946279c92432 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:00:13 +0100 Subject: [PATCH 049/136] Fix typos found by codespell --- sc2reader/resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sc2reader/resources.py b/sc2reader/resources.py index dfcf9d75..4722cf28 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -73,7 +73,7 @@ class Replay(Resource): #: Deprecated, use :attr:`game_type` or :attr:`real_type` instead type = str() - #: The game type choosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA + #: The game type chosen at game creation: 1v1, 2v2, 3v3, 4v4, FFA game_type = str() #: The real type of the replay as observed by counting players on teams. @@ -1237,7 +1237,7 @@ def use_property(prop, player=None): # Lobby properties can require on player properties. # How does this work? I assume that one player satisfying the - # property requirments is sufficient + # property requirements is sufficient if requirement.is_lobby: values = [setting] else: From 0c3398409d5c2c8fa92be21e99cae4833fa32207 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:01:31 +0100 Subject: [PATCH 050/136] Fix typos found by codespell --- sc2reader/decoders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/decoders.py b/sc2reader/decoders.py index 78372e8f..8520e259 100644 --- a/sc2reader/decoders.py +++ b/sc2reader/decoders.py @@ -20,7 +20,7 @@ class ByteDecoder(object): Used to unpack parse byte aligned files. """ - #: The Bytes object used internaly for reading from the + #: The Bytes object used internally for reading from the #: decoder contents. cStringIO is faster than managing our #: own string access in python. For PyPy installations a #: managed string implementation might be faster. From 80c95c9d8d998e8d89dbd11607ee1a04fa6be7a4 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:02:51 +0100 Subject: [PATCH 051/136] Fix typos found by codespell --- sc2reader/readers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/readers.py b/sc2reader/readers.py index c7bac539..ccae2121 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -1074,7 +1074,7 @@ def control_group_update_event(self, data): def decrement_game_time_remaining_event(self, data): # really this should be set to 19, and a new GameEventsReader_41743 should be introduced that specifies 32 bits. - # but I dont care about ability to read old replays. + # but I don't care about ability to read old replays. return dict(decrement_ms=data.read_bits(32)) From e108f4dfbde80ef6802c6a0216d4738a702da9eb Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:05:32 +0100 Subject: [PATCH 052/136] Fix typos found by codespell --- sc2reader/data/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index 6f6fa1a7..b6ec2d75 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -84,7 +84,7 @@ def __init__(self, unit_id): #: The unique in-game id for this unit. The id can sometimes be zero because #: TargetUnitCommandEvents will create a new unit with id zero when a unit - #: behind the fog of war is targetted. + #: behind the fog of war is targeted. self.id = unit_id #: A reference to the unit type this unit is current in. @@ -301,11 +301,11 @@ class Build(object): """ :param build_id: The build number identifying this dataset. - The datapack for a particualr group of builds. Maps internal integer ids + The datapack for a particular group of builds. Maps internal integer ids to :class:`Unit` and :class:`Ability` types. Also contains builder methods for creating new units and changing their types. - All build data is valid for standard games only. For arcade maps milage + All build data is valid for standard games only. For arcade maps mileage may vary. """ From d416eaa76ec5ac6e1e38a8eebb2a332f5ee0381a Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:06:16 +0100 Subject: [PATCH 053/136] Fix typos found by codespell --- sc2reader/scripts/sc2parse.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 311db343..868f897a 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -27,7 +27,7 @@ def main(): parser = argparse.ArgumentParser( - description="Recursively parses replay files, inteded for debugging parse issues." + description="Recursively parses replay files, intended for debugging parse issues." ) parser.add_argument( "--one_each", From 605d068de6128f4d5d5653fdf730806e1f0a41d0 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:07:57 +0100 Subject: [PATCH 054/136] Fix typos found by codespell --- sc2reader/scripts/sc2attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/scripts/sc2attributes.py b/sc2reader/scripts/sc2attributes.py index 109cf30b..a794e235 100644 --- a/sc2reader/scripts/sc2attributes.py +++ b/sc2reader/scripts/sc2attributes.py @@ -50,7 +50,7 @@ def main(): global decisions parser = argparse.ArgumentParser( - description="Recursively parses replay files, inteded for debugging parse issues." + description="Recursively parses replay files, intended for debugging parse issues." ) parser.add_argument( "folders", metavar="folder", type=str, nargs="+", help="Path to a folder" From 5a526f8a592d7de249166198c26d1ccb8df47589 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Sun, 19 Jan 2020 05:08:56 +0100 Subject: [PATCH 055/136] Fix typos found by codespell --- sc2reader/scripts/sc2replayer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index d8711962..011cffc6 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -31,7 +31,7 @@ def getch(): except ImportError as e: try: - # Opps, we might be on windows, try this one + # Oops, we might be on windows, try this one from msvcrt import getch except ImportError as e: # We can't make getch happen, just dump events to the screen From 6a32c33690eac9c13fa698cb8feb0fb062ea5206 Mon Sep 17 00:00:00 2001 From: Rohit Sanbhadti Date: Sun, 5 Jul 2020 20:20:45 -0700 Subject: [PATCH 056/136] fix toDict in replay plugin --- sc2reader/scripts/sc2json.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sc2reader/scripts/sc2json.py b/sc2reader/scripts/sc2json.py index 7cc2f331..0e35a7a3 100755 --- a/sc2reader/scripts/sc2json.py +++ b/sc2reader/scripts/sc2json.py @@ -34,9 +34,7 @@ def main(): args = parser.parse_args() factory = sc2reader.factories.SC2Factory() - factory.register_plugin( - "Replay", toJSON(encoding=args.encoding, indent=args.indent) - ) + factory.register_plugin("Replay", toJSON(indent=args.indent)) replay_json = factory.load_replay(args.path[0]) print(replay_json) From 1d56cd45d534efe44d112fc3c3786a30dc07a1c7 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 8 Jul 2020 07:27:22 +0200 Subject: [PATCH 057/136] Typo: augment found by Codespell --- docs/source/articles/conceptsinsc2reader.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/articles/conceptsinsc2reader.rst b/docs/source/articles/conceptsinsc2reader.rst index d4467029..8a83704a 100644 --- a/docs/source/articles/conceptsinsc2reader.rst +++ b/docs/source/articles/conceptsinsc2reader.rst @@ -55,7 +55,7 @@ Many attributes in sc2reader are prefixed with ``game_`` and ``real_``. Game ref GameEngine ---------------- -The game engine is used to process replay events and augument the replay with new statistics and game state. It implements a plugin system that allows developers +The game engine is used to process replay events and augment the replay with new statistics and game state. It implements a plugin system that allows developers to inject their own logic into the game loop. It also allows plugins to ``yield`` new events to the event stream. This allows for basic message passing between plugins. From 54b0cc163b42f2396f8cdd2d5d7c45692d273005 Mon Sep 17 00:00:00 2001 From: Rohit Sanbhadti Date: Tue, 7 Jul 2020 22:39:15 -0700 Subject: [PATCH 058/136] Update sc2reader/scripts/sc2json.py Fixed type - Committed suggestion from PR review https://github.com/ggtracker/sc2reader/pull/118 Co-authored-by: Christian Clauss --- sc2reader/scripts/sc2json.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sc2reader/scripts/sc2json.py b/sc2reader/scripts/sc2json.py index 0e35a7a3..c7e842e8 100755 --- a/sc2reader/scripts/sc2json.py +++ b/sc2reader/scripts/sc2json.py @@ -34,7 +34,12 @@ def main(): args = parser.parse_args() factory = sc2reader.factories.SC2Factory() - factory.register_plugin("Replay", toJSON(indent=args.indent)) + try: + factory.register_plugin( + "Replay", toJSON(encoding=args.encoding, indent=args.indent) + ) # legacy Python + except TypeError: + factory.register_plugin("Replay", toJSON(indent=args.indent)) replay_json = factory.load_replay(args.path[0]) print(replay_json) From f0b22eccc97cada64119f939bd870127efe0538e Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 8 Jul 2020 07:58:40 +0200 Subject: [PATCH 059/136] Update CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index aea5dd40..76299e88 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -13,5 +13,6 @@ Contributors: Kevin Leung - @StoicLoofah on github Daniele Zannotti (Durrza) Mike Anderson + Christian Clauss - @cclauss on github Special thanks to ggtracker, inc (ggtracker.com) for sponsoring sc2reader's continued development. From d1a7870691f24189323dd69468353bcd714a8253 Mon Sep 17 00:00:00 2001 From: Talv Date: Thu, 30 Jul 2020 18:41:38 +0200 Subject: [PATCH 060/136] Add support for protocol 80949 (v5.0.0) --- sc2reader/readers.py | 66 +++++++++++++++++- sc2reader/resources.py | 7 +- ...7-28 - (T)Ocrucius VS (Z)Rairden.SC2Replay | Bin 0 -> 153601 bytes test_replays/test_replays.py | 5 ++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 test_replays/5.0.0.80949/2020-07-28 - (T)Ocrucius VS (Z)Rairden.SC2Replay diff --git a/sc2reader/readers.py b/sc2reader/readers.py index ccae2121..97e18074 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -167,7 +167,9 @@ def __call__(self, data, replay): ai_build=data.read_bits(8 if replay.base_build >= 38749 else 7) if replay.base_build >= 23925 else None, - handicap=data.read_bits(7), + handicap=data.read_bits( + 32 if replay.base_build >= 80669 else 7 + ), observe=data.read_bits(2), logo_index=data.read_uint32() if replay.base_build >= 32283 @@ -270,6 +272,9 @@ def __call__(self, data, replay): ac_enemy_wave_type=data.read_uint32() if replay.base_build >= 77379 else None, + selected_commander_prestige=data.read_uint32() + if replay.base_build >= 80871 + else None, ) for i in range(data.read_bits(5)) ], @@ -2196,6 +2201,65 @@ def set_sync_playing(self, data): return dict(sync_load=data.read_uint32()) +class GameEventsReader_80669(GameEventsReader_65895): + # this is almost the same as `command_event` from previous build + # the only addition is introduction of extra command flag: + # > https://news.blizzard.com/en-us/starcraft2/23471116/starcraft-ii-4-13-0-ptr-patch-notes + # > New order command flag: Attack Once + # > When issuing an attack order, it is now allowed to issue an “attack once” order with order command flags. + # > const int c_cmdAttackOnce = 26; + # ideally this part of the code should be more generic so it doesn't have to copy-pasted as a whole + # every time there's a tiny change in one of the sub-structs + def command_event(self, data): + return dict( + flags=data.read_bits(27), + ability=dict( + ability_link=data.read_uint16(), + ability_command_index=data.read_bits(5), + ability_command_data=data.read_uint8() if data.read_bool() else None, + ) + if data.read_bool() + else None, + data={ # Choice + 0: lambda: ("None", None), + 1: lambda: ( + "TargetPoint", + dict( + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ) + ), + ), + 2: lambda: ( + "TargetUnit", + dict( + flags=data.read_uint16(), + timer=data.read_uint8(), + unit_tag=data.read_uint32(), + unit_link=data.read_uint16(), + control_player_id=data.read_bits(4) + if data.read_bool() + else None, + upkeep_player_id=data.read_bits(4) + if data.read_bool() + else None, + point=dict( + x=data.read_bits(20), + y=data.read_bits(20), + z=data.read_uint32() - 2147483648, + ), + ), + ), + 3: lambda: ("Data", dict(data=data.read_uint32())), + }[data.read_bits(2)](), + sequence=data.read_uint32() + 1, + other_unit_tag=data.read_uint32() if data.read_bool() else None, + unit_group=data.read_uint32() if data.read_bool() else None, + ) + + class TrackerEventsReader(object): def __init__(self): self.EVENT_DISPATCH = { diff --git a/sc2reader/resources.py b/sc2reader/resources.py index 4722cf28..4196cabb 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -773,7 +773,12 @@ def register_default_readers(self): self.register_reader( "replay.game.events", readers.GameEventsReader_65895(), - lambda r: 65895 <= r.base_build, + lambda r: 65895 <= r.base_build < 80669, + ) + self.register_reader( + "replay.game.events", + readers.GameEventsReader_80669(), + lambda r: 80669 <= r.base_build, ) self.register_reader( "replay.game.events", diff --git a/test_replays/5.0.0.80949/2020-07-28 - (T)Ocrucius VS (Z)Rairden.SC2Replay b/test_replays/5.0.0.80949/2020-07-28 - (T)Ocrucius VS (Z)Rairden.SC2Replay new file mode 100644 index 0000000000000000000000000000000000000000..a2fbc3ee49fe0b915c64d3abb533561265141ac0 GIT binary patch literal 153601 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