diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 96001a59..dce8cf4c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,8 +1,7 @@ CHANGELOG ============ - -0.6.5 - December ?? 2013 +0.7.0 - --------------------------- * Deprecated unit.killed_by in favor of unit.killing_player @@ -17,6 +16,27 @@ CHANGELOG * Removed the defunct replay.player_names attribute. * Removed the defunct replay.events_by_type attribute. * Removed the defunct replay.other_people attribute. +* Replays can now be pickled and stored for later consumption. +* All references to the gateway attribute have been replaced in favor of region; e.g. replay.region +* Use generic UnitType and Ability classes for data. This means no more unit._type_class.__class__.__name__. But hopefully people were not doing that anyway. +* Now a CorruptTrackerFileError is raised when the tracker file is corrupted (generally only older resume_from_replay replays) +* Added replay.resume_from_replay flag. See replay.resume_user_info for additional info. +* PacketEvent is now ProgressEvent. +* SetToHotkeyEvent is now SetControlGroupEvent. +* AddToHotkeyEvent is now AddToControlGroupEvent. +* GetFromHotkeyEvent is now GetControlGroupEvent. +* PlayerAbilityEvent is no longer part of the event hierarchy. +* AbilityEvent doubled as both an abstract and concrete class (very bad, see #160). Now split into: + * AbilityEvent is now CommandEvent + * AbilityEvent is now BasicCommandEvent +* TargetAbilityEvent is now TargetUnitCommandEvent +* LocationAbilityEvent is now TargetPointCommandEvent +* SelfAbilityEvent is now DataCommandEvent +* Removed the defunct replay.player_names attribute. +* Removed the defunct replay.events_by_type attribute. +* Removed the defunct replay.other_people attribute. + +* event.name is no longer a class property; it can only be accessed from an event instance. * PingEvents now have new attributes: * event.to_all - true if ping seen by all * event.to_allies - true if ping seen by allies @@ -28,7 +48,7 @@ CHANGELOG --------------------------- * Fix bug in code for logging errors. -* Fix siege tank supply count +* Fix siege tank supply count. * Small improvements to message.events parsing. 0.6.3 - September 15th 2013 diff --git a/sc2reader/__init__.py b/sc2reader/__init__.py index 040febcb..8ae84731 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.6.4" +__version__ = "0.7.0-pre" import os import sys diff --git a/sc2reader/constants.py b/sc2reader/constants.py index f06fccf6..aa3e0ade 100644 --- a/sc2reader/constants.py +++ b/sc2reader/constants.py @@ -23,7 +23,7 @@ '프로토스': 'Protoss', '저그': 'Zerg', - # ??eu + # plPL 'Terranie': 'Terran', 'Protosi': 'Protoss', 'Zergi': 'Zerg', @@ -124,7 +124,7 @@ COLOR_CODES_INV = dict(zip(COLOR_CODES.values(), COLOR_CODES.keys())) -REGIONS = { +SUBREGIONS = { # United States 'us': { 1: 'us', @@ -142,6 +142,7 @@ 1: 'kr', 2: 'tw', }, + # Taiwan - appear to both map to same place 'tw': { 1: 'kr', diff --git a/sc2reader/data/__init__.py b/sc2reader/data/__init__.py index f9ddcb6b..b0503ce1 100755 --- a/sc2reader/data/__init__.py +++ b/sc2reader/data/__init__.py @@ -73,7 +73,7 @@ def __init__(self, unit_id): self.killed_units = list() #: The unique in-game id for this unit. The id can sometimes be zero because - #: TargetAbilityEvents will create a new unit with id zero when a unit + #: TargetUnitCommandEvents will create a new unit with id zero when a unit #: behind the fog of war is targetted. self.id = unit_id diff --git a/sc2reader/data/attributes.json b/sc2reader/data/attributes.json index 8265f61b..b0bffe78 100644 --- a/sc2reader/data/attributes.json +++ b/sc2reader/data/attributes.json @@ -1,1562 +1,1562 @@ { "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", + "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" } - ], + ], "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", - "Arta": "Artanis", - "Kara": "Karax", - "Kerr": "Kerrigan", - "Rayn": "Raynor", - "Swan": "Swann", - "Vora": "Vorazun", + "Commander", + { + "": "Pick Commander", + "Arta": "Artanis", + "Kara": "Karax", + "Kerr": "Kerrigan", + "Rayn": "Raynor", + "Swan": "Swann", + "Vora": "Vorazun", "Zaga": "Zagara" } - ], + ], "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" } - ], + ], "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" } ] - }, + }, "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." -} \ No newline at end of file +} diff --git a/sc2reader/data/create_lookup.py b/sc2reader/data/create_lookup.py index 459685f2..73623836 100755 --- a/sc2reader/data/create_lookup.py +++ b/sc2reader/data/create_lookup.py @@ -1,16 +1,14 @@ - - abilities = dict() with open('hots_abilities.csv', 'r') as f: - for line in f: - num, ability = line.strip('\r\n ').split(',') - abilities[ability] = [""]*32 - -with open('command_lookup.csv','r') as f: - for line in f: - ability, commands = line.strip('\r\n ').split('|',1) - abilities[ability] = commands.split('|') - -with open('new_lookup.csv','w') as out: - for ability, commands in sorted(abilities.items()): - out.write(','.join([ability]+commands)+'\n') + for line in f: + num, ability = line.strip('\r\n ').split(',') + abilities[ability] = [""]*32 + +with open('command_lookup.csv', 'r') as f: + for line in f: + ability, commands = line.strip('\r\n ').split('|', 1) + abilities[ability] = commands.split('|') + +with open('new_lookup.csv', 'w') as out: + for ability, commands in sorted(abilities.items()): + out.write(','.join([ability]+commands)+'\n') diff --git a/sc2reader/engine/engine.py b/sc2reader/engine/engine.py index f1909534..65caced1 100644 --- a/sc2reader/engine/engine.py +++ b/sc2reader/engine/engine.py @@ -5,6 +5,7 @@ from sc2reader.events import * from sc2reader.engine.events import InitGameEvent, EndGameEvent, PluginExit + class GameEngine(object): """ GameEngine Specification -------------------------- @@ -17,14 +18,14 @@ class GameEngine(object): Example Usage:: class Plugin1(): - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): pass class Plugin2(): def handleEvent(self, event, replay): pass - def handleTargetAbilityEvent(self, event, replay): + def handleTargetUnitCommandEvent(self, event, replay): pass ... @@ -34,11 +35,11 @@ def handleTargetAbilityEvent(self, event, replay): engine.reigster_plugin(Plugin(5)) engine.run(replay) - Calls functions in the following order for a ``TargetAbilityEvent``:: + Calls functions in the following order for a ``TargetUnitCommandEvent``:: - Plugin1.handleAbilityEvent(event, replay) + Plugin1.handleCommandEvent(event, replay) Plugin2.handleEvent(event, replay) - Plugin2.handleTargetAbilityEvent(event, replay) + Plugin2.handleTargetUnitCommandEvent(event, replay) Plugin Specification @@ -56,9 +57,8 @@ def handleEventName(self, event, replay) * handleMessageEvent - called for events in replay.message.events * handleGameEvent - called for events in replay.game.events * handleTrackerEvent - called for events in replay.tracker.events - * handlePlayerActionEvent - called for all game events indicating player actions - * handleAbilityEvent - called for all types of ability events - * handleHotkeyEvent - called for all player hotkey 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: @@ -90,7 +90,7 @@ def handleEvent(self, event, replay): return ... - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): try: possibly_throwing_error() catch Error as e: @@ -199,26 +199,18 @@ def _get_event_handlers(self, event, plugins): def _get_plugin_event_handlers(self, plugin, event): handlers = list() - if isinstance(event, Event) and self._has_event_handler(plugin, Event): - handlers.append(self._get_event_handler(plugin, Event)) - if isinstance(event, MessageEvent) and self._has_event_handler(plugin, MessageEvent): - handlers.append(self._get_event_handler(plugin, MessageEvent)) - if isinstance(event, GameEvent) and self._has_event_handler(plugin, GameEvent): - handlers.append(self._get_event_handler(plugin, GameEvent)) - if isinstance(event, TrackerEvent) and self._has_event_handler(plugin, TrackerEvent): - handlers.append(self._get_event_handler(plugin, TrackerEvent)) - if isinstance(event, PlayerActionEvent) and self._has_event_handler(plugin, PlayerActionEvent): - handlers.append(self._get_event_handler(plugin, PlayerActionEvent)) - if isinstance(event, AbilityEvent) and self._has_event_handler(plugin, AbilityEvent): - handlers.append(self._get_event_handler(plugin, AbilityEvent)) - if isinstance(event, HotkeyEvent) and self._has_event_handler(plugin, HotkeyEvent): - handlers.append(self._get_event_handler(plugin, HotkeyEvent)) - if self._has_event_handler(plugin, event): - handlers.append(self._get_event_handler(plugin, event)) + 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 - - def _has_event_handler(self, plugin, event): - return hasattr(plugin, 'handle'+event.name) - - def _get_event_handler(self, plugin, event): - return getattr(plugin, 'handle'+event.name, None) diff --git a/sc2reader/engine/plugins/apm.py b/sc2reader/engine/plugins/apm.py index 5eec2ac2..526bf028 100644 --- a/sc2reader/engine/plugins/apm.py +++ b/sc2reader/engine/plugins/apm.py @@ -7,7 +7,7 @@ class APMTracker(object): """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is - any Selection, Hotkey, or Ability event. + any Selection, ControlGroup, or Command event. Also provides ``player.avg_apm`` which is defined as the sum of all the above actions divided by the number of seconds played by the player (not @@ -23,7 +23,15 @@ def handleInitGame(self, event, replay): human.aps = defaultdict(int) human.seconds_played = replay.length.seconds - def handlePlayerActionEvent(self, event, replay): + def handleControlGroupEvent(self, event, replay): + event.player.aps[event.second] += 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 + + def handleCommandEvent(self, event, replay): event.player.aps[event.second] += 1.4 event.player.apm[int(event.second/60)] += 1.4 diff --git a/sc2reader/engine/plugins/context.py b/sc2reader/engine/plugins/context.py index 3372d84b..c83336d2 100644 --- a/sc2reader/engine/plugins/context.py +++ b/sc2reader/engine/plugins/context.py @@ -8,13 +8,13 @@ @loggable class ContextLoader(object): - name='ContextLoader' + name = 'ContextLoader' def handleInitGame(self, event, replay): replay.units = set() replay.unit = dict() - # keep track of last TargetAbilityEvent for UpdateTargetAbilityEvent + # keep track of last TargetUnitCommandEvent for UpdateTargetUnitCommandEvent self.last_target_ability_event = {} def handleGameEvent(self, event, replay): @@ -23,7 +23,7 @@ def handleGameEvent(self, event, replay): def handleMessageEvent(self, event, replay): self.load_message_game_player(event, replay) - def handleAbilityEvent(self, event, replay): + def handleCommandEvent(self, event, replay): if not replay.datapack: return @@ -35,7 +35,7 @@ def handleAbilityEvent(self, event, replay): if not getattr(replay, 'marked_error', None): replay.marked_error = True event.logger.error(replay.filename) - event.logger.error("Release String: "+replay.release_string) + event.logger.error("Release String: " + replay.release_string) for player in replay.players: try: event.logger.error("\t"+unicode(player).encode('ascii', 'ignore')) @@ -53,7 +53,7 @@ def handleAbilityEvent(self, event, replay): elif event.other_unit_id is not None: self.logger.error("Other unit {0} not found".format(event.other_unit_id)) - def handleTargetAbilityEvent(self, event, replay): + def handleTargetUnitCommandEvent(self, event, replay): self.last_target_ability_event[event.player.pid] = event if not replay.datapack: @@ -71,16 +71,16 @@ def handleTargetAbilityEvent(self, event, replay): event.target = unit replay.objects[event.target_unit_id] = unit - def handleUpdateTargetAbilityEvent(self, event, replay): - # We may not find a TargetAbilityEvent before finding an - # UpdateTargetAbilityEvent, perhaps due to Missing Abilities in the + def handleUpdateTargetUnitCommandEvent(self, event, replay): + # We may not find a TargetUnitCommandEvent before finding an + # UpdateTargetUnitCommandEvent, perhaps due to Missing Abilities in the # datapack if event.player.pid in self.last_target_ability_event: - # store corresponding TargetAbilityEvent data in this 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 - self.handleTargetAbilityEvent(event, replay) + self.handleTargetUnitCommandEvent(event, replay) def handleSelectionEvent(self, event, replay): if not replay.datapack: @@ -128,6 +128,11 @@ def handleResourceTradeEvent(self, event, replay): event.sender = event.player event.recipient = replay.players[event.recipient_id] + def handleHijackReplayGameEvent(self, event, replay): + replay.resume_from_replay = True + replay.resume_method = event.method + replay.resume_user_info = event.user_infos + def handlePlayerStatsEvent(self, event, replay): self.load_tracker_player(event, replay) @@ -278,7 +283,7 @@ 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.frames)) + 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 diff --git a/sc2reader/engine/plugins/creeptracker.py b/sc2reader/engine/plugins/creeptracker.py index 25ead4af..14120770 100644 --- a/sc2reader/engine/plugins/creeptracker.py +++ b/sc2reader/engine/plugins/creeptracker.py @@ -5,8 +5,12 @@ from sets import Set except ImportError: Set = set -from PIL.Image import open as PIL_open -from PIL.Image import ANTIALIAS +try: + # required for CreepTracker, but CreepTracker is optional + from PIL.Image import open as PIL_open + from PIL.Image import ANTIALIAS +except ImportError: + pass try: from StringIO import StringIO except ImportError: @@ -43,7 +47,7 @@ def handleUnitDiedEvent(self, event, replay): except Exception as e: print("Whoa! {}".format(e)) pass - + def handleUnitInitEvent(self,event,replay): try: @@ -53,7 +57,7 @@ def handleUnitInitEvent(self,event,replay): except Exception as e: print("Whoa! {}".format(e)) pass - + def handleUnitBornEvent(self,event,replay): try: if event.unit_type_name== "Hatchery": @@ -81,14 +85,14 @@ def handleEndGame(self, event, replay): pass -## The class used to used to calculate the creep spread +## 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 - ##and a stringIO containing the minimap image will be saved for - ##every minite in the game and the minimap with creep highlighted + ##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 @@ -125,7 +129,7 @@ def __init__(self,replay): # 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) ) @@ -138,10 +142,10 @@ def __init__(self,replay): 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.image_scale = float(self.map_height) / cropsize[1] 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): ## this function converts all radius into map coordinates ## centred around the origin that the creep can exist @@ -180,7 +184,7 @@ def add_to_list(self,player_id,unit_id,unit_location,unit_type,event_time): 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 + ## 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]) @@ -194,7 +198,7 @@ def remove_from_list(self,unit_id,time_frame): self.creep_gen_units_times[player_id].append(time_frame) def cgu_gen_times_to_chunks(self,cgu_time_list): - ## this function returns the index and value of every cgu time + ## 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))) @@ -211,7 +215,7 @@ def cgu_in_min_to_cgu_units(self,player_id,cgu_in_minutes): cgu_units.append(self.creep_gen_units[player_id][index]) 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, @@ -224,7 +228,7 @@ def reduce_cgu_per_minute(self,player_id): self.creep_gen_units_times[player_id] = list(minutes) def get_creep_spread_area(self,player_id): - ## iterates through all cgus and and calculate the area + ## 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]]),\ @@ -234,7 +238,7 @@ def get_creep_spread_area(self,player_id): 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) - if self.debug: + if self.debug: 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]=\ @@ -251,7 +255,7 @@ def cgu_radius_to_map_positions(self,cgu_radius,radius_to_coordinates): point = cgu[0] radius = cgu[1] ## subtract all radius_to_coordinates with centre of - ## cgu radius to change centre of circle + ## 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) diff --git a/sc2reader/engine/plugins/gameheart.py b/sc2reader/engine/plugins/gameheart.py index 2812f5b1..08bfc67e 100644 --- a/sc2reader/engine/plugins/gameheart.py +++ b/sc2reader/engine/plugins/gameheart.py @@ -69,6 +69,9 @@ def fix_events(self, replay, start_frame): 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 obs = Observer(p.sid, p.slot_data, p.uid, p.init_data, p.pid) # Because these obs start the game as players the client diff --git a/sc2reader/engine/plugins/selection.py b/sc2reader/engine/plugins/selection.py index 79e9d995..69aa12a7 100644 --- a/sc2reader/engine/plugins/selection.py +++ b/sc2reader/engine/plugins/selection.py @@ -40,17 +40,17 @@ def handleSelectionEvent(self, event, replay): if error: event.player.selection_errors += 1 - def handleGetFromHotkeyEvent(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) event.player.selection[10] = new_selection if error: event.player.selection_errors += 1 - def handleSetToHotkeyEvent(self, event, replay): + def handleSetControlGroupEvent(self, event, replay): event.player.selection[event.control_group] = event.player.selection[10] - def handleAddToHotkeyEvent(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 = self._select(new_selection, event.player.selection[10]) diff --git a/sc2reader/events/game.py b/sc2reader/events/game.py index 59996bf9..0f0e2220 100644 --- a/sc2reader/events/game.py +++ b/sc2reader/events/game.py @@ -13,8 +13,6 @@ class GameEvent(Event): """ This is the base class for all game events. The attributes below are universally available. """ - name = 'GameEvent' - 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 @@ -35,6 +33,9 @@ def __init__(self, frame, pid): #: A flag indicating if it is a local or global event. self.is_local = (pid != 16) + #: Short cut string for event class name + 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" @@ -51,9 +52,6 @@ class GameStartEvent(GameEvent): Recorded when the game starts and the frames start to roll. This is a global non-player event. """ - - name = 'GameStartEvent' - def __init__(self, frame, pid, data): super(GameStartEvent, self).__init__(frame, pid) @@ -65,9 +63,6 @@ class PlayerLeaveEvent(GameEvent): """ Recorded when a player leaves the game. """ - - name = 'PlayerLeaveEvent' - def __init__(self, frame, pid, data): super(PlayerLeaveEvent, self).__init__(frame, pid) @@ -80,9 +75,6 @@ class UserOptionsEvent(GameEvent): This event is recorded for each player at the very beginning of the game before the :class:`GameStartEvent`. """ - - name = 'UserOptionsEvent' - def __init__(self, frame, pid, data): super(UserOptionsEvent, self).__init__(frame, pid) #: @@ -116,40 +108,31 @@ def __init__(self, frame, pid, data): def create_command_event(frame, pid, data): ability_type = data['data'][0] if ability_type == 'None': - return AbilityEvent(frame, pid, data) + return BasicCommandEvent(frame, pid, data) elif ability_type == 'TargetUnit': - return TargetAbilityEvent(frame, pid, data) + return TargetUnitCommandEvent(frame, pid, data) elif ability_type == 'TargetPoint': - return LocationAbilityEvent(frame, pid, data) + return TargetPointCommandEvent(frame, pid, data) elif ability_type == 'Data': - return SelfAbilityEvent(frame, pid, data) - - -class PlayerActionEvent(GameEvent): - name = 'PlayerActionEvent' + return DataCommandEvent(frame, pid, data) @loggable -class AbilityEvent(PlayerActionEvent): +class CommandEvent(GameEvent): """ Ability events are generated when ever a player in the game issues a command to a unit or group of units. They are split into three subclasses of ability, each with their own set of associated data. The attributes listed below are shared across all ability event types. - See :class:`LocationAbilityEvent`, :class:`TargetAbilityEvent`, and :class:`SelfAbilityEvent` - for individual details. + See :class:`TargetPointCommandEvent`, :class:`TargetUnitCommandEvent`, and + :class:`DataCommandEvent` for individual details. """ - - name = 'AbilityEvent' - - is_player_action = True - def __init__(self, frame, pid, data): - super(AbilityEvent, self).__init__(frame, pid) + super(CommandEvent, self).__init__(frame, pid) #: Flags on the command??? self.flags = data['flags'] @@ -255,28 +238,38 @@ def __str__(self): return string -class LocationAbilityEvent(AbilityEvent): +class BasicCommandEvent(CommandEvent): + """ + Extends :class:`CommandEvent` + + This event is recorded for events that have no extra information recorded. + + 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) + + +class TargetPointCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` This event is recorded when ever a player issues a command that targets a location and NOT a unit. Commands like Psistorm, Attack Move, Fungal Growth, and EMP fall under this category. - Note that like all AbilityEvents, the event will be recorded regardless + Note that like all CommandEvents, the event will be recorded regardless of whether or not the command was successful. """ - - name = 'LocationAbilityEvent' - def __init__(self, frame, pid, data): - super(LocationAbilityEvent, self).__init__(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) @@ -285,21 +278,19 @@ def __init__(self, frame, pid, data): self.location = (self.x, self.y, self.z) -class TargetAbilityEvent(AbilityEvent): +class TargetUnitCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` - TargetAbilityEvents are recorded when ever a player issues a command that targets a unit. + This event is recorded when ever a player issues a command that targets a unit. The location of the target unit at the time of the command is also recorded. Commands like Chronoboost, Transfuse, and Snipe fall under this category. - Note that all AbilityEvents are recorded regardless of whether or not the command was successful. + Note that like all CommandEvents, the event will be recorded regardless + of whether or not the command was successful. """ - - name = 'TargetAbilityEvent' - def __init__(self, frame, pid, data): - super(TargetAbilityEvent, self).__init__(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) @@ -327,10 +318,10 @@ def __init__(self, frame, pid, data): 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) @@ -339,43 +330,41 @@ def __init__(self, frame, pid, data): self.location = (self.x, self.y, self.z) -class UpdateTargetAbilityEvent(TargetAbilityEvent): +class UpdateTargetUnitCommandEvent(TargetUnitCommandEvent): """ - Extends :class:`TargetAbilityEvent` + Extends :class:`TargetUnitCommandEvent` - This event is generated when a TargetAbilityEvent is updated, likely due to + This event is generated when a TargetUnitCommandEvent is updated, likely due to changing the target unit. It is unclear if this needs to be a separate event - from TargetAbilityEvent, but for flexibility, it will be treated + from TargetUnitCommandEvent, but for flexibility, it will be treated differently. One example of this event occuring is casting inject on a hatchery while holding shift, and then shift clicking on a second hatchery. """ - name = 'UpdateTargetAbilityEvent' + name = 'UpdateTargetUnitCommandEvent' -class SelfAbilityEvent(AbilityEvent): +class DataCommandEvent(CommandEvent): """ - Extends :class:`AbilityEvent` + Extends :class:`CommandEvent` - SelfAbilityEvents are recorded when ever a player issues a command that has no target. Commands + DataCommandEvent are recorded when ever a player issues a command that has no target. Commands like Burrow, SeigeMode, Train XYZ, and Stop fall under this category. - Note that all AbilityEvents are recorded regardless of whether or not the command was successful. + Note that like all CommandEvents, the event will be recorded regardless + of whether or not the command was successful. """ - - name = 'SelfAbilityEvent' - def __init__(self, frame, pid, data): - super(SelfAbilityEvent, self).__init__(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) @loggable -class SelectionEvent(PlayerActionEvent): +class SelectionEvent(GameEvent): """ Selection events are generated when ever the active selection of the player is updated. Unlike other game events, these events can also be @@ -384,12 +373,8 @@ class SelectionEvent(PlayerActionEvent): Starting in Starcraft 2.0.0, selection events targetting 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:`HotkeyEvent` is generated. + a :class:`ControlGroupEvent` is generated. """ - - name = 'SelectionEvent' - is_player_action = True - def __init__(self, frame, pid, data): super(SelectionEvent, self).__init__(frame, pid) @@ -438,43 +423,35 @@ def __str__(self): def create_control_group_event(frame, pid, data): update_type = data['control_group_update'] if update_type == 0: - return SetToHotkeyEvent(frame, pid, data) + return SetControlGroupEvent(frame, pid, data) elif update_type == 1: - return AddToHotkeyEvent(frame, pid, data) + return AddToControlGroupEvent(frame, pid, data) elif update_type == 2: - return GetFromHotkeyEvent(frame, pid, data) + return GetControlGroupEvent(frame, pid, data) elif update_type == 3: # TODO: What could this be?!? - return HotkeyEvent(frame, pid, data) + return ControlGroupEvent(frame, pid, data) else: # No idea what this is but we're seeing update_types of 4 and 5 in 3.0 - return HotkeyEvent(frame, pid, data) - - + return ControlGroupEvent(frame, pid, data) + @loggable -class HotkeyEvent(PlayerActionEvent): +class ControlGroupEvent(GameEvent): """ - Hotkey events are recorded when ever a player action modifies a control - group. I know that calling control group events hotkey events doesn't make - sense but for backwards compatibility I haven't changed it yet. Sorry. - - There are three kinds of hotkey events, generated by each of the possible + ControlGroup events are recorded when ever a player action modifies or accesses a control + group. There are three kinds of events, generated by each of the possible player actions: - * :class:`SetToHotkeyEvent` - Recorded when a user sets a control group (ctrl+#). - * :class:`GetFromHotkeyEvent` - Recorded when a user retrieves a control group (#). - * :class:`AddToHotkeyEvent` - Recorded when a user adds to a control group (shift+ctrl+#) + * :class:`SetControlGroup` - Recorded when a user sets a control group (ctrl+#). + * :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. See the class entry for details. """ - - name = 'HotkeyEvent' - is_player_action = True - def __init__(self, frame, pid, data): - super(HotkeyEvent, self).__init__(frame, pid) + super(ControlGroupEvent, self).__init__(frame, pid) #: Index to the control group being modified self.control_group = data['control_group_index'] @@ -495,38 +472,33 @@ def __init__(self, frame, pid, data): self.mask_data = data['remove_mask'][1] -class SetToHotkeyEvent(HotkeyEvent): +class SetControlGroupEvent(ControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` This event does a straight forward replace of the current control group contents with the player's current selection. This event doesn't have masks set. """ - name = 'SetToHotkeyEvent' - -class AddToHotkeyEvent(HotkeyEvent): +class AddToControlGroupEvent(SetControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` This event adds the current selection to the control group. """ - name = 'AddToHotkeyEvent' - -class GetFromHotkeyEvent(HotkeyEvent): +class GetControlGroupEvent(ControlGroupEvent): """ - Extends :class:`HotkeyEvent` + Extends :class:`ControlGroupEvent` + This event replaces the current selection with the contents of the control group. The mask data is used to limit that selection to units that are currently selectable. You might have 1 medivac and 8 marines on the control group but if the 8 marines are inside the medivac they cannot be part of your selection. """ - name = 'GetFromHotkeyEvent' - @loggable class CameraEvent(GameEvent): @@ -535,9 +507,6 @@ class CameraEvent(GameEvent): It does not matter why the camera changed, this event simply records the current state of the camera after changing. """ - - name = 'CameraEvent' - def __init__(self, frame, pid, data): super(CameraEvent, self).__init__(frame, pid) @@ -565,8 +534,10 @@ def __str__(self): @loggable class ResourceTradeEvent(GameEvent): - name = 'ResourceTradeEvent' - + """ + 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) @@ -602,8 +573,9 @@ def __str__(self): class ResourceRequestEvent(GameEvent): - name = 'ResourceRequestEvent' - + """ + Generated when a player creates a resource request. + """ def __init__(self, frame, pid, data): super(ResourceRequestEvent, self).__init__(frame, pid) @@ -627,8 +599,9 @@ def __str__(self): class ResourceRequestFulfillEvent(GameEvent): - name = 'ResourceRequestFulfillEvent' - + """ + Generated when a player accepts a resource request. + """ def __init__(self, frame, pid, data): super(ResourceRequestFulfillEvent, self).__init__(frame, pid) @@ -637,10 +610,25 @@ def __init__(self, frame, pid, data): class ResourceRequestCancelEvent(GameEvent): - name = 'ResourceRequestCancelEvent' - + """ + 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'] + + +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'] + + #: Information on the users hijacking the game + self.user_infos = data['user_infos'] diff --git a/sc2reader/events/message.py b/sc2reader/events/message.py index aeadc568..e9859733 100644 --- a/sc2reader/events/message.py +++ b/sc2reader/events/message.py @@ -8,8 +8,9 @@ @loggable class MessageEvent(Event): - name = 'MessageEvent' - + """ + 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 @@ -20,6 +21,9 @@ def __init__(self, frame, pid): #: The second of the game (game time not real time) this event was applied self.second = frame >> 4 + #: Short cut string for event class name + self.name = self.__class__.__name__ + 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) @@ -30,8 +34,9 @@ def __str__(self): @loggable class ChatEvent(MessageEvent): - name = 'ChatEvent' - + """ + 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. @@ -51,18 +56,22 @@ def __init__(self, frame, pid, target, text): @loggable -class PacketEvent(MessageEvent): - name = 'PacketEvent' +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) - def __init__(self, frame, pid, info): - super(PacketEvent, self).__init__(frame, pid) - self.info = info + #: Marks the load progress for the player. Scaled 0-100. + self.progress = progress @loggable class PingEvent(MessageEvent): - name = 'PingEvent' - + """ + Records pings made by players in game. + """ def __init__(self, frame, pid, target, x, y): super(PingEvent, self).__init__(frame, pid) diff --git a/sc2reader/events/tracker.py b/sc2reader/events/tracker.py index 767a9fdf..41b5c2f2 100644 --- a/sc2reader/events/tracker.py +++ b/sc2reader/events/tracker.py @@ -15,12 +15,15 @@ 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 + #: Ignore all but the lowest 32 bits of the frame self.frame = frames % 2**32 #: The second of the game (game time not real time) this event was applied self.second = self.frame >> 4 + #: Short cut string for event class name + self.name = self.__class__.__name__ + def load_context(self, replay): pass @@ -61,9 +64,6 @@ 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. """ - - name = 'PlayerStatsEvent' - def __init__(self, frames, data, build): super(PlayerStatsEvent, self).__init__(frames) @@ -241,12 +241,9 @@ class UnitBornEvent(TrackerEvent): Unfortunately, units that are born do not have events marking their beginnings like :class:`UnitInitEvent` and :class:`UnitDoneEvent` do. The closest thing to it are the - :class:`~sc2reader.event.game.AbilityEvent` game events where the ability is a train unit + :class:`~sc2reader.event.game.CommandEvent` game events where the command is a train unit command. """ - - name = 'UnitBornEvent' - def __init__(self, frames, data, build): super(UnitBornEvent, self).__init__(frames) @@ -302,9 +299,6 @@ class UnitDiedEvent(TrackerEvent): Generated when a unit dies or is removed from the game for any reason. Reasons include morphing, merging, and getting killed. """ - - name = 'UnitDiedEvent' - def __init__(self, frames, data, build): super(UnitDiedEvent, self).__init__(frames) @@ -371,8 +365,10 @@ def __str__(self): class UnitOwnerChangeEvent(TrackerEvent): - name = 'UnitOwnerChangeEvent' - + """ + 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) @@ -405,8 +401,11 @@ def __str__(self): class UnitTypeChangeEvent(TrackerEvent): - name = 'UnitTypeChangeEvent' - + """ + Generated when the unit's type changes. This generally tracks upgrades to buildings (Hatch, + 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) @@ -430,8 +429,9 @@ def __str__(self): class UpgradeCompleteEvent(TrackerEvent): - name = 'UpgradeCompleteEvent' - + """ + Generated when a player completes an upgrade. + """ def __init__(self, frames, data, build): super(UpgradeCompleteEvent, self).__init__(frames) @@ -442,7 +442,7 @@ def __init__(self, frames, data, build): self.player = None #: The name of the upgrade - self.upgrade_type_name = data[1] + self.upgrade_type_name = data[1].decode('utf8') #: The number of times this upgrade as been researched self.count = data[2] @@ -457,9 +457,6 @@ 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. """ - - name = 'UnitInitEvent' - def __init__(self, frames, data, build): super(UnitInitEvent, self).__init__(frames) @@ -515,9 +512,6 @@ 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. """ - - name = 'UnitDoneEvent' - def __init__(self, frames, data, build): super(UnitDoneEvent, self).__init__(frames) @@ -538,8 +532,11 @@ def __str__(self): class UnitPositionsEvent(TrackerEvent): - name = 'UnitPositionsEvent' - + """ + Generated every 15 seconds. Marks the positions of the first 255 units that were damaged in + 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/factories/plugins/replay.py b/sc2reader/factories/plugins/replay.py index 4353d8c3..ad465609 100644 --- a/sc2reader/factories/plugins/replay.py +++ b/sc2reader/factories/plugins/replay.py @@ -62,7 +62,7 @@ def toDict(replay): # Consolidate replay metadata into dictionary return { - 'gateway': getattr(replay, 'gateway', 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), @@ -94,7 +94,7 @@ def toDict(replay): def APMTracker(replay): """ Builds ``player.aps`` and ``player.apm`` dictionaries where an action is - any Selection, Hotkey, or Ability event. + any Selection, Hotkey, or Command event. Also provides ``player.avg_apm`` which is defined as the sum of all the above actions divided by the number of seconds played by the player (not @@ -106,7 +106,7 @@ def APMTracker(replay): player.seconds_played = replay.length.seconds for event in player.events: - if event.name == 'SelectionEvent' or 'AbilityEvent' in event.name or 'Hotkey' 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 @@ -122,7 +122,7 @@ def APMTracker(replay): @plugin def SelectionTracker(replay): - debug = replay.opt.debug + debug = replay.opt['debug'] logger = log_utils.get_logger(SelectionTracker) for person in replay.entities: @@ -140,13 +140,13 @@ def SelectionTracker(replay): 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 == 'SetToHotkeyEvent': + 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)) - elif event.name == 'AddToHotkeyEvent': + 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) @@ -155,7 +155,7 @@ def SelectionTracker(replay): if debug: logger.info("[{0}] {1} added current selection to hotkey {2}".format(Length(seconds=event.second), person.name, event.hotkey)) - elif event.name == 'GetFromHotkeyEvent': + 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) diff --git a/sc2reader/factories/sc2factory.py b/sc2reader/factories/sc2factory.py index 7518767b..36020b2d 100644 --- a/sc2reader/factories/sc2factory.py +++ b/sc2reader/factories/sc2factory.py @@ -156,7 +156,7 @@ def _get_plugins(self, cls): return plugins def _get_options(self, cls, **new_options): - options = utils.AttributeDict() + options = dict() for opt_cls, cls_options in self.options.items(): if issubclass(cls, opt_cls): options.update(cls_options) @@ -175,7 +175,7 @@ def _load_resources(self, resources, options=None, **new_options): yield self._load_resource(resource, options=options) def load_remote_resource_contents(self, resource, **options): - self.logger.info("Fetching remote resource: "+resource) + self.logger.info("Fetching remote resource: " + resource) return urlopen(resource).read() def load_local_resource_contents(self, location, **options): diff --git a/sc2reader/objects.py b/sc2reader/objects.py index 3dd22651..203a7340 100644 --- a/sc2reader/objects.py +++ b/sc2reader/objects.py @@ -101,7 +101,9 @@ def __init__(self, sid, slot_data): self.handicap = slot_data['handicap'] #: The entity's team number. None for observers - self.team_id = slot_data['team_id']+1 + self.team_id = None + 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) @@ -133,9 +135,6 @@ def __init__(self, sid, slot_data): #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[int(parts[0])] - #: Deprecated, see Entity.region - self.gateway = self.region - #: The Battle.net subregion the entity is registered to self.subregion = int(parts[2]) @@ -210,9 +209,6 @@ def __init__(self, pid, detail_data, attribute_data): #: The Battle.net region the entity is registered to self.region = GATEWAY_LOOKUP[detail_data['bnet']['region']] - #: Deprecated, see `Player.region` - self.gateway = self.region - #: The Battle.net subregion the entity is registered to self.subregion = detail_data['bnet']['subregion'] @@ -359,12 +355,7 @@ class PlayerSummary(): #: Subregion id of player subregion = int() - #: The player's gateway, such as us, eu - gateway = str() - - #: The player's region, such as na, la, eu or ru. This is - # provided for convenience, but as of 20121018 is strictly a - # function of gateway and subregion. + #: The player's region, such as us, eu, sea region = str() #: unknown1 diff --git a/sc2reader/readers.py b/sc2reader/readers.py index 35766e20..8bbd7e9a 100644 --- a/sc2reader/readers.py +++ b/sc2reader/readers.py @@ -34,7 +34,7 @@ def __call__(self, data, replay): 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, + toon_handle=data.read_aligned_string(data.read_bits(7)) if replay.base_build >= 34784 else None, ) for i in range(data.read_bits(5))], game_description=dict( @@ -226,7 +226,7 @@ def __call__(self, data, replay): elif flag == 2: # Loading progress message progress = data.read_uint32()-2147483648 - packets.append(PacketEvent(frame, pid, progress)) + packets.append(ProgressEvent(frame, pid, progress)) elif flag == 3: # Server ping message pass @@ -327,7 +327,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 @@ -1362,7 +1362,7 @@ def __init__(self): 21: (None, self.save_game_event), # New 22: (None, self.save_game_done_event), # Override 23: (None, self.load_game_done_event), # Override - 43: (None, self.hijack_replay_game_event), # New + 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 @@ -1392,7 +1392,7 @@ def load_game_done_event(self, data): def hijack_replay_game_event(self, data): return dict( user_infos=[dict( - game_unit_id=data.read_bits(4), + 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, @@ -1526,7 +1526,7 @@ def __init__(self): 61: (None, self.trigger_hotkey_pressed_event), 103: (None, self.command_manager_state_event), 104: (None, self.command_update_target_point_event), - 105: (UpdateTargetAbilityEvent, self.command_update_target_unit_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), diff --git a/sc2reader/resources.py b/sc2reader/resources.py index fa6daf5a..15d89453 100644 --- a/sc2reader/resources.py +++ b/sc2reader/resources.py @@ -1,4 +1,4 @@ - # -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, unicode_literals, division from collections import defaultdict, namedtuple @@ -17,13 +17,13 @@ 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.constants import REGIONS, GAME_SPEED_FACTOR, LOBBY_PROPERTIES +from sc2reader.constants import GAME_SPEED_FACTOR, LOBBY_PROPERTIES class Resource(object): def __init__(self, file_object, filename=None, factory=None, **options): self.factory = factory - self.opt = utils.AttributeDict(options) + self.opt = options self.logger = log_utils.get_logger(self.__class__) self.filename = filename or getattr(file_object, 'name', 'Unavailable') @@ -120,8 +120,8 @@ class Replay(Resource): #: The :class:`Length` of the replay in real time adjusted for the game speed real_length = None - #: The gateway the game was played on: us, eu, sea, etc - gateway = str() + #: The region the game was played on: us, eu, sea, etc + region = str() #: An integrated list of all the game events events = list() @@ -137,7 +137,7 @@ class Replay(Resource): #: A dual key dict mapping player names and numbers to #: :class:`Player` objects - player = utils.PersonDict() + player = dict() #: A list of :class:`Observer` objects from the game observers = list() @@ -148,7 +148,7 @@ class Replay(Resource): #: A dual key dict mapping :class:`Person` object to their #: person id's and names - person = utils.PersonDict() + person = dict() #: A list of :class:`Person` objects from the game representing #: only the human players from the :attr:`people` list @@ -183,6 +183,15 @@ class Replay(Resource): #: SC2 Expansion. One of 'WoL', 'HotS' expasion = str() + #: True of the game was resumed from a replay + resume_from_replay = False + + #: A flag marking which method was used to resume from replay. Unknown interpretation. + resume_method = None + + #: 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): super(Replay, self).__init__(replay_file, filename, **options) self.datapack = None @@ -191,7 +200,7 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en # The current load level of the replay self.load_level = None - #default values, filled in during file read + # default values, filled in during file read self.speed = "" self.type = "" self.game_type = "" @@ -201,15 +210,15 @@ def __init__(self, replay_file, filename=None, load_level=4, engine=sc2reader.en self.is_private = False self.map = None self.map_hash = "" - self.gateway = "" + self.region = "" self.events = list() self.teams, self.team = list(), dict() - self.player = utils.PersonDict() - self.observer = utils.PersonDict() - self.human = utils.PersonDict() - self.computer = utils.PersonDict() - self.entity = utils.PersonDict() + self.player = dict() + self.observer = dict() + self.human = dict() + self.computer = dict() + self.entity = dict() self.players = list() self.observers = list() # Unordered list of Observer @@ -313,7 +322,7 @@ def load_details(self): self.cooperative = options['cooperative'] self.battle_net = options['battle_net'] self.hero_duplicates_allowed = options['hero_duplicates_allowed'] - + if 'replay.attributes.events' in self.raw_data: # Organize the attribute data to be useful self.attributes = defaultdict(dict) @@ -333,17 +342,17 @@ def load_details(self): self.map_name = details['map_name'] - self.gateway = details['cache_handles'][0].server.lower() + 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.gateway == 'sg': - self.gateway = 'sea' + # 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' + 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: @@ -373,7 +382,7 @@ 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 we don't at least have details and attributes_events we can go no further if 'replay.details' not in self.raw_data: return if 'replay.attributes.events' not in self.raw_data: @@ -473,7 +482,7 @@ def get_team(team_id): self.recorder = None entity_names = sorted(map(lambda p: p.name, self.entities)) - hash_input = self.gateway+":"+','.join(entity_names) + 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 @@ -615,33 +624,42 @@ 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'] + return state + class Map(Resource): url_template = 'http://{0}.depot.battle.net:1119/{1}.s2ma' - #: The localized (only enUS supported right now) map name - name = str() + def __init__(self, map_file, filename=None, region=None, map_hash=None, **options): + super(Map, self).__init__(map_file, filename, **options) - #: The map's author - author = str() + #: The localized (only enUS supported right now) map name. + self.name = str() - #: The map description as written by author - description = str() + #: The localized (only enUS supported right now) map author. + self.author = str() - def __init__(self, map_file, filename=None, gateway=None, map_hash=None, **options): - super(Map, self).__init__(map_file, filename, **options) + #: The localized (only enUS supported right now) map description. + self.description = str() + + #: The localized (only enUS supported right now) map website. + self.website = str() #: The unique hash used to identify this map on bnet's depots. self.hash = map_hash - #: The gateway this map was posted to. Maps must be posted individually to each gateway. - self.gateway = gateway + #: The region this map was posted to. Maps must be posted individually to each region. + self.region = region #: A URL reference to the location of this map on bnet's depots. - self.url = Map.get_url(gateway, map_hash) + self.url = Map.get_url(self.region, map_hash) #: The opened MPQArchive for this map self.archive = mpyq.MPQArchive(map_file) @@ -653,9 +671,9 @@ def __init__(self, map_file, filename=None, gateway=None, map_hash=None, **optio # 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 = self.archive.read_file('enUS.SC2Data\LocalizedData\GameStrings.txt').decode('utf8') - if game_strings: - for line in game_strings.split('\r\n'): + 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'): if len(line) == 0: continue @@ -670,29 +688,34 @@ def __init__(self, map_file, filename=None, gateway=None, map_hash=None, **optio self.website = value #: A reference to the map's :class:`~sc2reader.objects.MapInfo` object - self.map_info = MapInfo(self.archive.read_file('MapInfo')) + self.map_info = None + map_info_file = self.archive.read_file('MapInfo') + if map_info_file: + self.map_info = MapInfo(map_info_file) - doc_info = ElementTree.fromstring(self.archive.read_file('DocumentInfo').decode('utf8')) + doc_info_file = self.archive.read_file('DocumentInfo') + if doc_info_file: + doc_info = ElementTree.fromstring(doc_info_file.decode('utf8')) - 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 + 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 + #: (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 - #: A list of module names this map depends on - self.dependencies = list() - for dependency_node in doc_info.findall('Dependencies/Value'): - self.dependencies.append(dependency_node.text) + #: A list of module names this map depends on + self.dependencies = list() + for dependency_node in doc_info.findall('Dependencies/Value'): + self.dependencies.append(dependency_node.text) @classmethod - def get_url(cls, gateway, map_hash): + def get_url(cls, region, map_hash): """Builds a download URL for the map from its components.""" - if gateway and map_hash: + if region and map_hash: # it seems like sea maps are stored on us depots. - gateway = 'us' if gateway == 'sea' else gateway - return cls.url_template.format(gateway, map_hash) + region = 'us' if region == 'sea' else region + return cls.url_template.format(region, map_hash) else: return None @@ -852,8 +875,8 @@ def load_translations(self): files.append(utils.DepotFile(file_hash)) self.localization_urls[language] = files - # Grab the gateway from the one of the files - self.gateway = list(self.localization_urls.values())[0][0].server.lower() + # Grab the region from the one of the files + self.region = list(self.localization_urls.values())[0][0].server.lower() # Each of the localization urls points to an XML file with a set of # localization strings and their unique ids. After reading these mappings @@ -864,7 +887,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() @@ -875,9 +898,9 @@ 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" @@ -885,7 +908,7 @@ 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] @@ -944,7 +967,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: @@ -958,7 +981,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:]], []) @@ -1023,9 +1046,8 @@ def load_players(self): settings = self.player_settings[index] player.is_ai = not isinstance(struct[0][1], dict) if not player.is_ai: - player.gateway = self.gateway + player.region = self.region player.subregion = struct[0][1][0][2] - player.region = REGIONS[player.gateway].get(player.subregion, 'Unknown') player.bnetid = struct[0][1][0][3] player.unknown1 = struct[0][1][0] player.unknown2 = struct[0][1][1] diff --git a/sc2reader/scripts/sc2parse.py b/sc2reader/scripts/sc2parse.py index 063f6410..48145e1b 100755 --- a/sc2reader/scripts/sc2parse.py +++ b/sc2reader/scripts/sc2parse.py @@ -48,7 +48,7 @@ def main(): 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 'AbilityEvent' in event.name]) + 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__)) @@ -66,10 +66,10 @@ def main(): print("") print(path) print('{build} - {real_type} on {map_name} - Played {start_time}'.format(**e.replay.__dict__)) - print('[ERROR]', e.message) + 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(e.buffer.read_range(e.location, e.location + 50).encode('hex')) print except Exception as e: print("") @@ -77,20 +77,20 @@ def main(): 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.message)) + print('[ERROR] {0}'.format(e)) for pid, attributes in replay.attributes.items(): print("{0} {1}".format(pid, attributes)) - for pid, info in enumerate(replay.raw_data['replay.details'].players): + for pid, info in enumerate(replay.players): print("{0} {1}".format(pid, info)) - for message in replay.raw_data['replay.message.events'].messages: + for message in replay.messages: print("{0} {1}".format(message.pid, message.text)) traceback.print_exc() 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.message)) - print('[ERROR] {0}'.format(e2.message)) + print('[ERROR] {0}'.format(e)) + print('[ERROR] {0}'.format(e2)) traceback.print_exc() print diff --git a/sc2reader/scripts/sc2printer.py b/sc2reader/scripts/sc2printer.py index 52b39c1e..c780d16e 100755 --- a/sc2reader/scripts/sc2printer.py +++ b/sc2reader/scripts/sc2printer.py @@ -28,6 +28,11 @@ def printReplay(filepath, arguments): 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])) + if arguments.observers: + print(" Observers:") + for observer in replay.observers: + print(" {0}".format(observer.name)) + if arguments.messages: print(" Messages:") for message in replay.messages: @@ -43,9 +48,9 @@ def printReplay(filepath, arguments): 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: @@ -84,31 +89,33 @@ def main(): 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]") + 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") + 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]") + 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]") + 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]") + 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]") + 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]") + 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]") + 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]") + help="print(player build orders (first 64 items) [default off]") arguments = parser.parse_args() for path in arguments.paths: diff --git a/sc2reader/scripts/sc2replayer.py b/sc2reader/scripts/sc2replayer.py index cf373ed1..7a72c7ed 100755 --- a/sc2reader/scripts/sc2replayer.py +++ b/sc2reader/scripts/sc2replayer.py @@ -45,7 +45,7 @@ def getch(): def main(): parser = argparse.ArgumentParser( description="""Step by step replay of game events; shows only the - Initialization, Ability, and Selection events by default. Press any + Initialization, Command, and Selection events by default. Press any key to advance through the events in sequential order.""" ) @@ -77,7 +77,7 @@ def main(): # Loop through the events for event in events: - if isinstance(event, AbilityEvent) or \ + if isinstance(event, CommandEvent) or \ isinstance(event, SelectionEvent) or \ isinstance(event, PlayerLeaveEvent) or \ isinstance(event, GameStartEvent) or \ diff --git a/sc2reader/utils.py b/sc2reader/utils.py index 94fb5637..2ee3d382 100644 --- a/sc2reader/utils.py +++ b/sc2reader/utils.py @@ -48,57 +48,11 @@ def __str__(self): return self.url -class PersonDict(dict): - """ - Deprecated! - - Supports lookup on both the player name and player id - - :: - - person = PersonDict() - person[1] = Player(1,"ShadesofGray") - me = person.name("ShadesofGray") - del person[me.pid] - - Delete is supported on the player id only - """ - def __init__(self): - super(PersonDict, self).__init__() - self._key_map = dict() - - def name(self, player_name): - """ deprecated because it is possible for multiple players to have the same name. """ - return self[self._key_map[player_name]] - - def __setitem__(self, key, value): - self._key_map[value.name] = key - super(PersonDict, self).__setitem__(key, value) - - 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) - - -class AttributeDict(dict): - """ - Support access to dictionary items via the dot syntax as though they - were class attributes. Also support setting new keys via dot syntax. - """ - def __getattr__(self, name): - try: - return self[name] - except KeyError: - raise AttributeError('No such attribute {0}'.format(name)) - - def __setattr__(self, name, value): - self[name] = value - - def copy(self): - return AttributeDict(self.items()) + return int((windows_time - 116444735995904000) / 10 ** 7) @loggable @@ -197,12 +151,6 @@ def recovery_attempt(): raise MPQError("Unable to extract file: {0}".format(data_file), e) -def merged_dict(a, b): - c = a.copy() - c.update(b) - return c - - def get_files(path, exclude=list(), depth=-1, followlinks=False, extension=None, **extras): """ Retrieves files from the given path with configurable behavior. @@ -252,12 +200,12 @@ class Length(timedelta): @property def hours(self): """ The number of hours in represented. """ - return self.seconds//3600 + return self.seconds // 3600 @property def mins(self): """ The number of minutes in excess of the hours. """ - return self.seconds//60 % 60 + return self.seconds // 60 % 60 @property def secs(self): @@ -328,7 +276,7 @@ def toDict(replay): # Consolidate replay metadata into dictionary return { - 'gateway': getattr(replay, 'gateway', 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), diff --git a/setup.py b/setup.py index 5b5c7879..47cf417c 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setuptools.setup( license="MIT", name="sc2reader", - version='0.6.4', + version='0.7.0-pre', 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(), @@ -46,7 +46,7 @@ ] }, - install_requires=['mpyq>=0.2.3', 'argparse', 'ordereddict', 'unittest2', 'pil'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.3'], + install_requires=['mpyq>=0.2.3', 'argparse', 'ordereddict', 'unittest2', 'pil'] if float(sys.version[:3]) < 2.7 else ['mpyq>=0.2.4'], packages=setuptools.find_packages(), include_package_data=True, zip_safe=True diff --git a/test_replays/2.0.11.26825/DaedalusPoint.SC2Replay b/test_replays/2.0.11.26825/DaedalusPoint.SC2Replay new file mode 100644 index 00000000..140a80a9 Binary files /dev/null and b/test_replays/2.0.11.26825/DaedalusPoint.SC2Replay differ diff --git a/test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay b/test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay new file mode 100755 index 00000000..1b46fd14 Binary files /dev/null and b/test_replays/2.1.3.28667/Habitation Station LE (54).SC2Replay differ diff --git a/test_replays/test_all.py b/test_replays/test_all.py index 9117b53b..37bc8d38 100644 --- a/test_replays/test_all.py +++ b/test_replays/test_all.py @@ -13,10 +13,10 @@ import unittest import sc2reader +from sc2reader.exceptions import CorruptTrackerFileError sc2reader.log_utils.log_to_console("INFO") - class TestReplays(unittest.TestCase): def test_teams(self): @@ -198,20 +198,20 @@ 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 "AbilityEvent" in event.name]) + 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") self.assertEqual(replay.expansion, "WoL") - ability_pids = set([event.player.pid for event in replay.events if "AbilityEvent" 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) 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 "TargetAbilityEvent" in event.name and event.ability.name == "SpawnLarva"]) + 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): @@ -228,6 +228,8 @@ def test_oracle_parsing(self): def test_resume_from_replay(self): 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") @@ -244,7 +246,7 @@ def test_send_resources(self): def test_cn_replays(self): replay = sc2reader.load_replay("test_replays/2.0.5.25092/cn1.SC2Replay") - self.assertEqual(replay.gateway, "cn") + self.assertEqual(replay.region, "cn") self.assertEqual(replay.expansion, "WoL") def test_unit_types(self): @@ -321,7 +323,7 @@ def test_factory_plugins(self): self.assertEqual(result["release"], "2.0.5.25092") self.assertEqual(result["game_length"], 986) self.assertEqual(result["real_length"], 704) - self.assertEqual(result["gateway"], "cn") + self.assertEqual(result["region"], "cn") self.assertEqual(result["game_fps"], 16.0) self.assertTrue(result["is_ladder"]) @@ -332,7 +334,7 @@ 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([ event.player.pid for event in replay.events if "TargetAbilityEvent" in event.name and event.ability.name == "SpawnLarva"]) + 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") @@ -379,7 +381,7 @@ def test_creepTracker(self): 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 @@ -394,8 +396,16 @@ def test_creepTracker(self): assert replay.player[2].creep_spread_by_minute[780] == 22.42 def test_bad_unit_ids(self): - 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.9.26147/bad_unit_ids_2.SC2Replay", load_level=4) + with self.assertRaises(CorruptTrackerFileError): + 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) + + def test_daedalus_point(self): + 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") def test_214(self): replay = sc2reader.load_replay("test_replays/2.1.4/Catallena LE.SC2Replay", load_level=4) @@ -418,7 +428,7 @@ def test_lotv_creepTracker(self): 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 @@ -441,14 +451,14 @@ 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)) - + 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) - + def test_30_apms(self): from sc2reader.factories.plugins.replay import APMTracker, SelectionTracker, toJSON