Skip to content

Commit 19b75b3

Browse files
support 71094
1 parent 9b8e716 commit 19b75b3

6 files changed

Lines changed: 243 additions & 94 deletions

File tree

mgz/fast/__init__.py

Lines changed: 15 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,13 @@
11
"""Fast(er) parsing for situations requiring speed."""
22
import io
33
import struct
4-
from enum import Enum
54

6-
from mgz.util import check_flags
7-
8-
CHECKSUM_INTERVAL = 500
9-
10-
11-
class Operation(Enum):
12-
"""Operation types."""
13-
ACTION = 1
14-
SYNC = 2
15-
VIEWLOCK = 3
16-
CHAT = 4
17-
START = 5
18-
POSTGAME = 6
19-
SAVE = 7
5+
from mgz.fast.enums import Operation, Action, Postgame
6+
from mgz.fast.actions import parse_action_71094
7+
from mgz.util import check_flags, unpack
208

219

22-
class Action(Enum):
23-
"""Action types."""
24-
ORDER = 0
25-
STOP = 1
26-
WORK = 2
27-
MOVE = 3
28-
CREATE = 4
29-
ADD_ATTRIBUTE = 5
30-
GIVE_ATTRIBUTE = 6
31-
AI_ORDER = 10
32-
RESIGN = 11
33-
SPECTATE = 15
34-
ADD_WAYPOINT = 16
35-
STANCE = 18
36-
GUARD = 19
37-
FOLLOW = 20
38-
PATROL = 21
39-
FORMATION = 23
40-
SAVE = 27
41-
GROUP_MULTI_WAYPOINTS = 31
42-
CHAPTER = 32
43-
DE_ATTACK_MOVE = 33
44-
HD_UNKNOWN_34 = 34
45-
DE_UNKNOWN_35 = 35
46-
DE_UNKNOWN_37 = 37
47-
DE_AUTOSCOUT = 38
48-
DE_UNKNOWN_39 = 39
49-
DE_UNKNOWN_40 = 40
50-
DE_UNKNOWN_41 = 41
51-
DE_UNKNOWN_43 = 43
52-
AI_COMMAND = 53
53-
DE_UNKNOWN_80 = 80
54-
MAKE = 100
55-
RESEARCH = 101
56-
BUILD = 102
57-
GAME = 103
58-
WALL = 105
59-
DELETE = 106
60-
ATTACK_GROUND = 107
61-
TRIBUTE = 108
62-
DE_UNKNOWN_109 = 109
63-
REPAIR = 110
64-
UNGARRISON = 111
65-
MULTIQUEUE = 112
66-
GATE = 114
67-
FLARE = 115
68-
SPECIAL = 117
69-
QUEUE = 119
70-
GATHER_POINT = 120
71-
SELL = 122
72-
BUY = 123
73-
DROP_RELIC = 126
74-
TOWN_BELL = 127
75-
BACK_TO_WORK = 128
76-
DE_QUEUE = 129
77-
DE_UNKNOWN_130 = 130
78-
DE_UNKNOWN_131 = 131
79-
DE_UNKNOWN_135 = 135
80-
DE_UNKNOWN_138 = 138
81-
DE_TRIBUTE = 196
82-
POSTGAME = 255
83-
84-
85-
class Postgame(Enum):
86-
"""Postgame types."""
87-
WORLD_TIME = 1
88-
LEADERBOARDS = 2
10+
CHECKSUM_INTERVAL = 500
8911

9012

9113
def sync(data):
@@ -112,6 +34,9 @@ def viewlock(data):
11234

11335
def parse_action(action_type, data):
11436
"""Parse player, objects, and coordinates from actions."""
37+
player_id, length = struct.unpack_from('<bh', data)
38+
if len(data) == length + 3:
39+
return parse_action_71094(action_type, player_id, data[3:])
11540
if action_type == Action.RESIGN:
11641
return dict(player_id=data[0])
11742
if action_type == Action.TRIBUTE:
@@ -280,17 +205,22 @@ def parse_action(action_type, data):
280205
return dict()
281206

282207

283-
def action(data):
208+
def action(data, sequence=None):
284209
"""Handle actions."""
285210
length, = struct.unpack('<I', data.read(4))
286211
action_id = int.from_bytes(data.read(1), 'little')
287212
action_bytes = data.read(length - 1)
288-
data.read(4)
213+
if sequence is None:
214+
sequence, = struct.unpack('<I', data.read(4))
289215
action_type = Action(action_id)
290216
if action_type == Action.POSTGAME:
291217
payload = dict(bytes=action_bytes + data.read())
292218
else:
293-
payload = parse_action(action_type, action_bytes)
219+
try:
220+
payload = parse_action(action_type, action_bytes)
221+
except struct.error:
222+
return Action.ERROR, {}
223+
payload["sequence"] = sequence
294224
return action_type, payload
295225

296226

mgz/fast/actions.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""Action parsing for DE >= 71094."""
2+
import io
3+
import struct
4+
5+
from mgz.util import unpack, as_hex
6+
from mgz.fast.enums import Action
7+
8+
9+
def parse_action_71094(action_type, player_id, raw):
10+
data = io.BytesIO(raw)
11+
payload = {}
12+
if action_type is Action.RESIGN:
13+
unpack('<b', data)
14+
if action_type is Action.RESEARCH:
15+
object_id, selected, technology_id = unpack('<Ihh5x', data)
16+
selected_building_ids = unpack(f'<{selected}I', data, shorten=False)
17+
payload = dict(technology_id=technology_id, object_ids=[object_id])
18+
if action_type is Action.GAME:
19+
command_id = unpack('<h', data)
20+
payload = dict(command_id=command_id)
21+
if command_id == 0:
22+
source_player, target_player, mode_float, mode = unpack('<2xhhfb', data)
23+
payload.update(dict(target_player_id=target_player, diplomacy_mode=mode))
24+
elif command_id == 1:
25+
speed = unpack('<6xf', data)
26+
payload.update(dict(speed=speed))
27+
if action_type is Action.DE_QUEUE:
28+
selected, building_type, unit_id, amount, *object_ids = unpack('<h4xhhh', data)
29+
object_ids = list(unpack(f'<{selected}I', data, shorten=False))
30+
payload = dict(object_ids=object_ids, amount=amount, unit_id=unit_id)
31+
if action_type is Action.MOVE:
32+
x, y, selected = unpack('<4x2fh', data)
33+
object_ids = []
34+
data.read(4)
35+
if selected > 0:
36+
object_ids = list(unpack(f'<{selected}I', data, shorten=False))
37+
payload = dict(object_ids=object_ids, x=x, y=y)
38+
if action_type is Action.ORDER:
39+
target_id, x, y, selected = unpack('<I2fh', data)
40+
object_ids = []
41+
data.read(4)
42+
if selected > 0:
43+
object_ids = list(unpack(f'<{selected}I', data, shorten=False))
44+
payload = dict(object_ids=object_ids, target_id=target_id, x=x, y=y)
45+
if action_type is Action.BUILD:
46+
selected, x, y, building_id, unk2, unk3, unk4 = unpack('<h2xffI8xhbb', data)
47+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
48+
payload = dict(building_id=building_id, object_ids=object_ids, x=x, y=y)
49+
if action_type is Action.GATHER_POINT:
50+
selected, x, y, target_id, target_type = unpack('<h2xffii', data)
51+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
52+
payload = dict(target_id=target_id, target_type=target_type, x=x, y=y, object_ids=object_ids)
53+
if action_type is Action.STANCE:
54+
selected, stance_id = unpack('<II', data)
55+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
56+
payload = dict(stance_id=stance_id, object_ids=object_ids)
57+
if action_type is Action.SPECIAL:
58+
selected, target_id, x, y, order_id = unpack('<Iiff8xH', data)
59+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
60+
payload = dict(order_id=order_id, target_id=target_id, x=x, y=y, object_ids=object_ids)
61+
if action_type is Action.FORMATION:
62+
selected, formation_id = unpack('<II', data)
63+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
64+
payload = dict(formation_id=formation_id, object_ids=object_ids)
65+
if action_type in [Action.BUY, Action.SELL]:
66+
resource_id, amount, object_id = unpack('<hhI', data)
67+
payload = dict(resource_id=resource_id, amount=amount, object_ids=[object_id])
68+
if action_type is Action.DE_UNKNOWN_41:
69+
# autoscout enable?
70+
object_id, y = unpack('<II', data)
71+
payload = dict(object_ids=[object_id])
72+
if action_type is Action.AI_ORDER:
73+
# used for autoscout moves
74+
# 01 00 00 00 75 06 00 00 ff ff ff ff 21 03 00 00 00 00 30 42 00 00 a0 42 00 00 00 00 00 00 80 3f 64 ff 01 00
75+
a, object_id, c, x, y = unpack('<II4xIff', data)
76+
payload = dict(object_ids=[object_id], x=x, y=y)
77+
if action_type in [Action.BACK_TO_WORK, Action.DELETE]:
78+
object_id = unpack('<I', data)
79+
payload = dict(object_ids=[object_id])
80+
if action_type is Action.WALL:
81+
selected, x1, y1, x2, y2, building_id = unpack('<IHHHHI', data)
82+
data.read(8)
83+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
84+
payload = dict(object_ids=object_ids, x=x1, y=y1, x_end=x2, y_end=y2, building_id=building_id)
85+
if action_type in [Action.PATROL, Action.DE_ATTACK_MOVE]:
86+
selected, x, y = unpack('<I4xf36xf36x', data)
87+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
88+
payload = dict(object_ids=object_ids, x=x, y=y)
89+
if action_type is Action.UNGARRISON:
90+
selected, x, y, target_id, unk = unpack('<IffiI', data)
91+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
92+
payload = dict(object_ids=object_ids, x=x, y=y, target_id=target_id)
93+
if action_type is Action.FLARE:
94+
x, y, num = unpack('<4xffb', data)
95+
targets = list(unpack(f'<{num}b', data, shorten=False))
96+
payload = dict(x=x, y=y, targets=targets)
97+
if action_type is Action.TOWN_BELL:
98+
building_id, mode = unpack('<Ib',data)
99+
payload = dict(building_id=building_id, mode=mode)
100+
if action_type is Action.STOP:
101+
selected = unpack('<I', data)
102+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
103+
payload = dict(object_ids=object_ids)
104+
if action_type in [Action.FOLLOW, Action.GUARD]:
105+
selected, target_id = unpack('<II', data)
106+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
107+
payload = dict(object_ids=object_ids, target_id=target_id)
108+
if action_type is Action.ATTACK_GROUND:
109+
selected, x, y = unpack('<Iff', data)
110+
data.read(4)
111+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
112+
payload = dict(object_ids=object_ids, x=x, y=y)
113+
if action_type is Action.REPAIR:
114+
selected, target_id = unpack('<II', data)
115+
data.read(4)
116+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
117+
payload = dict(object_ids=object_ids, target_id=target_id)
118+
if action_type is Action.DE_TRIBUTE:
119+
wood, food, gold, stone = unpack('<ffff', data)
120+
data.read(16)
121+
unk1, unk2, unk3, target_id = unpack('<Ihhb', data)
122+
payload = dict(target_player_id=target_id, food=food, wood=wood, stone=stone, gold=gold)
123+
if action_type is Action.GATE:
124+
object_id = unpack('<I', data)
125+
payload = dict(object_ids=[object_id])
126+
if action_type in [Action.DE_AUTOSCOUT, Action.RATHA_ABILITY]:
127+
selected = unpack('<I', data)
128+
object_ids = list(unpack(f'{selected}I', data, shorten=False))
129+
payload = dict(object_ids=object_ids)
130+
return dict(player_id=player_id, **payload)

mgz/fast/enums.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Fast body parser enumerations."""
2+
3+
from enum import Enum
4+
5+
class Operation(Enum):
6+
"""Operation types."""
7+
ACTION = 1
8+
SYNC = 2
9+
VIEWLOCK = 3
10+
CHAT = 4
11+
START = 5
12+
POSTGAME = 6
13+
SAVE = 7
14+
15+
16+
class Action(Enum):
17+
"""Action types."""
18+
ERROR = -1
19+
ORDER = 0
20+
STOP = 1
21+
WORK = 2
22+
MOVE = 3
23+
CREATE = 4
24+
ADD_ATTRIBUTE = 5
25+
GIVE_ATTRIBUTE = 6
26+
AI_ORDER = 10
27+
RESIGN = 11
28+
SPECTATE = 15
29+
ADD_WAYPOINT = 16
30+
STANCE = 18
31+
GUARD = 19
32+
FOLLOW = 20
33+
PATROL = 21
34+
FORMATION = 23
35+
SAVE = 27
36+
GROUP_MULTI_WAYPOINTS = 31
37+
CHAPTER = 32
38+
DE_ATTACK_MOVE = 33
39+
HD_UNKNOWN_34 = 34
40+
DE_UNKNOWN_35 = 35
41+
DE_UNKNOWN_37 = 37
42+
DE_AUTOSCOUT = 38
43+
DE_UNKNOWN_39 = 39
44+
DE_UNKNOWN_40 = 40
45+
DE_UNKNOWN_41 = 41
46+
RATHA_ABILITY = 43
47+
AI_COMMAND = 53
48+
DE_UNKNOWN_80 = 80
49+
MAKE = 100
50+
RESEARCH = 101
51+
BUILD = 102
52+
GAME = 103
53+
WALL = 105
54+
DELETE = 106
55+
ATTACK_GROUND = 107
56+
TRIBUTE = 108
57+
DE_UNKNOWN_109 = 109
58+
REPAIR = 110
59+
UNGARRISON = 111
60+
MULTIQUEUE = 112
61+
GATE = 114
62+
FLARE = 115
63+
SPECIAL = 117
64+
QUEUE = 119
65+
GATHER_POINT = 120
66+
SELL = 122
67+
BUY = 123
68+
DROP_RELIC = 126
69+
TOWN_BELL = 127
70+
BACK_TO_WORK = 128
71+
DE_QUEUE = 129
72+
DE_UNKNOWN_130 = 130
73+
DE_UNKNOWN_131 = 131
74+
DE_UNKNOWN_135 = 135
75+
DE_UNKNOWN_138 = 138
76+
DE_TRIBUTE = 196
77+
POSTGAME = 255
78+
79+
80+
class Postgame(Enum):
81+
"""Postgame types."""
82+
WORLD_TIME = 1
83+
LEADERBOARDS = 2

mgz/fast/header.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import uuid
77
import zlib
88

9-
from mgz.util import get_version, Version
9+
from mgz.util import get_version, unpack, Version
1010

1111
ZLIB_WBITS = -15
1212
CLASSES = [b'\x0a', b'\x1e', b'\x46', b'\x50']
@@ -28,14 +28,6 @@ def _compile_object_search():
2828
_compile_object_search()
2929

3030

31-
def unpack(fmt, data):
32-
"""Unpack bytes according to format string."""
33-
output = struct.unpack(fmt, data.read(struct.calcsize(fmt)))
34-
if len(output) == 1:
35-
return output[0]
36-
return output
37-
38-
3931
def aoc_string(data):
4032
"""Read AOC string."""
4133
length = unpack('<h', data)

mgz/model/inputs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def add_action(self, action):
3838
name = action.payload['order']
3939
elif action.type is ActionEnum.GAME:
4040
name = action.payload['command']
41+
if name == 'Speed':
42+
param = action.payload['speed']
4143
elif action.type is ActionEnum.STANCE:
4244
name = 'Stance'
4345
param = action.payload['stance']

mgz/util.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,15 @@ def find_postgame(data, size):
332332
LOGGER.debug("found postgame candidate @ %d with length %d", pos, length)
333333
return i + LOOKAHEAD, length
334334
return None, None
335+
336+
337+
def unpack(fmt, data, shorten=True):
338+
"""Unpack bytes according to format string."""
339+
output = struct.unpack(fmt, data.read(struct.calcsize(fmt)))
340+
if len(output) == 1 and shorten:
341+
return output[0]
342+
return output
343+
344+
345+
def as_hex(d):
346+
return " ".join(["{:02x}".format(x) for x in d])

0 commit comments

Comments
 (0)