-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmultiworld.py
More file actions
167 lines (131 loc) · 5.4 KB
/
multiworld.py
File metadata and controls
167 lines (131 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
import dataclasses
import logging
import sys
import unpickle
import zlib
import io
import zipfile
from typing import Any, Optional, Iterator
log = logging.getLogger(__name__)
PlayerName = str
PlayerId = int
GameName = str
def SlotType(slot_type: int) -> int:
return slot_type # TODO: What is this? Maybe an enum? Look it up in AP src
def HintStatus(hint_status: int) -> int:
return hint_status # TODO: Look up what they mean
@dataclasses.dataclass
class SlotInfo:
player_name: PlayerName
game_name: GameName
slot_type: int
unknown: Any = None
@dataclasses.dataclass
class ServerOptions:
host: Optional[str] = None
port: Optional[int] = None
password: Optional[str] = None
server_password: Optional[str] = None
@property
def visible_password(self) -> str:
"""
Password that the user usually has to type in.
Normally, "no password" means "None" but the various clients
(except the integrated text client when using the name:password@...
syntax) turn empty passwords into "None".
"""
return "" if self.password is None else str(self.password)
_all: Optional[dict[str, Any]] = None
@dataclasses.dataclass(frozen=True)
class Hint:
unknown1: int
unknown2: int
unknown3: int
unknown4: int
unknown5: bool
unknown6: str
unknown7: int
hint_status: int
@dataclasses.dataclass
class MultiWorld:
slot_data: dict[PlayerId, dict[str, Any]] = dataclasses.field(default_factory=dict)
slot_info: dict[PlayerId, SlotInfo] = dataclasses.field(default_factory=dict)
connect_names: dict[PlayerName, tuple] = dataclasses.field(default_factory=dict) # TODO: What is the format?
locations: dict[PlayerId, dict[int, tuple]] = dataclasses.field(default_factory=dict) # TODO: What is the format?
server_options: ServerOptions = dataclasses.field(default_factory=ServerOptions)
version: Optional[tuple[int, int, int]] = (0, 0, 0)
seed_name: str = ""
race_mode: int = 0 # TODO: What do the numbers mean?
def get_slots_by_game_name(self, game_name: str) -> Iterator[SlotInfo]:
return filter(lambda slot: slot.game_name == game_name,
self.slot_info.values())
def get_slot_by_player_name(self, player_name: str) -> Optional[SlotInfo]:
return next(filter(lambda slot: slot.player_name == player_name,
self.slot_info.values()), None)
unpickle_mapping: unpickle.ResolveMapping = {
('NetUtils', 'NetworkSlot'): SlotInfo,
('NetUtils', 'SlotType'): SlotType,
('NetUtils', 'Hint'): Hint,
('NetUtils', 'HintStatus'): HintStatus,
}
def _find_multiworld(raw_data: Any) -> bytes:
if type(raw_data) is bytes or type(raw_data) is bytearray:
raw_data = io.BytesIO(raw_data)
try:
with zipfile.ZipFile(raw_data) as zip_file:
for filename in zip_file.namelist():
if filename.endswith('.archipelago'):
return zip_file.read(filename)
except zipfile.BadZipFile:
raw_data.seek(0)
return raw_data.read()
raise FileNotFoundError("Could not find .archipelago file")
def parse(raw_data: Any) -> MultiWorld:
return parse_bytes(_find_multiworld(raw_data))
def _get_inner(raw_data) -> tuple[int, bytes]:
format_version = raw_data[0]
logging.debug("Found multiworld format version 0x%02x", format_version)
# TODO: Check format version
return (format_version, zlib.decompress(raw_data[1:]))
def parse_bytes(raw_data: bytes) -> MultiWorld:
format_version, inner_data = _get_inner(raw_data)
# TODO: Check format version
data = unpickle.Unpickler(io.BytesIO(inner_data)).load()
data = unpickle.resolve(data, unpickle_mapping)
so = data.get('server_options', {})
server_options = ServerOptions(host=so.get('host', None),
port=so.get('port', None),
password=so.get('password', None),
server_password=so.get('password', None),
_all=so
)
return MultiWorld(slot_data=data.get('slot_data', {}),
slot_info=data.get('slot_info', {}),
connect_names=data.get('connect_names', {}),
version=data.get('version'),
locations=data.get('locations', {}),
seed_name=data.get('seed_name', ''),
race_mode=data.get('race_mode', 0),
server_options=server_options
)
if __name__ == '__main__':
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
import argparse
import pprint
parser = argparse.ArgumentParser()
parser.description = "Parse a .archipelago world file"
parser.add_argument("--no-resolve", action='store_true', default=False,
dest='noresolve',
help="Don't resolve objects")
parser.add_argument("world", type=str,
help="Path to .archipelago or .zip file")
args = parser.parse_args()
with open(args.world, 'rb') as f:
if args.noresolve:
pprint.pp(unpickle.Unpickler(
io.BytesIO(_get_inner(_find_multiworld(f))[1])
).load(), width=200)
else:
pprint.pp(parse(f), width=200)