diff --git a/README.md b/README.md index eb7b884..a9b5dc9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Age of Empires II recorded game parsing and summarization in Python 3. - The Conquerors (`.mgx`) - Userpatch 1.4 (`.mgz`) - Userpatch 1.5 (`.mgz`) +- HD Edition >= 4.6 (`.aoe2record`) - Definitive Edition (`.aoe2record`) ## Architecture @@ -30,6 +31,8 @@ Abstractions take parser output as input and return an object with normalized da | The Conquerors (`.mgx`) | | ✓ | | ✓ | ✓ | | | Userpatch <= 1.4 (`.mgz`) | | ✓ | | ✓ | ✓ | ✓ | | Userpatch 1.5 (`.mgz`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| HD Edition >= 4.6 | | ✓ | | ✓ | ✓ | ✓ | +| HD Edition 5.8 | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Definitive Edition <= 13.34 (`.aoe2record`) | | ✓ | | ✓ | ✓ | ✓ | | Definitive Edition > 13.34 (`.aoe2record`) | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/mgz/__init__.py b/mgz/__init__.py index f9ef066..1b03207 100644 --- a/mgz/__init__.py +++ b/mgz/__init__.py @@ -12,12 +12,14 @@ from mgz.header.scenario import scenario from mgz.header.lobby import lobby from mgz.header.de import de +from mgz.header.hd import hd compressed_header = Struct( "game_version"/CString(encoding='latin1'), "save_version"/VersionAdapter(Float32l), "version"/Computed(lambda ctx: get_version(ctx.game_version, ctx.save_version, None)), + "hd"/If(lambda ctx: ctx.version == Version.HD and ctx.save_version > 12.34, hd), "de"/If(lambda ctx: ctx.version == Version.DE, de), ai, replay, @@ -29,6 +31,7 @@ Terminated ) + subheader = Struct( "check"/Peek(Int32ul), "chapter_address"/If(lambda ctx: ctx.check < 100000000, Int32ul), diff --git a/mgz/enums.py b/mgz/enums.py index 9b0bf2e..aeceeef 100644 --- a/mgz/enums.py +++ b/mgz/enums.py @@ -203,7 +203,8 @@ def ResourceLevelEnum(ctx): medium=2, high=3, unknown1=4, - unknown2=5 + unknown2=5, + unknown3=6 ) def RevealMapEnum(ctx): diff --git a/mgz/fast/__init__.py b/mgz/fast/__init__.py index a207a9e..87b8ff3 100644 --- a/mgz/fast/__init__.py +++ b/mgz/fast/__init__.py @@ -37,6 +37,7 @@ class Action(Enum): GROUP_MULTI_WAYPOINTS = 31 CHAPTER = 32 DE_ATTACK_MOVE = 33 + HD_UNKNOWN_34 = 34 DE_UNKNOWN_35 = 35 DE_UNKNOWN_37 = 37 DE_AUTOSCOUT = 38 diff --git a/mgz/fast/header.py b/mgz/fast/header.py index a4125af..0347118 100644 --- a/mgz/fast/header.py +++ b/mgz/fast/header.py @@ -44,11 +44,18 @@ def aoc_string(data): def de_string(data): """Read DE string.""" - assert data.read(2) == b'\x60\x0A' + assert data.read(2) == b'\x60\x0a' length = unpack('= 20.06: data.read(9) data.read(8) - if version is not Version.DE: + if version not in (Version.DE, Version.HD): data.read(1) reveal_map_id, map_size, population, game_type_id, lock_teams = unpack('I4xIIbb', data) - if version is Version.DE: - data.read(9) + if version in (Version.DE, Version.HD): + data.read(5) + if save >= 13.13: + data.read(4) chat = [] for _ in range(0, unpack('= 1006, Bytes(1)), + Padding(15), + hd_string, + Padding(1), + If(lambda ctx: ctx._._.version >= 1005, hd_string), + hd_string, + Padding(16), + "test"/Int32ul, + "is_57"/Computed(lambda ctx: ctx.check == ctx.test) +) + +player = Struct( + "dlc_id"/Int32ul, + "color_id"/Int32ul, + "unk1_1006"/If(lambda ctx: ctx._._.version >= 1006, Bytes(1)), + "unk"/Bytes(2), + "dat_crc"/Bytes(4), + "mp_game_version"/Byte, + "team_index"/Int32ul, + "civ_id"/Int32ul, + "ai_type"/hd_string, + "ai_civ_name_index"/Byte, + "ai_name"/If(lambda ctx: ctx._._.version >= 1005, hd_string), + "name"/hd_string, + "type"/PlayerTypeEnum(Int32ul), + "steam_id"/Int64ul, + "player_number"/Int32sl, + Embedded(If(lambda ctx: ctx._._.version >= 1006 and not ctx._.test_57.is_57, Struct( + "hd_rm_rating"/Int32ul, + "hd_dm_rating"/Int32ul, + ))) +) + +hd = "hd"/Struct( + "version"/Float32l, + "interval_version"/Int32ul, + "game_options_version"/Int32ul, + "dlc_count"/Int32ul, + "dlc_ids"/Array(lambda ctx: ctx.dlc_count, Int32ul), + "dataset_ref"/Int32ul, + Peek("difficulty_id"/Int32ul), + DifficultyEnum("difficulty"/Int32ul), + "selected_map_id"/Int32ul, + "resolved_map_id"/Int32ul, + "reveal_map"/Int32ul, + Peek("victory_type_id"/Int32ul), + VictoryEnum("victory_type"/Int32ul), + Peek("starting_resources_id"/Int32ul), + ResourceLevelEnum("starting_resources"/Int32ul), + "starting_age_id"/Int32ul, + "starting_age"/AgeEnum(Computed(lambda ctx: ctx.starting_age_id)), + "ending_age_id"/Int32ul, + "ending_age"/AgeEnum(Computed(lambda ctx: ctx.ending_age_id)), + "game_type"/If(lambda ctx: ctx.version >= 1006, Int32ul), + separator, + "ver1000"/If(lambda ctx: ctx.version == 1000, Struct( + "map_name"/hd_string, + "unk"/hd_string + )), + separator, + "speed"/Float32l, + "treaty_length"/Int32ul, + "population_limit"/Int32ul, + "num_players"/Int32ul, + "unused_player_color"/Int32ul, + "victory_amount"/Int32ul, + separator, + "trade_enabled"/Flag, + "team_bonus_disabled"/Flag, + "random_positions"/Flag, + "all_techs"/Flag, + "num_starting_units"/Byte, + "lock_teams"/Flag, + "lock_speed"/Flag, + "multiplayer"/Flag, + "cheats"/Flag, + "record_game"/Flag, + "animals_enabled"/Flag, + "predators_enabled"/Flag, + "turbo_enabled"/Flag, + "shared_exploration"/Flag, + "team_positions"/Flag, + "unk"/Bytes(1), + Embedded(IfThenElse(lambda ctx: ctx.version == 1000, + Struct( + Bytes(40*3), + separator, + Bytes(40), + "strings"/Array(8, hd_string), + Bytes(16), + separator, + Bytes(10), + ), + Struct( + Peek(test_57), + "players"/Array(8, player), + "fog_of_war"/Flag, + "cheat_notifications"/Flag, + "colored_chat"/Flag, + Bytes(9), + separator, + "is_ranked"/Flag, + "allow_specs"/Flag, + "lobby_visibility"/Int32ul, + "custom_random_map_file_crc"/Int32ul, + "custom_scenario_or_campaign_file"/hd_string, + Bytes(8), + "custom_random_map_file"/hd_string, + Bytes(8), + "custom_random_map_scenarion_file"/hd_string, + Bytes(8), + "guid"/Bytes(16), + "lobby_name"/hd_string, + "modded_dataset"/hd_string, + "modded_dataset_workshop_id"/Bytes(4), + If(lambda ctx: ctx._.version >= 1005, + Struct( + Bytes(4), + hd_string, + Bytes(4) + ) + ) + ) + )) +) diff --git a/mgz/header/lobby.py b/mgz/header/lobby.py index d9dc93b..c2b6f7e 100644 --- a/mgz/header/lobby.py +++ b/mgz/header/lobby.py @@ -13,7 +13,7 @@ If(lambda ctx: find_save_version(ctx) >= 13.34, Padding(5)), If(lambda ctx: find_save_version(ctx) >= 20.06, Padding(9)), Array(8, "teams"/Byte), # team number selected by each player - If(lambda ctx: ctx._.version != Version.DE, + If(lambda ctx: ctx._.version not in (Version.DE, Version.HD), Padding(1), ), Peek("reveal_map_id"/Int32ul), @@ -29,7 +29,7 @@ "lock_teams"/Flag ) )), - If(lambda ctx: ctx._.version == Version.DE, + If(lambda ctx: ctx._.version in (Version.DE, Version.HD), Struct( "treaty_length"/Byte, "cheat_codes_used"/Int32ul, diff --git a/mgz/header/map_info.py b/mgz/header/map_info.py index 31d6e16..e37be66 100644 --- a/mgz/header/map_info.py +++ b/mgz/header/map_info.py @@ -37,7 +37,7 @@ "tile_num"/Computed(lambda ctx: ctx.size_x * ctx.size_y), "zone_num"/Int32ul, Array(lambda ctx: ctx.zone_num, Struct( - IfThenElse(lambda ctx: ctx._._.version == Version.DE, + IfThenElse(lambda ctx: ctx._._.version in (Version.DE, Version.HD), Padding(lambda ctx: 2048 + (ctx._.tile_num * 2)), Padding(lambda ctx: 1275 + ctx._.tile_num) ), diff --git a/mgz/header/objects.py b/mgz/header/objects.py index edcd64e..f52c6c1 100644 --- a/mgz/header/objects.py +++ b/mgz/header/objects.py @@ -72,7 +72,7 @@ "selected_group"/If(lambda ctx: find_version(ctx) == Version.AOK, Byte), ResourceEnum("resource_type"/Int16sl), "amount"/Float32l, - "worker_count"/Byte, + "worker_count"/IfThenElse(lambda ctx: find_save_version(ctx) == 12.36, Int32ul, Byte), "current_damage"/Byte, "damaged_lately_timer"/Byte, "under_attack"/Byte, @@ -83,6 +83,17 @@ "de_static_unk1"/If(lambda ctx: find_version(ctx) == Version.DE, Bytes(19)), "has_sprite_list"/Byte, "sprite_list"/If(lambda ctx: ctx.has_sprite_list != 0, RepeatUntil(lambda x,lst,ctx: lst[-1].type == 0, sprite_list)), + "hd_extension"/If(lambda ctx: find_version(ctx) == Version.HD and find_save_version(ctx) > 12.36, Struct( + "flag"/Flag, + Padding(4), + "has_array"/Int16ul, + "array"/If(lambda ctx: ctx.has_array, Struct( + "values"/RepeatUntil(lambda x,lst,ctx: lst[-1].type == 0, Struct( + "type"/Byte, + If(lambda ctx: ctx.type > 0, Bytes(16)) + )) + )) + )), "de_extension"/If(lambda ctx: find_version(ctx) == Version.DE, Struct( "particles"/Array(5, particle), If(lambda ctx: find_save_version(ctx) >= 13.15, Bytes(5)), @@ -116,10 +127,11 @@ "waypoint"/Int32ul, "state"/Int32sl, "range"/Float32l, - "target_id"/Int32ul, + "target_id"/Int32sl, "pause_time"/Float32l, "continue_counter"/Int32ul, - "flags"/Int32ul + "flags"/Int32ul, + "hd"/If(lambda ctx: find_version(ctx) == Version.HD, Int32ul) ) vector = Struct( @@ -168,7 +180,8 @@ moving = "moving"/Struct( Embedded(base_moving), - "de"/If(lambda ctx: find_version(ctx) == Version.DE, Bytes(17)) + "hd_moving"/If(lambda ctx: find_version(ctx) == Version.HD, Bytes(1)), + "de_moving"/If(lambda ctx: find_version(ctx) == Version.DE, Bytes(17)) ) move_to = "move_to"/Struct( @@ -200,7 +213,7 @@ unit_action = Struct( "type"/Int16ul, "data"/If(lambda ctx: ctx.type > 0, Struct( - "state"/IfThenElse(lambda ctx: find_version(ctx) in [Version.AOK, Version.AOC], Byte, Int32ul), + "state"/IfThenElse(lambda ctx: find_version(ctx) in [Version.AOK, Version.AOC, Version.HD], Byte, Int32ul), "target_object_pointer"/Int32sl, "target_object_pointer2"/Int32sl, "target_object_id"/Int32sl, @@ -227,6 +240,7 @@ action = "action"/Struct( Embedded(base_moving), + "hd_action"/If(lambda ctx: find_version(ctx) == Version.HD, Bytes(3)), "waiting"/Byte, "command_flag"/Byte, "selected_group_info"/If(lambda ctx: find_version(ctx) != Version.AOK, Int16ul), @@ -372,7 +386,8 @@ "has_ai"/Int32ul, "ai"/If(lambda ctx: ctx.has_ai > 0, unit_ai), "peek"/Peek(Bytes(5)), # TODO: figure out the right way to do this part - "de_position"/If(lambda ctx: ctx.peek != b'\x00\xff\xff\xff\xff', Struct( + "hd_position"/If(lambda ctx: find_version(ctx) == Version.HD and ctx.peek != b'\x00\xff\xff\xff\xff', Bytes(4)), + "de_position"/If(lambda ctx: find_version(ctx) == Version.DE and ctx.peek != b'\x00\xff\xff\xff\xff', Struct( "position"/vector, "flag"/Byte, )), @@ -410,7 +425,7 @@ "desolid_flag"/Byte, "pending_order"/Int32ul, "linked_owner"/Int32sl, - "linked_children"/Array(lambda ctx: 3 if find_version(ctx) == Version.DE else 4, Int32sl), + "linked_children"/Array(lambda ctx: 3 if find_version(ctx) in (Version.DE, Version.HD) else 4, Int32sl), "captured_unit_count"/Byte, "extra_actions"/action_list, "research_actions"/If(lambda ctx: find_version(ctx) != Version.DE, action_list), diff --git a/mgz/header/playerstats.py b/mgz/header/playerstats.py index 0a07149..c1c65ff 100644 --- a/mgz/header/playerstats.py +++ b/mgz/header/playerstats.py @@ -211,7 +211,7 @@ "hun_wonder_bonus"/Float32l, "spies_discount"/Float32l, ))), - Embedded(If(lambda ctx: find_version(ctx) == Version.DE, Struct( + Embedded(If(lambda ctx: find_version(ctx) in (Version.DE, Version.HD), Struct( Array(this._._.num_header_data - 198, Float32l) ))), Embedded(If(lambda ctx: find_version(ctx) in [Version.USERPATCH15, Version.MCP], Struct( diff --git a/mgz/header/scenario.py b/mgz/header/scenario.py index 0bf0f1f..6008e4c 100644 --- a/mgz/header/scenario.py +++ b/mgz/header/scenario.py @@ -4,7 +4,7 @@ PascalString, Peek, String, Struct, Bytes, If, IfThenElse) from mgz.enums import DifficultyEnum, PlayerTypeEnum, AgeEnum -from mgz.util import Find, Version, find_save_version +from mgz.util import Find, Version, find_save_version, find_version # pylint: disable=invalid-name, bad-continuation @@ -69,7 +69,7 @@ "stone"/Int32ul, "unk0"/Int32ul, "unk1"/Int32ul, - If(lambda ctx: ctx._._._.version == Version.DE, + If(lambda ctx: ctx._._._.version in (Version.DE, Version.HD), "unk2"/Int32ul ) )), @@ -95,7 +95,7 @@ disables = "disables"/Struct( Padding(4), Padding(64), - IfThenElse(lambda ctx: ctx._._.version != Version.DE, + If(lambda ctx: ctx._._.version != Version.DE, Struct( Array(16, "num_disabled_techs"/Int32ul), Array(16, Array(30, Padding(4))), @@ -103,15 +103,17 @@ Array(16, Array(30, Padding(4))), Array(16, "num_disabled_buildings"/Int32ul), Array(16, Array(20, Padding(4))), - ), - Padding(196) + ) ), - Padding(12) + If(lambda ctx: ctx._._.version == Version.DE, Bytes(196)), + If(lambda ctx: ctx._._.version == Version.HD, Bytes(644)), + "padding"/Bytes(12) ) # Game settings. game_settings = "game_settings"/Struct( Array(16, AgeEnum("starting_ages"/Int32sl)), + "hd"/If(lambda ctx: find_version(ctx) == Version.HD, Bytes(16)), Padding(4), Padding(8), "map_id"/If(lambda ctx: ctx._._.version != Version.AOK, Int32ul), diff --git a/mgz/model/__init__.py b/mgz/model/__init__.py index 014a884..be3bebf 100644 --- a/mgz/model/__init__.py +++ b/mgz/model/__init__.py @@ -15,6 +15,7 @@ from mgz.summary.diplomacy import get_diplomacy_type from mgz.summary.map import get_map_data from mgz.summary.objects import TC_IDS +from mgz.util import Version def parse_match(handle): @@ -28,8 +29,9 @@ def parse_match(handle): consts = get_consts() dataset_id, dataset = get_dataset(data['version'], data['mod']) + # self._header.hd.selected_map_id if self._header.hd else self._header.scenario.game_settings.map_id map_data, encoding, language = get_map_data( - data['scenario']['map_id'], + data['hd']['map_id'] if data['version'] is Version.HD else data['scenario']['map_id'], data['scenario']['instructions'], data['map']['dimension'], data['version'], @@ -231,11 +233,11 @@ def impl(obj): return hash(obj) seen.add(obj) if type(obj) is list: - return [v for v in [impl(o) for o in obj] if v] + return [v for v in [impl(o) for o in obj] if v is not None] elif type(obj) is dict: - return {k:v for k, v in {f:impl(d) for f, d in obj.items()}.items() if v} + return {k:v for k, v in {f:impl(d) for f, d in obj.items()}.items() if v is not None} elif dataclasses.is_dataclass(obj): - return {k:v for k, v in {f.name:impl(getattr(obj, f.name)) for f in dataclasses.fields(obj)}.items() if v} + return {k:v for k, v in {f.name:impl(getattr(obj, f.name)) for f in dataclasses.fields(obj)}.items() if v is not None} elif isinstance(obj, (codecs.CodecInfo, Enum)): return obj.name elif isinstance(obj, timedelta): diff --git a/mgz/reference.py b/mgz/reference.py index 2b897b2..86c3d0a 100644 --- a/mgz/reference.py +++ b/mgz/reference.py @@ -12,6 +12,8 @@ def get_dataset(version, mod): """Fetch dataset reference data.""" if version is Version.DE: dataset_id = 100 + elif version is Version.HD: + dataset_id = 300 elif mod: dataset_id = mod[0] else: diff --git a/mgz/summary/__init__.py b/mgz/summary/__init__.py index 4d034c6..a8e4109 100644 --- a/mgz/summary/__init__.py +++ b/mgz/summary/__init__.py @@ -147,6 +147,8 @@ def _process_body(self): # pylint: disable=too-many-locals, too-many-statements, rated = len(ratings) > 0 and set(ratings.values()) != {1600} if self._header.version == Version.DE: self._cache['hash'] = hashlib.sha1(self._header.de.guid) + elif self._header.version == Version.HD and self._header.save_version >= 12.49: + self._cache['hash'] = hashlib.sha1(self._header.hd.guid) else: self._cache['hash'] = hashlib.sha1(b''.join(checksums)) \ if len(checksums) == CHECKSUMS else None @@ -155,6 +157,8 @@ def _process_body(self): # pylint: disable=too-many-locals, too-many-statements, self._cache['platform_id'] = 'voobly' if self._header.version == Version.DE and self._header.de.multiplayer: self._cache['platform_id'] = 'de' + if self._header.version == Version.HD and self._header.hd.multiplayer: + self._cache['platform_id'] = 'hd' self._cache['ladder'] = ladder self._cache['rated'] = rated self._cache['ratings'] = ratings if rated else {} @@ -207,14 +211,20 @@ def get_diplomacy(self): return get_diplomacy_data(self.get_header(), self.get_teams()) def get_profile_ids(self): - """Get map of player color to profile IDs (DE only).""" + """Get map of player color to profile IDs (DE/HD only).""" if self._header.version == Version.DE: - return { - p.player_number: p.profile_id - for p in self._header.de.players - if p.player_number >= 0 and p.profile_id > 0 - } - return {} + key = 'de' + field = 'profile_id' + elif self._header.version == Version.HD and self._header.save_version >= 12.49: + key = 'hd' + field = 'steam_id' + else: + return {} + return { + p.player_number: p[field] + for p in self._header[key].players + if p.player_number >= 0 and p[field] > 0 + } def get_players(self): """Get players.""" @@ -249,6 +259,17 @@ def get_platform(self): if self._header.version == Version.DE: lobby_name = self._header.de.lobby_name.value.decode(self.get_encoding()).strip() guid = str(uuid.UUID(bytes=self._header.de.guid)) + elif self._header.version == Version.HD and self._header.save_version >= 12.49: + lobby_name = self._header.hd.lobby_name.value.decode(self.get_encoding()).strip() + guid = str(uuid.UUID(bytes=self._header.hd.guid)) + rating_key = "hd_{}_rating".format(self._header.lobby.game_type.lower()) + for player in self._header.hd.players: + if player.player_number < 1: + continue + if rating_key not in player: + continue + self._cache['ratings'][player.name.value.decode(self.get_encoding())] = player[rating_key] + self._cache['rated'] = self._header.hd.is_ranked return { 'platform_id': self._cache['platform_id'], 'platform_match_id': guid, @@ -287,7 +308,7 @@ def get_map(self): tiles = tiles = [(tile.terrain_type, tile.elevation) for tile in self._header.map_info.tile] if not self._cache['map']: self._cache['map'], self._cache['encoding'], self._cache['language'] = get_map_data( - self._header.scenario.game_settings.map_id, + self._header.hd.selected_map_id if self._header.hd else self._header.scenario.game_settings.map_id, self._header.scenario.messages.instructions, self._header.map_info.size_x, self._header.version, diff --git a/mgz/summary/dataset.py b/mgz/summary/dataset.py index 7754208..9e620e1 100644 --- a/mgz/summary/dataset.py +++ b/mgz/summary/dataset.py @@ -5,6 +5,22 @@ from mgz.reference import get_dataset +def resolve_hd_version(hd, save_version): + """Best guess at HD version.""" + if hd.version == 1006: + if 'test_57' in hd and hd.test_57.is_57: + return '5.7' + else: + return '5.8' + if hd.version == 1005: + return '>=5.0,<5.7' + if hd.version == 1004: + return '4.8' + if save_version >= 12.36: + return '>=4.6,<4.8' + return None + + def get_dataset_data(header): """Get dataset.""" sample = header.initial.players[0].attributes.player_stats @@ -18,6 +34,12 @@ def get_dataset_data(header): 'name': 'Definitive Edition', 'version': None }, ref + elif header.version == Version.HD: + return { + 'id': 300, + 'name': 'HD Edition', + 'version': resolve_hd_version(header.hd, header.save_version) + }, ref if 'mod' in sample and sample.mod['id'] == 0 and sample.mod['version'] == '2': raise ValueError("invalid mod version") if 'mod' in sample and sample.mod['id'] > 0: diff --git a/mgz/summary/map.py b/mgz/summary/map.py index 964a66d..12dbb30 100644 --- a/mgz/summary/map.py +++ b/mgz/summary/map.py @@ -47,7 +47,9 @@ ['位置:', 'utf-8', 'zh'], ['舞台: ', 'utf-8', 'zh'], ['Vị trí: ', 'utf-8', 'vi'], - ['위치: ', 'utf-8', 'kr'] + ['위치: ', 'utf-8', 'kr'], + ['Τύπος Χάρτη: ', 'utf-8', 'gr'], + ['Emplacement: ', 'utf-8', 'fr'] ] LANGUAGE_MARKERS = [ ['Dostepne', 'ISO-8859-2', 'pl'], @@ -98,11 +100,10 @@ def lookup_name(map_id, name, version, reference): """Lookup base game map if applicable.""" custom = True is_de = version == Version.DE - if (map_id != 44 and not is_de) or (map_id != 59 and is_de): + is_hd = version == Version.HD + if (map_id != 44 and not (is_de or is_hd)) or (map_id != 59 and (is_de or is_hd)): map_keys = [int(k) for k in reference['maps'].keys()] - if is_de and map_id in map_keys: - name = reference['maps'][str(map_id)] - elif not is_de and map_id in map_keys: + if map_id in map_keys: name = reference['maps'][str(map_id)] elif version == Version.AOK: return name, False diff --git a/mgz/util.py b/mgz/util.py index 6c2ce49..9278b61 100644 --- a/mgz/util.py +++ b/mgz/util.py @@ -38,6 +38,7 @@ class Version(Enum): DE = 21 USERPATCH14RC2 = 22 MCP = 30 + HD = 19 class MgzPrefixed(Subconstruct): @@ -74,10 +75,12 @@ def get_version(game_version, save_version, log_version): if game_version == 'VER 9.4': if log_version == 3: return Version.AOC10 - if log_version == 4: - return Version.AOC10C if log_version == 5 or save_version >= 12.97: return Version.DE + if save_version >= 12.36: + return Version.HD + if log_version == 4: + return Version.AOC10C return Version.AOC if game_version == 'VER 9.8': return Version.USERPATCH12 @@ -265,6 +268,7 @@ def _parse(self, stream, context, path): # TODO: make this section more reliable marker_aok = read_bytes.find(b"\x9a\x99\x99\x3f") marker_up = read_bytes.find(b"\xf6\x28\x9c\x3f") + marker_hd = read_bytes.find(b"\xae\x47\xa1\x3f") if save_version >= 25.01: marker_de = read_bytes.find(b"\x3d\x0a\xb7\x3f") elif save_version >= 20.16: @@ -278,12 +282,14 @@ def _parse(self, stream, context, path): else: marker_de = read_bytes.find(b"\x7b\x14\xae\x3f") new_marker = -1 - if marker_up > 0 and marker_de < 0: # aok marker can appear in up + if marker_up > 0 and marker_de < 0 and marker_hd < 0: # aok marker can appear in up new_marker = marker_up - elif marker_de > 0 and marker_up < 0 and marker_aok < 0: + elif marker_de > 0 and marker_up < 0 and marker_aok < 0 and marker_hd < 0: new_marker = marker_de - elif marker_aok > 0 and marker_up < 0 and marker_de < 0: + elif marker_aok > 0 and marker_up < 0 and marker_de < 0 and marker_hd < 0: new_marker = marker_aok + elif marker_hd > 0 and marker_up < 0 and marker_de < 0 and marker_aok < 0: + new_marker = marker_hd if new_marker == -1: raise RuntimeError("could not find scenario marker") marker = new_marker diff --git a/setup.py b/setup.py index ebf05cb..fe62371 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='mgz', - version='1.5.5', + version='1.6.0', description='Parse Age of Empires 2 recorded games.', url='https://github.com/happyleavesaoc/aoc-mgz/', license='MIT', @@ -12,7 +12,7 @@ packages=find_packages(), install_requires=[ 'aiohttp>=3.6.2', - 'aocref>=1.0.5', + 'aocref>=1.0.6', 'construct==2.8.16', 'dataclasses==0.8; python_version < "3.7"', 'flatbuffers>=1.10', diff --git a/tests/recs/hd-4.6.aoe2record b/tests/recs/hd-4.6.aoe2record new file mode 100644 index 0000000..febfc4c Binary files /dev/null and b/tests/recs/hd-4.6.aoe2record differ diff --git a/tests/recs/hd-4.7.aoe2record b/tests/recs/hd-4.7.aoe2record new file mode 100644 index 0000000..4130c4b Binary files /dev/null and b/tests/recs/hd-4.7.aoe2record differ diff --git a/tests/recs/hd-4.8.aoe2record b/tests/recs/hd-4.8.aoe2record new file mode 100644 index 0000000..86e844f Binary files /dev/null and b/tests/recs/hd-4.8.aoe2record differ diff --git a/tests/recs/hd-5.0.aoe2record b/tests/recs/hd-5.0.aoe2record new file mode 100644 index 0000000..8a1377a Binary files /dev/null and b/tests/recs/hd-5.0.aoe2record differ diff --git a/tests/recs/hd-5.1.aoe2record b/tests/recs/hd-5.1.aoe2record new file mode 100644 index 0000000..510ac6c Binary files /dev/null and b/tests/recs/hd-5.1.aoe2record differ diff --git a/tests/recs/hd-5.1a.aoe2record b/tests/recs/hd-5.1a.aoe2record new file mode 100644 index 0000000..96643fa Binary files /dev/null and b/tests/recs/hd-5.1a.aoe2record differ diff --git a/tests/recs/hd-5.3.aoe2record b/tests/recs/hd-5.3.aoe2record new file mode 100644 index 0000000..04c587e Binary files /dev/null and b/tests/recs/hd-5.3.aoe2record differ diff --git a/tests/recs/hd-5.5.aoe2record b/tests/recs/hd-5.5.aoe2record new file mode 100644 index 0000000..a3e4ca6 Binary files /dev/null and b/tests/recs/hd-5.5.aoe2record differ diff --git a/tests/recs/hd-5.6.aoe2record b/tests/recs/hd-5.6.aoe2record new file mode 100644 index 0000000..b5be644 Binary files /dev/null and b/tests/recs/hd-5.6.aoe2record differ diff --git a/tests/recs/hd-5.7.aoe2record b/tests/recs/hd-5.7.aoe2record new file mode 100644 index 0000000..6a264bf Binary files /dev/null and b/tests/recs/hd-5.7.aoe2record differ diff --git a/tests/recs/hd-5.8.aoe2record b/tests/recs/hd-5.8.aoe2record new file mode 100644 index 0000000..629ce0f Binary files /dev/null and b/tests/recs/hd-5.8.aoe2record differ diff --git a/tests/test_fast.py b/tests/test_fast.py index 71fb444..156c6f2 100644 --- a/tests/test_fast.py +++ b/tests/test_fast.py @@ -19,7 +19,6 @@ def test_players(self): self.assertEqual(players[1]['diplomacy'], [0, 1, 4, -1, -1, -1, -1, -1, -1]) self.assertEqual(players[2]['diplomacy'], [0, 4, 1, -1, -1, -1, -1, -1, -1]) - def test_map(self): self.assertEqual(self.data['scenario']['map_id'], 44) @@ -41,3 +40,21 @@ def test_players(self): def test_map(self): self.assertEqual(self.data['scenario']['map_id'], 9) self.assertEqual(self.data['lobby']['seed'], -1970180596) + + +class TestFastHD(unittest.TestCase): + + @classmethod + def setUpClass(cls): + with open('tests/recs/hd-5.8.aoe2record', 'rb') as handle: + cls.data = parse(handle) + + def test_version(self): + self.assertEqual(self.data['version'], Version.HD) + + def test_players(self): + players = self.data.get('players') + self.assertEqual(len(players), 7) + + def test_map(self): + self.assertEqual(self.data['scenario']['map_id'], 0)