diff options
Diffstat (limited to 'apworld/context.py')
-rw-r--r-- | apworld/context.py | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/apworld/context.py b/apworld/context.py new file mode 100644 index 0000000..848efb8 --- /dev/null +++ b/apworld/context.py | |||
@@ -0,0 +1,304 @@ | |||
1 | import asyncio | ||
2 | import os | ||
3 | import pkgutil | ||
4 | import subprocess | ||
5 | from typing import Any | ||
6 | |||
7 | import websockets | ||
8 | |||
9 | import Utils | ||
10 | import settings | ||
11 | from CommonClient import CommonContext, server_loop, gui_enabled, logger | ||
12 | from NetUtils import Endpoint, decode, encode | ||
13 | from Utils import async_start | ||
14 | |||
15 | PORT = 43182 | ||
16 | MESSAGE_MAX_SIZE = 16*1024*1024 | ||
17 | |||
18 | |||
19 | class Lingo2GameContext: | ||
20 | server: Endpoint | None | ||
21 | client: "Lingo2ClientContext" | ||
22 | |||
23 | def __init__(self): | ||
24 | self.server = None | ||
25 | |||
26 | def send_connected(self): | ||
27 | msg = { | ||
28 | "cmd": "Connected", | ||
29 | "user": self.client.username, | ||
30 | "seed_name": self.client.seed_name, | ||
31 | "version": self.client.server_version, | ||
32 | "generator_version": self.client.generator_version, | ||
33 | "team": self.client.team, | ||
34 | "slot": self.client.slot, | ||
35 | "checked_locations": self.client.checked_locations, | ||
36 | "slot_data": self.client.slot_data, | ||
37 | } | ||
38 | |||
39 | async_start(self.send_msgs([msg]), name="game Connected") | ||
40 | |||
41 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): | ||
42 | msg = { | ||
43 | "cmd": "ItemSentNotif", | ||
44 | "item_name": item_name, | ||
45 | "receiver_name": receiver_name, | ||
46 | "item_flags": item_flags, | ||
47 | } | ||
48 | |||
49 | async_start(self.send_msgs([msg]), name="item sent notif") | ||
50 | |||
51 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): | ||
52 | msg = { | ||
53 | "cmd": "HintReceived", | ||
54 | "item_name": item_name, | ||
55 | "location_name": location_name, | ||
56 | "receiver_name": receiver_name, | ||
57 | "item_flags": item_flags, | ||
58 | "self": int(for_self), | ||
59 | } | ||
60 | |||
61 | async_start(self.send_msgs([msg]), name="hint received notif") | ||
62 | |||
63 | def send_item_received(self, items): | ||
64 | msg = { | ||
65 | "cmd": "ItemReceived", | ||
66 | "items": items, | ||
67 | } | ||
68 | |||
69 | async_start(self.send_msgs([msg]), name="item received") | ||
70 | |||
71 | def send_location_info(self, locations): | ||
72 | msg = { | ||
73 | "cmd": "LocationInfo", | ||
74 | "locations": locations, | ||
75 | } | ||
76 | |||
77 | async_start(self.send_msgs([msg]), name="location info") | ||
78 | |||
79 | def send_text_message(self, parts): | ||
80 | msg = { | ||
81 | "cmd": "TextMessage", | ||
82 | "data": parts, | ||
83 | } | ||
84 | |||
85 | async_start(self.send_msgs([msg]), name="notif") | ||
86 | |||
87 | async def send_msgs(self, msgs: list[Any]) -> None: | ||
88 | """ `msgs` JSON serializable """ | ||
89 | if not self.server or not self.server.socket.open or self.server.socket.closed: | ||
90 | return | ||
91 | await self.server.socket.send(encode(msgs)) | ||
92 | |||
93 | |||
94 | class Lingo2ClientContext(CommonContext): | ||
95 | game_ctx: Lingo2GameContext | ||
96 | |||
97 | game = "Lingo 2" | ||
98 | items_handling = 0b111 | ||
99 | |||
100 | slot_data: dict[str, Any] | None | ||
101 | |||
102 | def __init__(self, server_address: str | None = None, password: str | None = None): | ||
103 | super().__init__(server_address, password) | ||
104 | |||
105 | def make_gui(self): | ||
106 | ui = super().make_gui() | ||
107 | ui.base_title = "Archipelago Lingo 2 Client" | ||
108 | return ui | ||
109 | |||
110 | async def server_auth(self, password_requested: bool = False): | ||
111 | self.auth = self.username | ||
112 | await self.send_connect() | ||
113 | |||
114 | def on_package(self, cmd: str, args: dict): | ||
115 | if cmd == "RoomInfo": | ||
116 | self.seed_name = args.get("seed_name", None) | ||
117 | elif cmd == "Connected": | ||
118 | self.slot_data = args.get("slot_data", None) | ||
119 | |||
120 | if self.game_ctx.server is not None: | ||
121 | self.game_ctx.send_connected() | ||
122 | elif cmd == "ReceivedItems": | ||
123 | if self.game_ctx.server is not None: | ||
124 | cur_index = 0 | ||
125 | items = [] | ||
126 | |||
127 | for item in args["items"]: | ||
128 | index = cur_index + args["index"] | ||
129 | cur_index += 1 | ||
130 | |||
131 | item_msg = { | ||
132 | "id": item.item, | ||
133 | "index": index, | ||
134 | "flags": item.flags, | ||
135 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | ||
136 | } | ||
137 | |||
138 | if item.player != self.slot: | ||
139 | item_msg["sender"] = self.player_names.get(item.player) | ||
140 | |||
141 | items.append(item_msg) | ||
142 | |||
143 | self.game_ctx.send_item_received(items) | ||
144 | elif cmd == "PrintJSON": | ||
145 | if self.game_ctx.server is not None: | ||
146 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | ||
147 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | ||
148 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | ||
149 | receiver_name = self.player_names.get(args["receiving"]) | ||
150 | |||
151 | if args["type"] == "Hint" and not args.get("found", False): | ||
152 | self.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | ||
153 | int(args["receiving"]) == self.slot) | ||
154 | elif args["receiving"] != self.slot: | ||
155 | self.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | ||
156 | |||
157 | parts = [] | ||
158 | for message_part in args["data"]: | ||
159 | if "type" not in message_part and "text" in message_part: | ||
160 | parts.append({"type": "text", "text": message_part["text"]}) | ||
161 | elif message_part["type"] == "player_id": | ||
162 | parts.append({ | ||
163 | "type": "player", | ||
164 | "text": self.player_names.get(int(message_part["text"])), | ||
165 | "self": int(int(message_part["text"]) == self.slot), | ||
166 | }) | ||
167 | elif message_part["type"] == "item_id": | ||
168 | parts.append({ | ||
169 | "type": "item", | ||
170 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]) | ||
171 | }) | ||
172 | elif message_part["type"] == "location_id": | ||
173 | parts.append({ | ||
174 | "type": "location", | ||
175 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
176 | message_part["player"]) | ||
177 | }) | ||
178 | elif "text" in message_part: | ||
179 | parts.append({"type": "text", "text": message_part["text"]}) | ||
180 | |||
181 | self.game_ctx.send_text_message(parts) | ||
182 | elif cmd == "LocationInfo": | ||
183 | if self.game_ctx.server is not None: | ||
184 | locations = [] | ||
185 | |||
186 | for location in args["locations"]: | ||
187 | locations.append({ | ||
188 | "id": location.location, | ||
189 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
190 | "player": self.player_names.get(location.player), | ||
191 | "flags": location.flags, | ||
192 | "self": int(location.player) == self.slot, | ||
193 | }) | ||
194 | |||
195 | self.game_ctx.send_location_info(locations) | ||
196 | |||
197 | |||
198 | async def pipe_loop(ctx: Lingo2GameContext): | ||
199 | while not ctx.client.exit_event.is_set(): | ||
200 | try: | ||
201 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, | ||
202 | max_size=MESSAGE_MAX_SIZE) | ||
203 | ctx.server = Endpoint(socket) | ||
204 | logger.info("Connected to Lingo 2!") | ||
205 | if ctx.client.auth is not None: | ||
206 | ctx.send_connected() | ||
207 | async for data in ctx.server.socket: | ||
208 | for msg in decode(data): | ||
209 | await process_game_cmd(ctx, msg) | ||
210 | except ConnectionRefusedError: | ||
211 | logger.info("Could not connect to Lingo 2.") | ||
212 | finally: | ||
213 | ctx.server = None | ||
214 | |||
215 | |||
216 | async def process_game_cmd(ctx: Lingo2GameContext, args: dict): | ||
217 | cmd = args["cmd"] | ||
218 | |||
219 | if cmd == "Connect": | ||
220 | server = args.get("server") | ||
221 | player = args.get("player") | ||
222 | password = args.get("password") | ||
223 | |||
224 | if password != "": | ||
225 | server_address = f"{player}:{password}@{server}" | ||
226 | else: | ||
227 | server_address = f"{player}:None@{server}" | ||
228 | |||
229 | async_start(ctx.client.connect(server_address), name="client connect") | ||
230 | elif cmd == "Disconnect": | ||
231 | async_start(ctx.client.disconnect(), name="client disconnect") | ||
232 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | ||
233 | async_start(ctx.client.send_msgs([args]), name="client forward") | ||
234 | |||
235 | |||
236 | async def run_game(): | ||
237 | exe_file = settings.get_settings().lingo2_options.exe_file | ||
238 | |||
239 | from worlds import AutoWorldRegister | ||
240 | world = AutoWorldRegister.world_types["Lingo 2"] | ||
241 | |||
242 | if world.zip_path is not None: | ||
243 | # This is a packaged apworld. | ||
244 | init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") | ||
245 | init_path = Utils.local_path("data", "lingo2_init.tscn") | ||
246 | |||
247 | with open(init_path, "wb") as file_handle: | ||
248 | file_handle.write(init_scene) | ||
249 | |||
250 | subprocess.Popen( | ||
251 | [ | ||
252 | exe_file, | ||
253 | "--scene", | ||
254 | init_path, | ||
255 | "--", | ||
256 | str(world.zip_path.absolute()), | ||
257 | ], | ||
258 | cwd=os.path.dirname(exe_file), | ||
259 | ) | ||
260 | else: | ||
261 | # The world is unzipped and being run in source. | ||
262 | subprocess.Popen( | ||
263 | [ | ||
264 | exe_file, | ||
265 | "--scene", | ||
266 | Utils.local_path("worlds", "lingo2", "client", "run_from_source.tscn"), | ||
267 | "--", | ||
268 | Utils.local_path("worlds", "lingo2", "client"), | ||
269 | ], | ||
270 | cwd=os.path.dirname(exe_file), | ||
271 | ) | ||
272 | |||
273 | |||
274 | def client_main(*launch_args: str) -> None: | ||
275 | async def main(): | ||
276 | async_start(run_game()) | ||
277 | |||
278 | client_ctx = Lingo2ClientContext() | ||
279 | game_ctx = Lingo2GameContext() | ||
280 | |||
281 | client_ctx.game_ctx = game_ctx | ||
282 | game_ctx.client = client_ctx | ||
283 | |||
284 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") | ||
285 | |||
286 | if gui_enabled: | ||
287 | client_ctx.run_gui() | ||
288 | client_ctx.run_cli() | ||
289 | |||
290 | pipe_task = asyncio.create_task(pipe_loop(game_ctx), name="GameWatcher") | ||
291 | |||
292 | try: | ||
293 | await pipe_task | ||
294 | except Exception as e: | ||
295 | logger.exception(e) | ||
296 | |||
297 | await client_ctx.exit_event.wait() | ||
298 | await client_ctx.shutdown() | ||
299 | |||
300 | Utils.init_logging("Lingo2Client", exception_logger="Client") | ||
301 | import colorama | ||
302 | colorama.just_fix_windows_console() | ||
303 | asyncio.run(main()) | ||
304 | colorama.deinit() | ||