@@ -44,11 +44,18 @@ def aoc_string(data):
4444
4545def de_string (data ):
4646 """Read DE string."""
47- assert data .read (2 ) == b'\x60 \x0A '
47+ assert data .read (2 ) == b'\x60 \x0a '
4848 length = unpack ('<h' , data )
4949 return unpack (f'<{ length } s' , data )
5050
5151
52+ def hd_string (data ):
53+ """Read HD string."""
54+ length = unpack ('<h' , data )
55+ assert data .read (2 ) == b'\x60 \x0a '
56+ return unpack (f'<{ length } s' , data )
57+
58+
5259def parse_object (data , offset ):
5360 """Parse an object."""
5461 class_id , object_id , instance_id , pos_x , pos_y = struct .unpack_from ('<bxH14xIxff' , data , offset )
@@ -141,11 +148,13 @@ def parse_lobby(data, version, save):
141148 if save >= 20.06 :
142149 data .read (9 )
143150 data .read (8 )
144- if version is not Version .DE :
151+ if version not in ( Version .DE , Version . HD ) :
145152 data .read (1 )
146153 reveal_map_id , map_size , population , game_type_id , lock_teams = unpack ('I4xIIbb' , data )
147- if version is Version .DE :
148- data .read (9 )
154+ if version in (Version .DE , Version .HD ):
155+ data .read (5 )
156+ if save >= 13.13 :
157+ data .read (4 )
149158 chat = []
150159 for _ in range (0 , unpack ('<I' , data )):
151160 message = data .read (unpack ('<I' , data )).strip (b'\x00 ' )
@@ -157,7 +166,7 @@ def parse_lobby(data, version, save):
157166 return dict (
158167 reveal_map_id = reveal_map_id ,
159168 map_size = map_size ,
160- population = population * (25 if version is not Version .DE else 1 ),
169+ population = population * (25 if version not in ( Version .DE , Version . HD ) else 1 ),
161170 game_type_id = game_type_id ,
162171 lock_teams = lock_teams == 1 ,
163172 chat = chat ,
@@ -175,7 +184,7 @@ def parse_map(data, version):
175184 size_x , size_y , zone_num = unpack ('<III' , data )
176185 tile_num = size_x * size_y
177186 for _ in range (zone_num ):
178- if version is Version .DE :
187+ if version in ( Version .DE , Version . HD ) :
179188 data .read (2048 + (tile_num * 2 ))
180189 else :
181190 data .read (1275 + tile_num )
@@ -216,15 +225,19 @@ def parse_scenario(data, num_players, version):
216225 data .read (196 )
217226 for _ in range (0 , 16 ):
218227 data .read (24 )
219- if version is Version .DE :
228+ if version in ( Version .DE , Version . HD ) :
220229 data .read (4 )
221230 data .read (12672 )
222231 if version is Version .DE :
223232 data .read (196 )
224233 else :
225234 for _ in range (0 , 16 ):
226235 data .read (332 )
236+ if version is Version .HD :
237+ data .read (644 )
227238 data .read (88 )
239+ if version is Version .HD :
240+ data .read (16 )
228241 map_id , difficulty_id = unpack ('<II' , data )
229242 remainder = data .read ()
230243 if version is Version .DE :
@@ -293,7 +306,60 @@ def parse_de(data, version, save):
293306 players = players ,
294307 guid = str (uuid .UUID (bytes = guid )),
295308 lobby = lobby .decode ('utf-8' ),
296- mod = mod
309+ mod = mod .decode ('utf-8' )
310+ )
311+
312+
313+ def parse_hd (data , version , save ):
314+ """Parse HD-specifc header."""
315+ if version is not Version .HD or save <= 12.34 :
316+ return None
317+ data .read (12 )
318+ dlc_count = unpack ('<I' , data )
319+ data .read (dlc_count * 4 )
320+ data .read (8 )
321+ map_id = unpack ('<I' , data )
322+ data .read (80 )
323+ players = []
324+ for _ in range (8 ):
325+ data .read (4 )
326+ color_id = unpack ('<i' , data )
327+ data .read (12 )
328+ civilization_id = unpack ('<I' , data )
329+ hd_string (data )
330+ data .read (1 )
331+ hd_string (data )
332+ name = hd_string (data )
333+ data .read (4 )
334+ steam_id , number = unpack ('<Qi' , data )
335+ data .read (8 )
336+ if name :
337+ players .append (dict (
338+ number = number ,
339+ color_id = color_id ,
340+ name = name ,
341+ profile_id = steam_id ,
342+ civilization_id = civilization_id
343+ ))
344+ data .read (26 )
345+ hd_string (data )
346+ data .read (8 )
347+ hd_string (data )
348+ data .read (8 )
349+ hd_string (data )
350+ data .read (8 )
351+ guid = data .read (16 )
352+ lobby = hd_string (data )
353+ mod = hd_string (data )
354+ data .read (8 )
355+ hd_string (data )
356+ data .read (4 )
357+ return dict (
358+ players = players ,
359+ guid = str (uuid .UUID (bytes = guid )),
360+ lobby = lobby .decode ('utf-8' ),
361+ mod = mod .decode ('utf-8' ),
362+ map_id = map_id
297363 )
298364
299365
@@ -310,15 +376,15 @@ def parse_version(header, data):
310376 log = unpack ('<I' , data )
311377 game , save = unpack ('<7sxf' , header )
312378 version = get_version (game .decode ('ascii' ), round (save , 2 ), log )
313- if version not in (Version .USERPATCH15 , Version .DE ):
379+ if version not in (Version .USERPATCH15 , Version .DE , Version . HD ):
314380 raise RuntimeError (f"{ version } not supported" )
315381 return version , round (save , 2 )
316382
317383
318384def parse_players (header , num_players , version ):
319385 """Parse all players."""
320386 cur = header .tell ()
321- gaia = b'Gaia' if version is Version .DE else b'GAIA'
387+ gaia = b'Gaia' if version in ( Version .DE , Version . HD ) else b'GAIA'
322388 anchor = header .read ().find (b'\x05 \x00 ' + gaia + b'\x00 ' )
323389 header .seek (cur + anchor - num_players - 43 )
324390 mod = parse_mod (header , num_players , version )
@@ -350,6 +416,7 @@ def parse(data):
350416 header = decompress (data )
351417 version , save = parse_version (header , data )
352418 de = parse_de (header , version , save )
419+ hd = parse_hd (header , version , save )
353420 metadata , num_players = parse_metadata (header )
354421 map_ = parse_map (header , version )
355422 players , mod = parse_players (header , num_players , version )
@@ -362,6 +429,7 @@ def parse(data):
362429 players = players ,
363430 map = map_ ,
364431 de = de ,
432+ hd = hd ,
365433 mod = mod ,
366434 metadata = metadata ,
367435 scenario = scenario ,
0 commit comments