diff options
Diffstat (limited to 'apworld/context.py')
-rw-r--r-- | apworld/context.py | 654 |
1 files changed, 654 insertions, 0 deletions
diff --git a/apworld/context.py b/apworld/context.py new file mode 100644 index 0000000..a0ee34d --- /dev/null +++ b/apworld/context.py | |||
@@ -0,0 +1,654 @@ | |||
1 | import asyncio | ||
2 | import os | ||
3 | import pkgutil | ||
4 | import subprocess | ||
5 | import sys | ||
6 | from typing import Any | ||
7 | |||
8 | import websockets | ||
9 | |||
10 | import Utils | ||
11 | import settings | ||
12 | from BaseClasses import ItemClassification | ||
13 | from CommonClient import CommonContext, server_loop, gui_enabled, logger, get_base_parser, handle_url_arg | ||
14 | from NetUtils import Endpoint, decode, encode, ClientStatus | ||
15 | from Utils import async_start | ||
16 | from . import Lingo2World | ||
17 | from .tracker import Tracker | ||
18 | |||
19 | ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz" | ||
20 | MESSAGE_MAX_SIZE = 16*1024*1024 | ||
21 | PORT = 43182 | ||
22 | |||
23 | KEY_STORAGE_MAPPING = { | ||
24 | "a": (1, 0), "b": (1, 1), "c": (1, 2), "d": (1, 3), "e": (1, 4), "f": (1, 5), "g": (1, 6), "h": (1, 7), "i": (1, 8), | ||
25 | "j": (1, 9), "k": (1, 10), "l": (1, 11), "m": (1, 12), "n": (2, 0), "o": (2, 1), "p": (2, 2), "q": (2, 3), | ||
26 | "r": (2, 4), "s": (2, 5), "t": (2, 6), "u": (2, 7), "v": (2, 8), "w": (2, 9), "x": (2, 10), "y": (2, 11), | ||
27 | "z": (2, 12), | ||
28 | } | ||
29 | |||
30 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} | ||
31 | |||
32 | |||
33 | class Lingo2Manager: | ||
34 | game_ctx: "Lingo2GameContext" | ||
35 | client_ctx: "Lingo2ClientContext" | ||
36 | tracker: Tracker | ||
37 | |||
38 | keyboard: dict[str, int] | ||
39 | worldports: set[int] | ||
40 | goaled: bool | ||
41 | |||
42 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | ||
43 | self.game_ctx = game_ctx | ||
44 | self.game_ctx.manager = self | ||
45 | self.client_ctx = client_ctx | ||
46 | self.client_ctx.manager = self | ||
47 | self.tracker = Tracker(self) | ||
48 | self.keyboard = {} | ||
49 | self.worldports = set() | ||
50 | |||
51 | self.reset() | ||
52 | |||
53 | def reset(self): | ||
54 | for k in ALL_LETTERS: | ||
55 | self.keyboard[k] = 0 | ||
56 | |||
57 | self.worldports = set() | ||
58 | self.goaled = False | ||
59 | |||
60 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | ||
61 | ret: dict[str, int] = {} | ||
62 | |||
63 | for k, v in new_keyboard.items(): | ||
64 | if v > self.keyboard.get(k, 0): | ||
65 | self.keyboard[k] = v | ||
66 | ret[k] = v | ||
67 | |||
68 | if len(ret) > 0: | ||
69 | self.tracker.refresh_state() | ||
70 | self.game_ctx.send_accessible_locations() | ||
71 | |||
72 | return ret | ||
73 | |||
74 | def update_worldports(self, new_worldports: set[int]) -> set[int]: | ||
75 | ret = new_worldports.difference(self.worldports) | ||
76 | self.worldports.update(new_worldports) | ||
77 | |||
78 | if len(ret) > 0: | ||
79 | self.tracker.refresh_state() | ||
80 | self.game_ctx.send_accessible_locations() | ||
81 | |||
82 | return ret | ||
83 | |||
84 | |||
85 | class Lingo2GameContext: | ||
86 | server: Endpoint | None | ||
87 | manager: Lingo2Manager | ||
88 | |||
89 | def __init__(self): | ||
90 | self.server = None | ||
91 | |||
92 | def send_connected(self): | ||
93 | if self.server is None: | ||
94 | return | ||
95 | |||
96 | msg = { | ||
97 | "cmd": "Connected", | ||
98 | "user": self.manager.client_ctx.username, | ||
99 | "seed_name": self.manager.client_ctx.seed_name, | ||
100 | "version": self.manager.client_ctx.server_version, | ||
101 | "generator_version": self.manager.client_ctx.generator_version, | ||
102 | "team": self.manager.client_ctx.team, | ||
103 | "slot": self.manager.client_ctx.slot, | ||
104 | "checked_locations": self.manager.client_ctx.checked_locations, | ||
105 | "slot_data": self.manager.client_ctx.slot_data, | ||
106 | } | ||
107 | |||
108 | async_start(self.send_msgs([msg]), name="game Connected") | ||
109 | |||
110 | def send_connection_refused(self, text): | ||
111 | if self.server is None: | ||
112 | return | ||
113 | |||
114 | msg = { | ||
115 | "cmd": "ConnectionRefused", | ||
116 | "text": text, | ||
117 | } | ||
118 | |||
119 | async_start(self.send_msgs([msg]), name="game ConnectionRefused") | ||
120 | |||
121 | def send_item_sent_notification(self, item_name, receiver_name, item_flags): | ||
122 | if self.server is None: | ||
123 | return | ||
124 | |||
125 | msg = { | ||
126 | "cmd": "ItemSentNotif", | ||
127 | "item_name": item_name, | ||
128 | "receiver_name": receiver_name, | ||
129 | "item_flags": item_flags, | ||
130 | } | ||
131 | |||
132 | async_start(self.send_msgs([msg]), name="item sent notif") | ||
133 | |||
134 | def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): | ||
135 | if self.server is None: | ||
136 | return | ||
137 | |||
138 | msg = { | ||
139 | "cmd": "HintReceived", | ||
140 | "item_name": item_name, | ||
141 | "location_name": location_name, | ||
142 | "receiver_name": receiver_name, | ||
143 | "item_flags": item_flags, | ||
144 | "self": int(for_self), | ||
145 | } | ||
146 | |||
147 | async_start(self.send_msgs([msg]), name="hint received notif") | ||
148 | |||
149 | def send_item_received(self, items): | ||
150 | if self.server is None: | ||
151 | return | ||
152 | |||
153 | msg = { | ||
154 | "cmd": "ItemReceived", | ||
155 | "items": items, | ||
156 | } | ||
157 | |||
158 | async_start(self.send_msgs([msg]), name="item received") | ||
159 | |||
160 | def send_location_info(self, locations): | ||
161 | if self.server is None: | ||
162 | return | ||
163 | |||
164 | msg = { | ||
165 | "cmd": "LocationInfo", | ||
166 | "locations": locations, | ||
167 | } | ||
168 | |||
169 | async_start(self.send_msgs([msg]), name="location info") | ||
170 | |||
171 | def send_text_message(self, parts): | ||
172 | if self.server is None: | ||
173 | return | ||
174 | |||
175 | msg = { | ||
176 | "cmd": "TextMessage", | ||
177 | "data": parts, | ||
178 | } | ||
179 | |||
180 | async_start(self.send_msgs([msg]), name="notif") | ||
181 | |||
182 | def send_accessible_locations(self): | ||
183 | if self.server is None: | ||
184 | return | ||
185 | |||
186 | msg = { | ||
187 | "cmd": "AccessibleLocations", | ||
188 | "locations": list(self.manager.tracker.accessible_locations), | ||
189 | } | ||
190 | |||
191 | if len(self.manager.tracker.accessible_worldports) > 0: | ||
192 | msg["worldports"] = list(self.manager.tracker.accessible_worldports) | ||
193 | |||
194 | if self.manager.tracker.goal_accessible and not self.manager.goaled: | ||
195 | msg["goal"] = True | ||
196 | |||
197 | async_start(self.send_msgs([msg]), name="accessible locations") | ||
198 | |||
199 | def send_update_locations(self, locations): | ||
200 | if self.server is None: | ||
201 | return | ||
202 | |||
203 | msg = { | ||
204 | "cmd": "UpdateLocations", | ||
205 | "locations": locations, | ||
206 | } | ||
207 | |||
208 | async_start(self.send_msgs([msg]), name="update locations") | ||
209 | |||
210 | def send_update_keyboard(self, updates): | ||
211 | if self.server is None: | ||
212 | return | ||
213 | |||
214 | msg = { | ||
215 | "cmd": "UpdateKeyboard", | ||
216 | "updates": updates, | ||
217 | } | ||
218 | |||
219 | async_start(self.send_msgs([msg]), name="update keyboard") | ||
220 | |||
221 | def send_update_worldports(self, worldports): | ||
222 | if self.server is None: | ||
223 | return | ||
224 | |||
225 | msg = { | ||
226 | "cmd": "UpdateWorldports", | ||
227 | "worldports": worldports, | ||
228 | } | ||
229 | |||
230 | async_start(self.send_msgs([msg]), name="update worldports") | ||
231 | |||
232 | def send_path_reply(self, object_type: str, object_id: int | None, path: list[str]): | ||
233 | if self.server is None: | ||
234 | return | ||
235 | |||
236 | msg = { | ||
237 | "cmd": "PathReply", | ||
238 | "type": object_type, | ||
239 | "path": path, | ||
240 | } | ||
241 | |||
242 | if object_id is not None: | ||
243 | msg["id"] = object_id | ||
244 | |||
245 | async_start(self.send_msgs([msg]), name="path reply") | ||
246 | |||
247 | async def send_msgs(self, msgs: list[Any]) -> None: | ||
248 | """ `msgs` JSON serializable """ | ||
249 | if not self.server or not self.server.socket.open or self.server.socket.closed: | ||
250 | return | ||
251 | await self.server.socket.send(encode(msgs)) | ||
252 | |||
253 | |||
254 | class Lingo2ClientContext(CommonContext): | ||
255 | manager: Lingo2Manager | ||
256 | |||
257 | game = "Lingo 2" | ||
258 | items_handling = 0b111 | ||
259 | |||
260 | slot_data: dict[str, Any] | None | ||
261 | victory_data_storage_key: str | ||
262 | |||
263 | def __init__(self, server_address: str | None = None, password: str | None = None): | ||
264 | super().__init__(server_address, password) | ||
265 | |||
266 | def make_gui(self): | ||
267 | ui = super().make_gui() | ||
268 | ui.base_title = "Archipelago Lingo 2 Client" | ||
269 | return ui | ||
270 | |||
271 | async def server_auth(self, password_requested: bool = False): | ||
272 | if password_requested and not self.password: | ||
273 | self.manager.game_ctx.send_connection_refused("Invalid password.") | ||
274 | else: | ||
275 | self.auth = self.username | ||
276 | await self.send_connect() | ||
277 | |||
278 | def handle_connection_loss(self, msg: str): | ||
279 | super().handle_connection_loss(msg) | ||
280 | |||
281 | exc_info = sys.exc_info() | ||
282 | self.manager.game_ctx.send_connection_refused(str(exc_info[1])) | ||
283 | |||
284 | def on_package(self, cmd: str, args: dict): | ||
285 | if cmd == "RoomInfo": | ||
286 | self.seed_name = args.get("seed_name", None) | ||
287 | elif cmd == "Connected": | ||
288 | self.slot_data = args.get("slot_data", None) | ||
289 | |||
290 | self.manager.reset() | ||
291 | |||
292 | self.manager.game_ctx.send_connected() | ||
293 | |||
294 | self.manager.tracker.setup_slot(self.slot_data) | ||
295 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
296 | self.manager.game_ctx.send_accessible_locations() | ||
297 | |||
298 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" | ||
299 | |||
300 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), | ||
301 | self.victory_data_storage_key) | ||
302 | msg_batch = [{ | ||
303 | "cmd": "Set", | ||
304 | "key": self.get_datastorage_key("keyboard1"), | ||
305 | "default": 0, | ||
306 | "want_reply": True, | ||
307 | "operations": [{"operation": "default", "value": 0}] | ||
308 | }, { | ||
309 | "cmd": "Set", | ||
310 | "key": self.get_datastorage_key("keyboard2"), | ||
311 | "default": 0, | ||
312 | "want_reply": True, | ||
313 | "operations": [{"operation": "default", "value": 0}] | ||
314 | }] | ||
315 | |||
316 | if self.slot_data.get("shuffle_worldports", False): | ||
317 | self.set_notify(self.get_datastorage_key("worldports")) | ||
318 | msg_batch.append({ | ||
319 | "cmd": "Set", | ||
320 | "key": self.get_datastorage_key("worldports"), | ||
321 | "default": [], | ||
322 | "want_reply": True, | ||
323 | "operations": [{"operation": "default", "value": []}] | ||
324 | }) | ||
325 | |||
326 | async_start(self.send_msgs(msg_batch), name="default keys") | ||
327 | elif cmd == "RoomUpdate": | ||
328 | if "checked_locations" in args: | ||
329 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
330 | self.manager.game_ctx.send_update_locations(args["checked_locations"]) | ||
331 | elif cmd == "ReceivedItems": | ||
332 | self.manager.tracker.set_collected_items(self.items_received) | ||
333 | |||
334 | cur_index = 0 | ||
335 | items = [] | ||
336 | |||
337 | for item in args["items"]: | ||
338 | index = cur_index + args["index"] | ||
339 | cur_index += 1 | ||
340 | |||
341 | item_msg = { | ||
342 | "id": item.item, | ||
343 | "index": index, | ||
344 | "flags": item.flags, | ||
345 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | ||
346 | } | ||
347 | |||
348 | if item.player != self.slot: | ||
349 | item_msg["sender"] = self.player_names.get(item.player) | ||
350 | |||
351 | items.append(item_msg) | ||
352 | |||
353 | self.manager.game_ctx.send_item_received(items) | ||
354 | |||
355 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): | ||
356 | self.manager.game_ctx.send_accessible_locations() | ||
357 | elif cmd == "PrintJSON": | ||
358 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | ||
359 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | ||
360 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | ||
361 | receiver_name = self.player_names.get(args["receiving"]) | ||
362 | |||
363 | if args["type"] == "Hint" and not args.get("found", False): | ||
364 | self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | ||
365 | int(args["receiving"]) == self.slot) | ||
366 | elif args["receiving"] != self.slot: | ||
367 | self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | ||
368 | |||
369 | parts = [] | ||
370 | for message_part in args["data"]: | ||
371 | if "type" not in message_part and "text" in message_part: | ||
372 | parts.append({"type": "text", "text": message_part["text"]}) | ||
373 | elif message_part["type"] == "player_id": | ||
374 | parts.append({ | ||
375 | "type": "player", | ||
376 | "text": self.player_names.get(int(message_part["text"])), | ||
377 | "self": int(int(message_part["text"]) == self.slot), | ||
378 | }) | ||
379 | elif message_part["type"] == "item_id": | ||
380 | parts.append({ | ||
381 | "type": "item", | ||
382 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
383 | "flags": message_part["flags"], | ||
384 | }) | ||
385 | elif message_part["type"] == "location_id": | ||
386 | parts.append({ | ||
387 | "type": "location", | ||
388 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
389 | message_part["player"]) | ||
390 | }) | ||
391 | elif "text" in message_part: | ||
392 | parts.append({"type": "text", "text": message_part["text"]}) | ||
393 | |||
394 | self.manager.game_ctx.send_text_message(parts) | ||
395 | elif cmd == "LocationInfo": | ||
396 | locations = [] | ||
397 | |||
398 | for location in args["locations"]: | ||
399 | locations.append({ | ||
400 | "id": location.location, | ||
401 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
402 | "player": self.player_names.get(location.player), | ||
403 | "flags": location.flags, | ||
404 | "self": int(location.player) == self.slot, | ||
405 | }) | ||
406 | |||
407 | self.manager.game_ctx.send_location_info(locations) | ||
408 | elif cmd == "Retrieved": | ||
409 | for k, v in args["keys"].items(): | ||
410 | if k == self.victory_data_storage_key: | ||
411 | self.handle_status_update(v) | ||
412 | elif cmd == "SetReply": | ||
413 | if args["key"] == self.get_datastorage_key("keyboard1"): | ||
414 | self.handle_keyboard_update(1, args) | ||
415 | elif args["key"] == self.get_datastorage_key("keyboard2"): | ||
416 | self.handle_keyboard_update(2, args) | ||
417 | elif args["key"] == self.get_datastorage_key("worldports"): | ||
418 | updates = self.manager.update_worldports(set(args["value"])) | ||
419 | if len(updates) > 0: | ||
420 | self.manager.game_ctx.send_update_worldports(updates) | ||
421 | elif args["key"] == self.victory_data_storage_key: | ||
422 | self.handle_status_update(args["value"]) | ||
423 | |||
424 | def get_datastorage_key(self, name: str): | ||
425 | return f"Lingo2_{self.slot}_{name}" | ||
426 | |||
427 | async def update_keyboard(self, updates: dict[str, int]): | ||
428 | kb1 = 0 | ||
429 | kb2 = 0 | ||
430 | |||
431 | for k, v in updates.items(): | ||
432 | if v == 0: | ||
433 | continue | ||
434 | |||
435 | effect = 0 | ||
436 | if v >= 1: | ||
437 | effect |= 1 | ||
438 | if v == 2: | ||
439 | effect |= 2 | ||
440 | |||
441 | pos = KEY_STORAGE_MAPPING[k] | ||
442 | if pos[0] == 1: | ||
443 | kb1 |= (effect << pos[1] * 2) | ||
444 | else: | ||
445 | kb2 |= (effect << pos[1] * 2) | ||
446 | |||
447 | msgs = [] | ||
448 | |||
449 | if kb1 != 0: | ||
450 | msgs.append({ | ||
451 | "cmd": "Set", | ||
452 | "key": self.get_datastorage_key("keyboard1"), | ||
453 | "want_reply": True, | ||
454 | "operations": [{ | ||
455 | "operation": "or", | ||
456 | "value": kb1 | ||
457 | }] | ||
458 | }) | ||
459 | |||
460 | if kb2 != 0: | ||
461 | msgs.append({ | ||
462 | "cmd": "Set", | ||
463 | "key": self.get_datastorage_key("keyboard2"), | ||
464 | "want_reply": True, | ||
465 | "operations": [{ | ||
466 | "operation": "or", | ||
467 | "value": kb2 | ||
468 | }] | ||
469 | }) | ||
470 | |||
471 | if len(msgs) > 0: | ||
472 | await self.send_msgs(msgs) | ||
473 | |||
474 | def handle_keyboard_update(self, field: int, args: dict[str, Any]): | ||
475 | keys = {} | ||
476 | value = args["value"] | ||
477 | |||
478 | for i in range(0, 13): | ||
479 | if (value & (1 << (i * 2))) != 0: | ||
480 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1 | ||
481 | if (value & (1 << (i * 2 + 1))) != 0: | ||
482 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2 | ||
483 | |||
484 | updates = self.manager.update_keyboard(keys) | ||
485 | if len(updates) > 0: | ||
486 | self.manager.game_ctx.send_update_keyboard(updates) | ||
487 | |||
488 | async def update_worldports(self, updates: set[int]): | ||
489 | await self.send_msgs([{ | ||
490 | "cmd": "Set", | ||
491 | "key": self.get_datastorage_key("worldports"), | ||
492 | "want_reply": True, | ||
493 | "operations": [{ | ||
494 | "operation": "update", | ||
495 | "value": updates | ||
496 | }] | ||
497 | }]) | ||
498 | |||
499 | def handle_status_update(self, value: int): | ||
500 | self.manager.goaled = (value == ClientStatus.CLIENT_GOAL) | ||
501 | self.manager.tracker.refresh_state() | ||
502 | self.manager.game_ctx.send_accessible_locations() | ||
503 | |||
504 | |||
505 | async def pipe_loop(manager: Lingo2Manager): | ||
506 | while not manager.client_ctx.exit_event.is_set(): | ||
507 | try: | ||
508 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, | ||
509 | max_size=MESSAGE_MAX_SIZE) | ||
510 | manager.game_ctx.server = Endpoint(socket) | ||
511 | logger.info("Connected to Lingo 2!") | ||
512 | if manager.client_ctx.auth is not None: | ||
513 | manager.game_ctx.send_connected() | ||
514 | manager.game_ctx.send_accessible_locations() | ||
515 | async for data in manager.game_ctx.server.socket: | ||
516 | for msg in decode(data): | ||
517 | await process_game_cmd(manager, msg) | ||
518 | except ConnectionRefusedError: | ||
519 | logger.info("Could not connect to Lingo 2.") | ||
520 | finally: | ||
521 | manager.game_ctx.server = None | ||
522 | |||
523 | |||
524 | async def process_game_cmd(manager: Lingo2Manager, args: dict): | ||
525 | cmd = args["cmd"] | ||
526 | |||
527 | if cmd == "Connect": | ||
528 | manager.client_ctx.seed_name = None | ||
529 | |||
530 | server = args.get("server") | ||
531 | player = args.get("player") | ||
532 | password = args.get("password") | ||
533 | |||
534 | if password != "": | ||
535 | server_address = f"{player}:{password}@{server}" | ||
536 | else: | ||
537 | server_address = f"{player}:None@{server}" | ||
538 | |||
539 | async_start(manager.client_ctx.connect(server_address), name="client connect") | ||
540 | elif cmd == "Disconnect": | ||
541 | manager.client_ctx.seed_name = None | ||
542 | |||
543 | async_start(manager.client_ctx.disconnect(), name="client disconnect") | ||
544 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | ||
545 | async_start(manager.client_ctx.send_msgs([args]), name="client forward") | ||
546 | elif cmd == "UpdateKeyboard": | ||
547 | updates = manager.update_keyboard(args["keyboard"]) | ||
548 | if len(updates) > 0: | ||
549 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | ||
550 | elif cmd == "CheckWorldport": | ||
551 | port_id = args["port_id"] | ||
552 | worldports = {port_id} | ||
553 | if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: | ||
554 | worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) | ||
555 | |||
556 | updates = manager.update_worldports(worldports) | ||
557 | if len(updates) > 0: | ||
558 | async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") | ||
559 | manager.game_ctx.send_update_worldports(updates) | ||
560 | elif cmd == "GetPath": | ||
561 | path = None | ||
562 | |||
563 | if args["type"] == "location": | ||
564 | path = manager.tracker.get_path_to_location(args["id"]) | ||
565 | elif args["type"] == "worldport": | ||
566 | path = manager.tracker.get_path_to_port(args["id"]) | ||
567 | elif args["type"] == "goal": | ||
568 | path = manager.tracker.get_path_to_goal() | ||
569 | |||
570 | manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path) | ||
571 | elif cmd == "Quit": | ||
572 | manager.client_ctx.exit_event.set() | ||
573 | |||
574 | |||
575 | async def run_game(): | ||
576 | exe_file = settings.get_settings().lingo2_options.exe_file | ||
577 | |||
578 | # This ensures we can use Steam features without having to open the game | ||
579 | # through steam. | ||
580 | steam_appid_path = os.path.join(os.path.dirname(exe_file), "steam_appid.txt") | ||
581 | with open(steam_appid_path, "w") as said_handle: | ||
582 | said_handle.write("2523310") | ||
583 | |||
584 | if Lingo2World.zip_path is not None: | ||
585 | # This is a packaged apworld. | ||
586 | init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") | ||
587 | init_path = Utils.local_path("data", "lingo2_init.tscn") | ||
588 | |||
589 | with open(init_path, "wb") as file_handle: | ||
590 | file_handle.write(init_scene) | ||
591 | |||
592 | subprocess.Popen( | ||
593 | [ | ||
594 | exe_file, | ||
595 | "--scene", | ||
596 | init_path, | ||
597 | "--", | ||
598 | str(Lingo2World.zip_path.absolute()), | ||
599 | ], | ||
600 | cwd=os.path.dirname(exe_file), | ||
601 | ) | ||
602 | else: | ||
603 | # The world is unzipped and being run in source. | ||
604 | subprocess.Popen( | ||
605 | [ | ||
606 | exe_file, | ||
607 | "--scene", | ||
608 | Utils.local_path("worlds", "lingo2", "client", "run_from_source.tscn"), | ||
609 | "--", | ||
610 | Utils.local_path("worlds", "lingo2", "client"), | ||
611 | ], | ||
612 | cwd=os.path.dirname(exe_file), | ||
613 | ) | ||
614 | |||
615 | |||
616 | def client_main(*launch_args: str) -> None: | ||
617 | async def main(args): | ||
618 | if settings.get_settings().lingo2_options.start_game: | ||
619 | async_start(run_game()) | ||
620 | |||
621 | client_ctx = Lingo2ClientContext(args.connect, args.password) | ||
622 | game_ctx = Lingo2GameContext() | ||
623 | manager = Lingo2Manager(game_ctx, client_ctx) | ||
624 | |||
625 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") | ||
626 | |||
627 | if gui_enabled: | ||
628 | client_ctx.run_gui() | ||
629 | client_ctx.run_cli() | ||
630 | |||
631 | pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher") | ||
632 | |||
633 | try: | ||
634 | await pipe_task | ||
635 | except Exception as e: | ||
636 | logger.exception(e) | ||
637 | |||
638 | await client_ctx.exit_event.wait() | ||
639 | client_ctx.ui.stop() | ||
640 | await client_ctx.shutdown() | ||
641 | |||
642 | Utils.init_logging("Lingo2Client", exception_logger="Client") | ||
643 | import colorama | ||
644 | |||
645 | parser = get_base_parser(description="Lingo 2 Archipelago Client") | ||
646 | parser.add_argument('--name', default=None, help="Slot Name to connect as.") | ||
647 | parser.add_argument("url", nargs="?", help="Archipelago connection url") | ||
648 | args = parser.parse_args(launch_args) | ||
649 | |||
650 | args = handle_url_arg(args, parser=parser) | ||
651 | |||
652 | colorama.just_fix_windows_console() | ||
653 | asyncio.run(main(args)) | ||
654 | colorama.deinit() | ||