Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ venv/
.tox/
*.pyc
*.sqlite
/.vs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Visual Studio to work

1 change: 1 addition & 0 deletions mgz/header/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
If(lambda ctx: find_save_version(ctx) >= 50, Bytes(8)),
If(lambda ctx: find_save_version(ctx) >= 61.5, Flag),
If(lambda ctx: find_save_version(ctx) >= 63, Bytes(5)),
If(lambda ctx: find_save_version(ctx) >= 64.3 and ctx.game_type == 5, Bytes(4)), # Campaign ?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crashed on the campaign replay parsing here, could use a check with previous version of the game

"unknown_count"/If(lambda ctx: find_save_version(ctx) >= 66.3, Int32ul),
If(lambda ctx: find_save_version(ctx) >= 66.3, Bytes(12)),
If(lambda ctx: find_save_version(ctx) >= 66.3, Array(lambda ctx: ctx.unknown_count, Bytes(4))),
Expand Down
13 changes: 7 additions & 6 deletions mgz/header/lobby.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@

# Player inputs in the lobby, and several host settings.
lobby = "lobby"/Struct(
If(lambda ctx: find_save_version(ctx) >= 13.34, Padding(5)),
If(lambda ctx: find_save_version(ctx) >= 20.06, Padding(9)),
If(lambda ctx: find_save_version(ctx) >= 26.16, Bytes(5)),
If(lambda ctx: find_save_version(ctx) >= 37, Bytes(8)),
If(lambda ctx: find_save_version(ctx) >= 64.3, Bytes(16)),
If(lambda ctx: find_save_version(ctx) >= 66.3, Bytes(1)),
#If(lambda ctx: find_save_version(ctx) >= 13.34, Padding(5)),
#If(lambda ctx: find_save_version(ctx) >= 20.06, Padding(9)),
#If(lambda ctx: find_save_version(ctx) >= 26.16, Bytes(5)),
#If(lambda ctx: find_save_version(ctx) >= 37, Bytes(8)),
#If(lambda ctx: find_save_version(ctx) >= 64.3, Bytes(16)),
#If(lambda ctx: find_save_version(ctx) >= 66.3, Bytes(1)),
# We ignore those previous data for now they were making the GoToLobbyStart more complex without being meaningfull,
Array(8, "teams"/Byte), # team number selected by each player
If(lambda ctx: ctx._.version not in (Version.DE, Version.HD),
Padding(1),
Expand Down
140 changes: 124 additions & 16 deletions mgz/header/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
PascalString, Peek, String, Struct, Bytes, If, IfThenElse)

from mgz.enums import DifficultyEnum, PlayerTypeEnum, AgeEnum
from mgz.util import Find, Version, find_save_version, find_version
from mgz.header.objects import de_string
from mgz.util import Find, GoToLobbyStart,Version, find_save_version, find_version

# pylint: disable=invalid-name, bad-continuation

Expand All @@ -26,12 +27,11 @@
"constant"/Int32ul, # 0x04 0x00 0x00 0x00
)),
Padding(5),
"elapsed_time"/Float32l,
"scenario_filename"/PascalString(lengthfield="scenario_filename_length"/Int16ul),
If(lambda ctx: ctx._._.version == Version.DE, Struct(
"elapsed_time"/Float32l, # We should try a record with a determined starting time to see if those byte are before elapsed_time or not
If(lambda ctx: ctx._._.version == Version.DE, Struct(
Padding(64),
# If(lambda ctx: find_save_version(ctx) >= 13.34, Padding(64)) 4*16 = 64
))
)),
"scenario_filename"/PascalString(lengthfield="scenario_filename_length"/Int16ul),
)

# Scenarios have intro text, a bitmap, and cinematics.
Expand All @@ -42,8 +42,7 @@
"defeat_id"/Int32sl,
"history_id"/Int32sl,
"scouts_id"/If(lambda ctx: ctx._._.version != Version.AOK, Int32sl),
"instructions_length"/Int16ul,
"instructions"/Bytes(lambda ctx: ctx.instructions_length),
"instructions"/PascalString(lengthfield="instructions_length"/Int16ul),
"hints"/PascalString(lengthfield="hints_length"/Int16ul),
"victory"/PascalString(lengthfield="victory_length"/Int16ul),
"defeat"/PascalString(lengthfield="defeat_length"/Int16ul),
Expand All @@ -65,6 +64,7 @@
scenario_players = "players"/Struct(
Array(16, "ai_names"/PascalString(lengthfield="ai_name_length"/Int16ul)),
Array(16, "ai"/Struct(Padding(8), "file"/PascalString(lengthfield="ai_file_length"/Int32ul))),
If(lambda ctx: ctx._._.version == Version.DE, Array(16, Padding(1))), # 16 byte 0x00 or 0x01 I think checking with a coop campaign could help understand those value, they could be opened slots
Padding(4),
Array(16, "resources"/Struct(
"gold"/Int32ul,
Expand All @@ -77,7 +77,7 @@
"unk2"/Int32ul
)
)),
Array(16, Padding(1)) # 0x01 x 16
If(lambda ctx: ctx._._.version != Version.DE, Array(16, Padding(1))) # 0x01 * 16
)

# Victory conditions.
Expand Down Expand Up @@ -109,24 +109,130 @@
Array(16, Array(20, Padding(4))),
)
),
If(lambda ctx: ctx._._.version == Version.DE, Bytes(196)),
If(lambda ctx: ctx._._.version == Version.DE,
Struct(
"unk_bytes"/Bytes(4),
# I can't find a nice way to do this with construct feel free to edit it, make it nicer
"p1_num_disabled_techs"/Int32ul,
"p2_num_disabled_techs"/Int32ul,
"p3_num_disabled_techs"/Int32ul,
"p4_num_disabled_techs"/Int32ul,
"p5_num_disabled_techs"/Int32ul,
"p6_num_disabled_techs"/Int32ul,
"p7_num_disabled_techs"/Int32ul,
"p8_num_disabled_techs"/Int32ul,
"p9_num_disabled_techs"/Int32ul,
"p10_num_disabled_techs"/Int32ul,
"p11_num_disabled_techs"/Int32ul,
"p12_num_disabled_techs"/Int32ul,
"p13_num_disabled_techs"/Int32ul,
"p14_num_disabled_techs"/Int32ul,
"p15_num_disabled_techs"/Int32ul,
"p16_num_disabled_techs"/Int32ul,
"p1_disabled_techs"/Array(lambda ctx: ctx.p1_num_disabled_techs, Int32ul),
"p2_disabled_techs"/Array(lambda ctx: ctx.p2_num_disabled_techs, Int32ul),
"p3_disabled_techs"/Array(lambda ctx: ctx.p3_num_disabled_techs, Int32ul),
"p4_disabled_techs"/Array(lambda ctx: ctx.p4_num_disabled_techs, Int32ul),
"p5_disabled_techs"/Array(lambda ctx: ctx.p5_num_disabled_techs, Int32ul),
"p6_disabled_techs"/Array(lambda ctx: ctx.p6_num_disabled_techs, Int32ul),
"p7_disabled_techs"/Array(lambda ctx: ctx.p7_num_disabled_techs, Int32ul),
"p8_disabled_techs"/Array(lambda ctx: ctx.p8_num_disabled_techs, Int32ul),
"p9_disabled_techs"/Array(lambda ctx: ctx.p9_num_disabled_techs, Int32ul),
"p10_disabled_techs"/Array(lambda ctx: ctx.p10_num_disabled_techs, Int32ul),
"p11_disabled_techs"/Array(lambda ctx: ctx.p11_num_disabled_techs, Int32ul),
"p12_disabled_techs"/Array(lambda ctx: ctx.p12_num_disabled_techs, Int32ul),
"p13_disabled_techs"/Array(lambda ctx: ctx.p13_num_disabled_techs, Int32ul),
"p14_disabled_techs"/Array(lambda ctx: ctx.p14_num_disabled_techs, Int32ul),
"p15_disabled_techs"/Array(lambda ctx: ctx.p15_num_disabled_techs, Int32ul),
"p16_disabled_techs"/Array(lambda ctx: ctx.p16_num_disabled_techs, Int32ul),

"p1_num_disabled_units"/Int32ul,
"p2_num_disabled_units"/Int32ul,
"p3_num_disabled_units"/Int32ul,
"p4_num_disabled_units"/Int32ul,
"p5_num_disabled_units"/Int32ul,
"p6_num_disabled_units"/Int32ul,
"p7_num_disabled_units"/Int32ul,
"p8_num_disabled_units"/Int32ul,
"p9_num_disabled_units"/Int32ul,
"p10_num_disabled_units"/Int32ul,
"p11_num_disabled_units"/Int32ul,
"p12_num_disabled_units"/Int32ul,
"p13_num_disabled_units"/Int32ul,
"p14_num_disabled_units"/Int32ul,
"p15_num_disabled_units"/Int32ul,
"p16_num_disabled_units"/Int32ul,
"p1_disabled_units"/Array(lambda ctx: ctx.p1_num_disabled_units, Int32ul),
"p2_disabled_units"/Array(lambda ctx: ctx.p2_num_disabled_units, Int32ul),
"p3_disabled_units"/Array(lambda ctx: ctx.p3_num_disabled_units, Int32ul),
"p4_disabled_units"/Array(lambda ctx: ctx.p4_num_disabled_units, Int32ul),
"p5_disabled_units"/Array(lambda ctx: ctx.p5_num_disabled_units, Int32ul),
"p6_disabled_units"/Array(lambda ctx: ctx.p6_num_disabled_units, Int32ul),
"p7_disabled_units"/Array(lambda ctx: ctx.p7_num_disabled_units, Int32ul),
"p8_disabled_units"/Array(lambda ctx: ctx.p8_num_disabled_units, Int32ul),
"p9_disabled_units"/Array(lambda ctx: ctx.p9_num_disabled_units, Int32ul),
"p10_disabled_units"/Array(lambda ctx: ctx.p10_num_disabled_units, Int32ul),
"p11_disabled_units"/Array(lambda ctx: ctx.p11_num_disabled_units, Int32ul),
"p12_disabled_units"/Array(lambda ctx: ctx.p12_num_disabled_units, Int32ul),
"p13_disabled_units"/Array(lambda ctx: ctx.p13_num_disabled_units, Int32ul),
"p14_disabled_units"/Array(lambda ctx: ctx.p14_num_disabled_units, Int32ul),
"p15_disabled_units"/Array(lambda ctx: ctx.p15_num_disabled_units, Int32ul),
"p16_disabled_units"/Array(lambda ctx: ctx.p16_num_disabled_units, Int32ul),

"p1_num_disabled_buildings"/Int32ul,
"p2_num_disabled_buildings"/Int32ul,
"p3_num_disabled_buildings"/Int32ul,
"p4_num_disabled_buildings"/Int32ul,
"p5_num_disabled_buildings"/Int32ul,
"p6_num_disabled_buildings"/Int32ul,
"p7_num_disabled_buildings"/Int32ul,
"p8_num_disabled_buildings"/Int32ul,
"p9_num_disabled_buildings"/Int32ul,
"p10_num_disabled_buildings"/Int32ul,
"p11_num_disabled_buildings"/Int32ul,
"p12_num_disabled_buildings"/Int32ul,
"p13_num_disabled_buildings"/Int32ul,
"p14_num_disabled_buildings"/Int32ul,
"p15_num_disabled_buildings"/Int32ul,
"p16_num_disabled_buildings"/Int32ul,
"p1_disabled_buildings"/Array(lambda ctx: ctx.p1_num_disabled_buildings, Int32ul),
"p2_disabled_buildings"/Array(lambda ctx: ctx.p2_num_disabled_buildings, Int32ul),
"p3_disabled_buildings"/Array(lambda ctx: ctx.p3_num_disabled_buildings, Int32ul),
"p4_disabled_buildings"/Array(lambda ctx: ctx.p4_num_disabled_buildings, Int32ul),
"p5_disabled_buildings"/Array(lambda ctx: ctx.p5_num_disabled_buildings, Int32ul),
"p6_disabled_buildings"/Array(lambda ctx: ctx.p6_num_disabled_buildings, Int32ul),
"p7_disabled_buildings"/Array(lambda ctx: ctx.p7_num_disabled_buildings, Int32ul),
"p8_disabled_buildings"/Array(lambda ctx: ctx.p8_num_disabled_buildings, Int32ul),
"p9_disabled_buildings"/Array(lambda ctx: ctx.p9_num_disabled_buildings, Int32ul),
"p10_disabled_buildings"/Array(lambda ctx: ctx.p10_num_disabled_buildings, Int32ul),
"p11_disabled_buildings"/Array(lambda ctx: ctx.p11_num_disabled_buildings, Int32ul),
"p12_disabled_buildings"/Array(lambda ctx: ctx.p12_num_disabled_buildings, Int32ul),
"p13_disabled_buildings"/Array(lambda ctx: ctx.p13_num_disabled_buildings, Int32ul),
"p14_disabled_buildings"/Array(lambda ctx: ctx.p14_num_disabled_buildings, Int32ul),
"p15_disabled_buildings"/Array(lambda ctx: ctx.p15_num_disabled_buildings, Int32ul),
"p16_disabled_buildings"/Array(lambda ctx: ctx.p16_num_disabled_buildings, Int32ul),
)
),
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)),
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(4), # 0x9d 0xff 0xff 0xff const
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,
"de_data"/If(lambda ctx: ctx._._.version == Version.DE,
Struct(
Padding(29),
Padding(12),
de_string,
de_string,
Padding(9),
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)),
Expand Down Expand Up @@ -167,7 +273,9 @@
"num_triggers"/Int32ul,
# parse if num > 0
"de"/If(lambda ctx: ctx._._.version == Version.DE,
Padding(1032)
#Padding(1032)
# We ignore trigger and jump to lobby, we know lobby contains repeated data we already have (revealed map, fog of war, map size and population limit)
"end_of_triggers"/GoToLobbyStart()
)
)

Expand Down
35 changes: 35 additions & 0 deletions mgz/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,41 @@ def _parse(self, stream, context, path):
stream.seek(end)
return end

class GoToLobbyStart(Construct):
"""Find the start of the lobby part or the end of the scenario part

Helpfull since triggers are going to be tough to parse
"""

def _parse(self, stream, context, path):
reveal_map = context._._.de.reveal_map
fog_of_war = context._._.de.fog_of_war
map_size = context._._.map_info.size_x
population_limit = context._._.de.population_limit
save_version = context._._.save_version
version = find_version(context)
start = stream.tell()
# Have to read everything to be able to use find()
read_bytes = stream.read()
spot = -1
if save_version >= 61.5:
spot = read_bytes.find(struct.pack('<I',int(reveal_map)) + struct.pack('<I', int(fog_of_war)) + struct.pack('<I',int(map_size)) + struct.pack('<I', int(population_limit)))
else:
pattern = re.search(struct.pack('<I', int(reveal_map)) + struct.pack('<I', int(fog_of_war)) + b'....' + struct.pack('<I', int(population_limit)), read_bytes, re.DOTALL)
spot = pattern.start()
end = -1
if spot > 0:
backtrack = 0
if version != Version.DE and version != Version.HD:
backtrack += 1
backtrack += 8 # Teams
end = start + spot - backtrack
stream.seek(end)
else:
raise ValueError("Can't find lobby start")
# if it doesn't exist we are cooked
return end


def find_postgame(data, size):
"""Find postgame and grab duration.
Expand Down
Binary file added tests/recs/de-64.3-campaign.aoe2record
Binary file not shown.