Skip to content

Commit deea5c4

Browse files
support 61.5, 62.0
1 parent 2d38f29 commit deea5c4

13 files changed

Lines changed: 88 additions & 30 deletions

File tree

mgz/fast/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class Action(Enum):
4444
DE_UNKNOWN_40 = 40
4545
DE_TRANSFORM = 41
4646
RATHA_ABILITY = 43
47+
DE_107_A = 44
4748
AI_COMMAND = 53
4849
DE_UNKNOWN_80 = 80
4950
MAKE = 100
@@ -74,6 +75,7 @@ class Action(Enum):
7475
DE_UNKNOWN_135 = 135
7576
DE_UNKNOWN_136 = 136
7677
DE_UNKNOWN_138 = 138
78+
DE_107_B = 140
7779
DE_TRIBUTE = 196
7880
POSTGAME = 255
7981

mgz/fast/header.py

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ def parse_mod(header, num_players, version):
110110

111111
def parse_player(header, player_number, num_players, save):
112112
"""Parse a player (and objects)."""
113-
type_, *diplomacy, name_length = unpack(f'<bx{num_players}x9i5xh', header)
113+
rep = 9
114+
if save >= 61.5:
115+
rep = num_players
116+
type_, *diplomacy, name_length = unpack(f'<bx{num_players}x{rep}i5xh', header)
114117
name, resources = unpack(f'<{name_length - 1}s2xIx', header)
115118
header.read(resources * 4)
116119
start_x, start_y, civilization_id, color_id = unpack('<xff9xb3xbx', header)
@@ -133,12 +136,16 @@ def parse_player(header, player_number, num_players, save):
133136
if save >= 37:
134137
offset = header.tell()
135138
data = header.read(100)
139+
device = data[8]
136140
# Jump to the end of player data
137141
player_end = re.search(b'\xff\xff\xff\xff\xff\xff\xff\xff.\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b', data, re.DOTALL)
138142
if not player_end:
139-
raise RuntimeError("could not find player end")
140-
device = data[8]
141-
header.seek(offset + player_end.end())
143+
# only a failure if this is not the last player, since we seek to the next block anyway
144+
# this issue happens on restored games
145+
if player_number < num_players - 1:
146+
raise RuntimeError("could not find player end")
147+
else:
148+
header.seek(offset + player_end.end())
142149

143150
return dict(
144151
number=player_number,
@@ -194,12 +201,13 @@ def parse_lobby(data, version, save):
194201
)
195202

196203

197-
def parse_map(data, version):
204+
def parse_map(data, version, save):
198205
"""Parse map."""
199-
data.read(60)
200206
tile_format = '<xbbx'
201207
if version is Version.DE:
202208
tile_format = '<bxb6x'
209+
if save >= 62.0:
210+
tile_format = '<bxxb6x'
203211
data.read(8)
204212
size_x, size_y, zone_num = unpack('<III', data)
205213
tile_num = size_x * size_y
@@ -220,6 +228,8 @@ def parse_map(data, version):
220228
data.read(num_obs * 8)
221229
x2, y2 = unpack('<II', data)
222230
data.read(x2 * y2 * 4)
231+
if save >= 61.5:
232+
data.read(x2 * y2 * 4)
223233
restore_time = unpack('<I', data)
224234
#if restore_time > 0:
225235
# raise RuntimeError("restored matches can't be parsed yet")
@@ -233,7 +243,10 @@ def parse_map(data, version):
233243

234244
def parse_scenario(data, num_players, version, save):
235245
"""Parse scenario section."""
236-
data.read(4455)
246+
next_uid, scenario_version = unpack('<II', data)
247+
if save >= 61.5:
248+
data.read(72)
249+
data.read(4447)
237250
scenario_filename = None
238251
if version is Version.DE:
239252
data.read(102)
@@ -264,7 +277,9 @@ def parse_scenario(data, num_players, version, save):
264277
map_id, difficulty_id = unpack('<II', data)
265278
remainder = data.read()
266279
if version is Version.DE:
267-
if save >= 37:
280+
if save >= 61.5:
281+
settings_version = 3.6
282+
elif save >= 37:
268283
settings_version = 3.5
269284
elif save >= 26.21:
270285
settings_version = 3.2
@@ -346,20 +361,26 @@ def parse_de(data, version, save, skip=False):
346361
for i in range(0, unpack('<I', data)):
347362
dlc_ids.append(unpack('<I', data))
348363
data.read(4)
349-
difficulty_id = unpack('<I', data)
364+
if save >= 61.5:
365+
map_dimension = unpack('<I', data)
366+
else:
367+
difficulty_id = unpack('<I', data)
350368
data.read(4)
351369
rms_map_id = unpack('<I', data)
352370
data.read(4)
353371
victory_type_id = unpack('<I', data)
354372
starting_resources_id = unpack('<I', data)
355373
starting_age_id = unpack('<I', data)
356374
ending_age_id = unpack('<I', data)
357-
data.read(8)
358-
speed = unpack('<d', data)
375+
data.read(12)
376+
speed = unpack('<f', data)
359377
treaty_length = unpack('<I', data)
360378
population_limit = unpack('<I', data)
361379
num_players = unpack('<I', data)
362380
data.read(14)
381+
if save >= 61.5:
382+
# not sure if this is difficulty under 61.5 or not
383+
difficulty_id = unpack('<B', data)
363384
random_positions, all_technologies = unpack('<bb', data)
364385
data.read(1)
365386
lock_teams = unpack('<b', data)
@@ -385,6 +406,8 @@ def parse_de(data, version, save, skip=False):
385406
team_id = unpack('<b', data)
386407
data.read(9)
387408
civilization_id = unpack('<I', data)
409+
if save >= 61.5:
410+
data.read(4)
388411
de_string(data)
389412
data.read(1)
390413
ai_name = de_string(data)
@@ -412,6 +435,8 @@ def parse_de(data, version, save, skip=False):
412435
data.read(12)
413436
if save >= 37:
414437
for _ in range(8 - num_players):
438+
if save >= 61.5:
439+
data.read(4)
415440
data.read(12)
416441
de_string(data)
417442
data.read(1)
@@ -463,6 +488,8 @@ def parse_de(data, version, save, skip=False):
463488
data.read(3)
464489
if save > 50:
465490
data.read(8)
491+
if save >= 61.5:
492+
data.read(1)
466493
if not skip:
467494
de_string(data)
468495
data.read(8)
@@ -595,11 +622,17 @@ def parse_players(header, num_players, version, save):
595622
cur = header.tell()
596623
gaia = b'Gaia' if version in (Version.DE, Version.HD) else b'GAIA'
597624
anchor = header.read().find(b'\x05\x00' + gaia + b'\x00')
598-
header.seek(cur + anchor - num_players - 43)
625+
rev = 43
626+
if save >= 61.5:
627+
rev = 7 + (num_players * 4)
628+
header.seek(cur + anchor - num_players - rev)
599629
mod = parse_mod(header, num_players, version)
600630
players = [parse_player(header, number, num_players, save) for number in range(num_players)]
601631
cur = header.tell()
602-
points_version = header.read().find(b'\x00\x00\x00@')
632+
pv = b'\x00\x00\x00@'
633+
if save >= 61.5:
634+
pv = b'\x66\x66\x06\x40'
635+
points_version = header.read().find(pv)
603636
header.seek(cur)
604637
header.read(points_version)
605638
for _ in range(num_players):
@@ -611,7 +644,7 @@ def parse_players(header, num_players, version, save):
611644
return [p[0] for p in players], mod, players[0][1]
612645

613646

614-
def parse_metadata(header, skip_ai=True):
647+
def parse_metadata(header, save, skip_ai=True):
615648
"""Parse recorded game metadata."""
616649
ai = unpack('<I', header)
617650

@@ -630,6 +663,12 @@ def parse_metadata(header, skip_ai=True):
630663
header.seek(offset + ai_end.end())
631664

632665
game_speed, owner_id, num_players, cheats = unpack('<24xf17xhbxb', header)
666+
667+
if save < 61.5:
668+
header.read(60)
669+
else:
670+
header.read(24 + (num_players * 4))
671+
633672
return dict(
634673
speed=game_speed,
635674
owner_id=owner_id,
@@ -646,8 +685,8 @@ def parse(data):
646685
raise RuntimeError(f"{version} not supported")
647686
de = parse_de(header, version, save)
648687
hd = parse_hd(header, version, save)
649-
metadata, num_players = parse_metadata(header)
650-
map_ = parse_map(header, version)
688+
metadata, num_players = parse_metadata(header, save)
689+
map_ = parse_map(header, version, save)
651690
players, mod, device = parse_players(header, num_players, version, save)
652691
scenario = parse_scenario(header, num_players, version, save)
653692
lobby = parse_lobby(header, version, save)

mgz/header/de.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dat_crc"/Bytes(8),
2929
"mp_game_version"/Byte,
3030
"civ_id"/Int32ul,
31+
"unk"/If(lambda ctx: find_save_version(ctx) >= 61.5, Int32ul),
3132
"ai_type"/de_string,
3233
"ai_civ_name_index"/Byte,
3334
"ai_name"/de_string,
@@ -59,8 +60,8 @@
5960
"dlc_count"/Int32ul,
6061
"dlc_ids"/Array(lambda ctx: ctx.dlc_count, Int32ul),
6162
"dataset_ref"/Int32ul,
62-
Peek("difficulty_id"/Int32ul),
63-
DifficultyEnum("difficulty"/Int32ul),
63+
Peek("difficulty_id"/Int32ul), # map size now?
64+
"diff"/Int32ul,
6465
"selected_map_id"/Int32ul,
6566
"resolved_map_id"/Int32ul,
6667
"reveal_map"/Int32ul,
@@ -81,6 +82,7 @@
8182
"num_players"/Int32ul,
8283
"unused_player_color"/Int32ul,
8384
"victory_amount"/Int32sl,
85+
"unk_byte"/If(lambda ctx: find_save_version(ctx) >= 61.5, Flag),
8486
separator,
8587
"trade_enabled"/Flag,
8688
"team_bonus_disabled"/Flag,
@@ -108,6 +110,7 @@
108110
"cheat_notifications"/Flag,
109111
"colored_chat"/Flag,
110112
"empty_slots"/If(lambda ctx: find_save_version(ctx) >= 37, Array(lambda ctx: 8 - ctx.num_players, Struct(
113+
"unk"/If(lambda ctx: find_save_version(ctx) >= 61.5, Int32ul),
111114
"i0x"/Int32ul,
112115
"i0a"/Int32ul,
113116
"i0b"/Int32ul,
@@ -154,6 +157,7 @@
154157
If(lambda ctx: find_save_version(ctx) >= 26.16, Bytes(8)),
155158
If(lambda ctx: find_save_version(ctx) >= 37, Bytes(3)),
156159
If(lambda ctx: find_save_version(ctx) >= 50, Bytes(8)),
160+
If(lambda ctx: find_save_version(ctx) >= 61.5, Flag),
157161
de_string,
158162
Bytes(5),
159163
If(lambda ctx: find_save_version(ctx) >= 13.13, Byte),

mgz/header/initial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88
from mgz.enums import MyDiplomacyEnum, TheirDiplomacyEnum
99
from mgz.header.objects import existing_object
1010
from mgz.header.playerstats import player_stats
11-
from mgz.util import Find, GotoObjectsEnd, RepeatUpTo, Version
11+
from mgz.util import Find, GotoObjectsEnd, RepeatUpTo, Version, find_save_version
1212

1313
# Player attributes.
1414
attributes = "attributes"/Struct(
1515
Array(lambda ctx: ctx._._._.replay.num_players, TheirDiplomacyEnum("their_diplomacy"/Byte)),
16-
Array(9, MyDiplomacyEnum("my_diplomacy"/Int32sl)),
16+
Array(lambda ctx: ctx._._._.replay.num_players if find_save_version(ctx) >= 61.5 else 9, MyDiplomacyEnum("my_diplomacy"/Int32sl)),
1717
"allied_los"/Int32ul,
1818
"allied_victory"/Flag,
1919
"player_name_length"/Int16ul,

mgz/header/map_info.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from construct import (Array, Byte, Computed, Embedded, Flag, IfThenElse,
44
Int32ul, Padding, Struct, Int16sl, If, Peek)
55

6-
from mgz.util import Version
6+
from mgz.util import Version, find_save_version
77

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

@@ -13,6 +13,7 @@
1313
"terrain_type"/Byte,
1414
Embedded(IfThenElse(lambda ctx: ctx._._.version == Version.DE,
1515
Embedded(Struct(
16+
If(lambda ctx: ctx._._._.save_version >= 62.0, Byte),
1617
Padding(1), # copy of previous byte
1718
"elevation"/Byte,
1819
"unk0"/Int16sl,
@@ -59,4 +60,5 @@
5960
"size_x_2"/Int32ul,
6061
"size_y_2"/Int32ul,
6162
Padding(lambda ctx: ctx.tile_num * 4), # visibility
63+
If(lambda ctx: find_save_version(ctx) >= 61.5, Padding(lambda ctx: ctx.tile_num * 4))
6264
)

mgz/header/objects.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@
112112
If(lambda ctx: find_save_version(ctx) >= 25.22 and find_type(ctx) == 10, Bytes(1)),
113113
If(lambda ctx: find_save_version(ctx) < 25.22 and find_type(ctx) == 10 and ctx.peek[0] == 0 and ctx.peek[0:2] != b"\x00\x0b", Bytes(1)),
114114
If(lambda ctx: find_type(ctx) == 20 and ctx.peek[4] == 0 and ctx.peek[4:6] != b"\x00\x0b", Bytes(1)),
115-
))
115+
)),
116+
If(lambda ctx: find_save_version(ctx) >= 61.5, Bytes(14)),
117+
If(lambda ctx: find_save_version(ctx) >= 62.0, Bytes(4))
116118
)),
117119
"hd_extension"/If(lambda ctx: find_version(ctx) == Version.HD and find_save_version(ctx) > 12.36, Struct(
118120
"flag"/Flag,
@@ -397,6 +399,7 @@
397399
"de"/If(lambda ctx: find_version(ctx) == Version.DE, Bytes(14)),
398400
"de_2"/If(lambda ctx: find_save_version(ctx) >= 26.16, Bytes(16)),
399401
"de_3"/If(lambda ctx: find_save_version(ctx) >= 26.18, Bytes(1)),
402+
"de_4"/If(lambda ctx: find_save_version(ctx) >= 61.5, Bytes(4)),
400403
"next_volley"/Byte,
401404
"using_special_animation"/Byte,
402405
"own_base"/Byte,
@@ -429,6 +432,7 @@
429432
"de_unknown3"/If(lambda ctx: 26.18 > find_save_version(ctx) >= 26.16, Bytes(5)),
430433
"de_unknown4"/If(lambda ctx: find_save_version(ctx) >= 26.18, Bytes(4)),
431434
"de_unknown5"/If(lambda ctx: find_save_version(ctx) >= 50, Bytes(48)),
435+
"de_unknown6"/If(lambda ctx: find_save_version(ctx) >= 61.5, Bytes(44))
432436
)
433437

434438
production_queue = "production_queue"/Struct(
@@ -474,6 +478,7 @@
474478
"de_unk_3"/If(lambda ctx: find_save_version(ctx) >= 25.22, Byte),
475479
"de_unk_4"/If(lambda ctx: find_save_version(ctx) >= 26.16, Bytes(4)),
476480
"de_unk_5"/If(lambda ctx: find_save_version(ctx) >= 50.4, Bytes(4)),
481+
"de_unk_6"/If(lambda ctx: find_save_version(ctx) >= 61.5, Bytes(12)),
477482
)
478483

479484

mgz/header/replay.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from construct import Array, Byte, Flag, Float32l, Int16ul, Int32sl, Int32ul, Padding, Struct, If, Embedded
44

5-
from mgz.util import Version
5+
from mgz.util import Version, find_save_version
66

77
# pylint: disable=invalid-name
88

@@ -35,6 +35,6 @@
3535
"king_campaign_player"/Byte,
3636
"king_campaign_scenario"/Byte,
3737
"player_turn"/Int32ul,
38-
"player_time_delta"/Array(9, "turn"/Int32ul),
38+
"player_time_delta"/Array(lambda obj: obj.num_players if find_save_version(obj) >= 61.5 else 9, "turn"/Int32ul),
3939
If(lambda ctx: ctx._.version == Version.DE, Padding(8))
4040
)

mgz/header/scenario.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
scenario_header = "scenario_header"/Struct(
1515
"next_uid"/Int32ul,
1616
"scenario_version"/Float32l,
17+
If(lambda ctx: find_save_version(ctx) >= 61.5, Padding(8)),
1718
Array(16, "names"/String(256)),
1819
Array(16, "player_ids"/Int32ul),
20+
If(lambda ctx: find_save_version(ctx) >= 61.5, Padding(64)),
1921
Array(16, "player_data"/Struct(
2022
"active"/Int32ul,
2123
"human"/Int32ul,
@@ -147,7 +149,8 @@
147149
If(lambda ctx: 26.16 > find_save_version(ctx) >= 25.22, Find(struct.pack('<d', 2.6), None)),
148150
If(lambda ctx: 26.21 > find_save_version(ctx) >= 26.16, Find(struct.pack('<d', 3.0), None)),
149151
If(lambda ctx: 37 > find_save_version(ctx) >= 26.21, Find(struct.pack('<d', 3.2), None)),
150-
If(lambda ctx: find_save_version(ctx) >= 37, Find(struct.pack('<d', 3.5), None))
152+
If(lambda ctx: 61.5 > find_save_version(ctx) >= 37, Find(struct.pack('<d', 3.5), None)),
153+
If(lambda ctx: find_save_version(ctx) >= 61.5, Find(struct.pack('<d', 3.6), None))
151154
),
152155
"end_of_game_settings"/Find(b'\x9a\x99\x99\x99\x99\x99\xf9\\x3f', None)
153156
)

mgz/model/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def parse_match(handle):
119119
data['map']['tiles'],
120120
de_seed=data['lobby']['seed']
121121
)
122-
except ValueError:
123-
raise RuntimeError("could not get map data")
122+
except ValueError as e:
123+
raise RuntimeError(f"could not get map data: {e}")
124124

125125
# Handle DE-specific data
126126
rated = None

mgz/util.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ def _parse(self, stream, context, path):
271271
marker -= 1
272272
count += 1
273273
# Backtrack through the rest of the next player structure
274-
backtrack = 43 + num_players
274+
offset = 9 * 4
275+
if save_version >= 61.5:
276+
offset = num_players * 4
277+
backtrack = 7 + num_players + offset
275278
# Otherwise, this is the last player
276279
else:
277280
# Search for the scenario header

0 commit comments

Comments
 (0)