# Game settings. game_settings = "game_settings" / Struct( Array(16, AgeEnum("starting_ages" / Int32sl)), Padding(4), Padding(8), "map_id" / If(lambda ctx: ctx._._.version != Version.AOK, Int32ul), Peek("difficulty_id" / Int32ul), DifficultyEnum( "difficulty" / Int32ul), "lock_teams" / Int32ul, If(lambda ctx: ctx._._.version == Version.DE, Padding(29)), Array( 9, "player_info" / Struct("data_ref" / Int32ul, PlayerTypeEnum("type" / Int32ul), "name" / PascalString(lengthfield="name_length" / Int32ul))), Padding(36), Padding(4), IfThenElse( lambda ctx: ctx._._.version == Version.DE, "end_of_game_settings" / Find(b'\x9a\x99\x99\x99\x99\x99\x01\x40', None), "end_of_game_settings" / Find(b'\x9a\x99\x99\x99\x99\x99\xf9\x3f', None))) # Triggers. triggers = "triggers" / Struct( Padding(1), "num_triggers" / Int32ul, # parse if num > 0 If(lambda ctx: ctx._._.version == Version.DE, Padding(1032))) # Scenario metadata. scenario = "scenario" / Struct( scenario_header, messages, scenario_players, victory,
# Objects that exist on the map at the start of the recorded game existing_object = "objects" / Struct( "type" / Byte, "player_id" / Byte, Embedded("properties" / Switch(lambda ctx: ctx.type, { 10: static, 20: animated, 25: animated, 30: moving, 40: action, 50: base_combat, 60: missile, 70: combat, 80: building, 90: static }, default=Pass)), ) # Default values for objects, nothing of real interest default_object = "default_object" / Struct( ObjectTypeEnum("type" / Byte), Padding(14), "properties" / Switch(lambda ctx: ctx.type, { "gaia": Padding(24), "object": Padding(32), "fish": Padding(32), "other": Padding(28), }, default="end_of_object" / Find(b'\x21\x16', 200)))
# The following sections can be refined with further research. # There are clearly some patterns. "has_extra"/Byte, If(lambda ctx: ctx.has_extra == 2, Padding( 34 )), Padding(4) ) # Units - typically villagers, scout, and any sheep within LOS unit = "unit"/Struct( Embedded(existing_object_header), # Not pretty, but we don't know how to parse a unit yet. # Also, this only works on non-restored games. # This isn't a constant footer, these are actually initial values of some sort. "end_of_unit"/Find(b'\xff\xff\xff\xff\x00\x00\x80\xbf\x00\x00\x80\xbf\xff\xff\xff' \ b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00\x00\x00\x00\x00\x00', None) ) # Buildings - ID doesn't match Build action ID - buildings can have multiple parts building = "building"/Struct( Embedded(existing_object_header), Padding(32), # The following sections can be refined with further research. # There are clearly some patterns. "has_extra"/Byte, If(lambda ctx: ctx.has_extra == 2, Padding( 17 )), Padding(16), "has_extra2"/Byte, If(lambda ctx: ctx.has_extra2 == 1, Padding(
lambda ctx: ctx.num_saved_views, "saved_view" / Struct("camera_x" / Float32l, "camera_y" / Float32l))), ))), "spawn_location" / Struct("x" / Int16ul, "y" / Int16ul), "culture" / Byte, "civilization" / Byte, "game_status" / Byte, "resigned" / Flag, Padding(1), "player_color" / Byte, Padding(1)) # Initial state of players, including Gaia. player = "players" / Struct( "type" / Byte, "unk" / Byte, attributes, "end_of_attr" / Tell, "start_of_objects" / Find(b'\x0b\x00\x08\x00\x00\x00\x02\x00\x00', None), Embedded("x" / Struct("end_of_objects" / GotoObjectsEnd(), Array(0, existing_object)))) x = Struct( "start_of_objects" / Find(b'\x0b\x00\x08\x00\x00\x00\x02\x00\x00', None), # If this isn't a restored game, we can read all the existing objects Embedded("not_restored" / If( this._.restore_time == 0 and this._._.version != Version.DE, Struct( RepeatUpTo(b'\x00', existing_object), Padding(14), "eoo" / Tell, "end_of_objects" / GotoObjectsEnd() # Find the objects end just in case ))), # Can't parse existing objects in a restored game, skip the whole structure
"civilization" / Byte, "game_status" / Byte, "resigned" / Flag, Padding(1), "player_color" / Byte, Padding(1), ) # Initial state of players, including Gaia. player = "players" / Struct( "type" / Byte, "unk" / Byte, attributes, "end_of_attr" / Tell, "start_of_objects" / Find([ b'\x0b\x00\x08\x00\x00\x00\x02\x00\x00', b'\x0b\x00\x0e\x00\x00\x00\x02\x00\x00' ], None), Embedded( IfThenElse( lambda ctx: ctx._.restore_time == 0, Struct( "objects" / RepeatUpTo(b'\x00', existing_object), Const(b'\x00\x0b'), # Skip Gaia trees for performance reasons Embedded( IfThenElse( this._.type != 2, Struct( "s_size" / Int32ul, "s_grow" / Int32ul, "sleeping_objects" / RepeatUpTo(b'\x00', existing_object),
Array( lambda ctx: ctx.num_saved_views, "saved_view" / Struct("camera_x" / Float32l, "camera_y" / Float32l))), "map_size" / Struct("x" / Int16ul, "y" / Int16ul), "culture" / Byte, "civilization" / Byte, "game_status" / Byte, "resigned" / Flag, Padding(1), "player_color" / Byte, Padding(1)) # Initial state of players, including Gaia. player = "players" / Struct( attributes, "start_of_objects" / Find(b'\x0b\x00\x08\x00\x00\x00\x02\x00\x00', None), # If this isn't a restored game, we can read all the existing objects Embedded("not_restored" / If( this._.restore_time == 0, Struct( RepeatUpTo(b'\x00', existing_object), Padding(14), "end_of_objects" / GotoObjectsEnd() # Find the objects end just in case ))), # Can't parse existing objects in a restored game, skip the whole structure Embedded("is_restored" / If( this._.restore_time > 0, Struct( "end_of_objects" / GotoObjectsEnd(),
If(lambda ctx: find_save_version(ctx) >= 13.07, Padding(1)), If(lambda ctx: find_save_version(ctx) >= 13.34, Padding(132)), If(lambda ctx: find_save_version(ctx) >= 20.06, Padding(1)), If(lambda ctx: find_save_version(ctx) >= 20.16, Padding(4)), If(lambda ctx: find_save_version(ctx) >= 25.02, Padding(4 * 16)), If(lambda ctx: find_save_version(ctx) >= 25.06, Padding(4)))), Array( 9, "player_info" / Struct("data_ref" / Int32ul, PlayerTypeEnum("type" / Int32ul), "name" / PascalString(lengthfield="name_length" / Int32ul))), Padding(36), Padding(4), IfThenElse( lambda ctx: ctx._._.version == Version.DE, Struct( If(lambda ctx: find_save_version(ctx) < 13.34, Find(struct.pack('<d', 2.2), None)), If(lambda ctx: 25.06 > find_save_version(ctx) >= 13.34, Find(struct.pack('<d', 2.4), None)), If(lambda ctx: 25.22 > find_save_version(ctx) >= 25.06, Find(struct.pack('<d', 2.5), None)), If(lambda ctx: 26.16 > find_save_version(ctx) >= 25.22, Find(struct.pack('<d', 2.6), None)), If(lambda ctx: find_save_version(ctx) >= 26.16, Find(struct.pack('<d', 3.0), None))), "end_of_game_settings" / Find(b'\x9a\x99\x99\x99\x99\x99\xf9\\x3f', None))) # Triggers. triggers = "triggers" / Struct( Padding(1), "num_triggers" / Int32ul, # parse if num > 0
ResourceEnum("resource_type"/Int16sl), "amount"/Float32l, "worker_count"/IfThenElse(lambda ctx: find_save_version(ctx) == 12.36, Int32ul, Byte), "current_damage"/Byte, "damaged_lately_timer"/Byte, "under_attack"/Byte, "pathing_group_len"/Int32ul, "pathing_group"/Array(lambda ctx: ctx.pathing_group_len, "object_id"/Int32ul), "group_id"/Int32sl, "roo_already_called"/Byte, "de_static_unk1"/If(lambda ctx: find_version(ctx) == Version.DE, Bytes(17)), If(lambda ctx: find_save_version(ctx) >= 26.16, Byte), "de_has_object_props"/If(lambda ctx: find_version(ctx) == Version.DE, Int16ul), "de_object_props"/IfThenElse(lambda ctx: ctx.de_has_object_props == 1, Struct( Bytes(162), Find(b'`\n\x00\x00`\n\x00\x00', 1000), # Skip a large unparseable block that (likely) contains RMS-modified object data Bytes(2) ), Struct( "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)), "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)), If(lambda ctx: find_save_version(ctx) >= 13.17, Bytes(2)), If(lambda ctx: find_save_version(ctx) >= 13.34, Struct( Bytes(2), de_string, de_string, Bytes(2) )) ))
If(lambda ctx: find_save_version(ctx) >= 25.06, Padding(4)), )), Array( 9, "player_info" / Struct("data_ref" / Int32ul, PlayerTypeEnum("type" / Int32ul), "name" / PascalString(lengthfield="name_length" / Int32ul))), Padding(36), Padding(4), IfThenElse( lambda ctx: ctx._._.version == Version.DE, IfThenElse( lambda ctx: find_save_version(ctx) >= 13.34, IfThenElse( lambda ctx: find_save_version(ctx) >= 25.06, "end_of_game_settings" / Find(b'\x00\x00\x00\x00\x00\x00\x04\x40', None), # double: 2.5 "end_of_game_settings" / Find(b'\x33\x33\x33\x33\x33\x33\x03\x40', None) # double: 2.4 ), "end_of_game_settings" / Find(b'\x9a\x99\x99\x99\x99\x99\x01\x40', None) # double: 2.2 ), "end_of_game_settings" / Find(b'\x9a\x99\x99\x99\x99\x99\xf9\\x3f', None) # double: 1.6 )) # Triggers. triggers = "triggers" / Struct( Padding(1), "num_triggers" / Int32ul, # parse if num > 0
Array( this.num_rules, "rules" / Struct( Padding(12), "num_facts" / Byte, "num_facts_actions" / Byte, Padding(2), Array( 16, "data" / Struct("type" / Int32ul, "id" / Int16ul, Padding( 2), Array(4, "params" / Int32ul)))))) ai = "ai" / Struct( "has_ai" / Int32ul, # if true, parse AI "yep" / If( this.has_ai == 1, IfThenElse( lambda ctx: ctx._.version == Version.DE, Find( b'\00' * 4096, None ), # The ai structure in DE seems to have changed, for now we simply skip it "ais" / Struct( "max_strings" / Int16ul, "num_strings" / Int16ul, Padding(4), Array( this.num_strings, "strings" / PascalString(lengthfield="name_length" / Int32ul, encoding='latin1')), Padding(6), Array(8, script), Padding(104), Array(80, "timers" / Int32sl), Array(256, "shared_goals" / Int32sl), Padding(4096),