about summary refs log tree commit diff stats
path: root/apworld/context.py
diff options
context:
space:
mode:
Diffstat (limited to 'apworld/context.py')
-rw-r--r--apworld/context.py802
1 files changed, 802 insertions, 0 deletions
diff --git a/apworld/context.py b/apworld/context.py new file mode 100644 index 0000000..09d8061 --- /dev/null +++ b/apworld/context.py
@@ -0,0 +1,802 @@
1import asyncio
2import os
3import pkgutil
4import subprocess
5import sys
6from typing import Any
7
8import websockets
9
10import Utils
11import settings
12from BaseClasses import ItemClassification
13from CommonClient import CommonContext, server_loop, gui_enabled, logger, get_base_parser, handle_url_arg
14from NetUtils import Endpoint, decode, encode, ClientStatus
15from Utils import async_start
16from . import Lingo2World
17from .tracker import Tracker
18
19ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz"
20MESSAGE_MAX_SIZE = 16*1024*1024
21PORT = 43182
22
23KEY_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
30REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()}
31
32
33# There is a distinction between an object's ID and its AP ID. The latter is stable between releases, whereas the former
34# can change and is also namespaced based on the object type. We should only store AP IDs in multiworld state (such as
35# slot data and data storage) to increase compatability between releases. The data we currently store is:
36# - Port pairings for worldport shuffle (slot data)
37# - Checked worldports for worldport shuffle (data storage)
38# - Latched doors (data storage)
39# The client generally deals in the actual object IDs rather than the stable IDs, although it does have to convert the
40# port pairing IDs when reading them from slot data. The context (this file here) does the work of converting back and
41# forth between the values. AP IDs are converted to IDs after reading them from data storage, and IDs are converted to
42# AP IDs before sending them to data storage.
43class Lingo2Manager:
44 game_ctx: "Lingo2GameContext"
45 client_ctx: "Lingo2ClientContext"
46 tracker: Tracker
47
48 keyboard: dict[str, int]
49 worldports: set[int]
50 goaled: bool
51 latches: set[int]
52 hinted_locations: set[int]
53
54 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"):
55 self.game_ctx = game_ctx
56 self.game_ctx.manager = self
57 self.client_ctx = client_ctx
58 self.client_ctx.manager = self
59 self.tracker = Tracker(self)
60 self.keyboard = {}
61
62 self.reset()
63
64 def reset(self):
65 for k in ALL_LETTERS:
66 self.keyboard[k] = 0
67
68 self.worldports = set()
69 self.goaled = False
70 self.latches = set()
71 self.hinted_locations = set()
72
73 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]:
74 ret: dict[str, int] = {}
75
76 for k, v in new_keyboard.items():
77 if v > self.keyboard.get(k, 0):
78 self.keyboard[k] = v
79 ret[k] = v
80
81 if len(ret) > 0:
82 self.tracker.refresh_state()
83 self.game_ctx.send_accessible_locations()
84
85 return ret
86
87 # Input should be real IDs, not AP IDs
88 def update_worldports(self, new_worldports: set[int]) -> set[int]:
89 ret = new_worldports.difference(self.worldports)
90 self.worldports.update(new_worldports)
91
92 if len(ret) > 0:
93 self.tracker.refresh_state()
94 self.game_ctx.send_accessible_locations()
95
96 return ret
97
98 def update_latches(self, new_latches: set[int]) -> set[int]:
99 ret = new_latches.difference(self.latches)
100 self.latches.update(new_latches)
101
102 return ret
103
104 def update_hinted_locations(self, new_locs: set[int]) -> set[int]:
105 ret = new_locs.difference(self.hinted_locations)
106 self.hinted_locations.update(new_locs)
107
108 return ret
109
110
111class Lingo2GameContext:
112 server: Endpoint | None
113 manager: Lingo2Manager
114
115 def __init__(self):
116 self.server = None
117
118 def send_connected(self):
119 if self.server is None:
120 return
121
122 msg = {
123 "cmd": "Connected",
124 "user": self.manager.client_ctx.username,
125 "seed_name": self.manager.client_ctx.seed_name,
126 "version": self.manager.client_ctx.server_version,
127 "generator_version": self.manager.client_ctx.generator_version,
128 "team": self.manager.client_ctx.team,
129 "slot": self.manager.client_ctx.slot,
130 "checked_locations": self.manager.client_ctx.checked_locations,
131 "slot_data": self.manager.client_ctx.slot_data,
132 }
133
134 async_start(self.send_msgs([msg]), name="game Connected")
135
136 def send_connection_refused(self, text):
137 if self.server is None:
138 return
139
140 msg = {
141 "cmd": "ConnectionRefused",
142 "text": text,
143 }
144
145 async_start(self.send_msgs([msg]), name="game ConnectionRefused")
146
147 def send_item_sent_notification(self, item_name, receiver_name, item_flags):
148 if self.server is None:
149 return
150
151 msg = {
152 "cmd": "ItemSentNotif",
153 "item_name": item_name,
154 "receiver_name": receiver_name,
155 "item_flags": item_flags,
156 }
157
158 async_start(self.send_msgs([msg]), name="item sent notif")
159
160 def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self):
161 if self.server is None:
162 return
163
164 msg = {
165 "cmd": "HintReceived",
166 "item_name": item_name,
167 "location_name": location_name,
168 "receiver_name": receiver_name,
169 "item_flags": item_flags,
170 "self": int(for_self),
171 }
172
173 async_start(self.send_msgs([msg]), name="hint received notif")
174
175 def send_item_received(self, items):
176 if self.server is None:
177 return
178
179 msg = {
180 "cmd": "ItemReceived",
181 "items": items,
182 }
183
184 async_start(self.send_msgs([msg]), name="item received")
185
186 def send_location_info(self, locations):
187 if self.server is None:
188 return
189
190 msg = {
191 "cmd": "LocationInfo",
192 "locations": locations,
193 }
194
195 async_start(self.send_msgs([msg]), name="location info")
196
197 def send_text_message(self, parts):
198 if self.server is None:
199 return
200
201 msg = {
202 "cmd": "TextMessage",
203 "data": parts,
204 }
205
206 async_start(self.send_msgs([msg]), name="notif")
207
208 def send_accessible_locations(self):
209 if self.server is None:
210 return
211
212 msg = {
213 "cmd": "AccessibleLocations",
214 "locations": list(self.manager.tracker.accessible_locations),
215 }
216
217 if len(self.manager.tracker.accessible_worldports) > 0:
218 msg["worldports"] = list(self.manager.tracker.accessible_worldports)
219
220 if self.manager.tracker.goal_accessible and not self.manager.goaled:
221 msg["goal"] = True
222
223 async_start(self.send_msgs([msg]), name="accessible locations")
224
225 def send_update_locations(self, locations):
226 if self.server is None:
227 return
228
229 msg = {
230 "cmd": "UpdateLocations",
231 "locations": locations,
232 }
233
234 async_start(self.send_msgs([msg]), name="update locations")
235
236 def send_update_keyboard(self, updates):
237 if self.server is None:
238 return
239
240 msg = {
241 "cmd": "UpdateKeyboard",
242 "updates": updates,
243 }
244
245 async_start(self.send_msgs([msg]), name="update keyboard")
246
247 # Input should be real IDs, not AP IDs
248 def send_update_worldports(self, worldports):
249 if self.server is None:
250 return
251
252 msg = {
253 "cmd": "UpdateWorldports",
254 "worldports": worldports,
255 }
256
257 async_start(self.send_msgs([msg]), name="update worldports")
258
259 def send_path_reply(self, object_type: str, object_id: int | None, path: list[str]):
260 if self.server is None:
261 return
262
263 msg = {
264 "cmd": "PathReply",
265 "type": object_type,
266 "path": path,
267 }
268
269 if object_id is not None:
270 msg["id"] = object_id
271
272 async_start(self.send_msgs([msg]), name="path reply")
273
274 def send_update_latches(self, latches):
275 if self.server is None:
276 return
277
278 msg = {
279 "cmd": "UpdateLatches",
280 "latches": latches,
281 }
282
283 async_start(self.send_msgs([msg]), name="update latches")
284
285 def send_ignored_locations(self, ignored_locations):
286 if self.server is None:
287 return
288
289 msg = {
290 "cmd": "SetIgnoredLocations",
291 "locations": ignored_locations,
292 }
293
294 async_start(self.send_msgs([msg]), name="set ignored locations")
295
296 def send_update_hinted_locations(self, hinted_locations):
297 if self.server is None:
298 return
299
300 msg = {
301 "cmd": "UpdateHintedLocations",
302 "locations": hinted_locations,
303 }
304
305 async_start(self.send_msgs([msg]), name="update hinted locations")
306
307 async def send_msgs(self, msgs: list[Any]) -> None:
308 """ `msgs` JSON serializable """
309 if not self.server or not self.server.socket.open or self.server.socket.closed:
310 return
311 await self.server.socket.send(encode(msgs))
312
313
314class Lingo2ClientContext(CommonContext):
315 manager: Lingo2Manager
316
317 game = "Lingo 2"
318 items_handling = 0b111
319
320 slot_data: dict[str, Any] | None
321 hints_data_storage_key: str
322 victory_data_storage_key: str
323
324 def __init__(self, server_address: str | None = None, password: str | None = None):
325 super().__init__(server_address, password)
326
327 def make_gui(self):
328 ui = super().make_gui()
329 ui.base_title = "Archipelago Lingo 2 Client"
330 return ui
331
332 async def server_auth(self, password_requested: bool = False):
333 if password_requested and not self.password:
334 self.manager.game_ctx.send_connection_refused("Invalid password.")
335 else:
336 self.auth = self.username
337 await self.send_connect()
338
339 def handle_connection_loss(self, msg: str):
340 super().handle_connection_loss(msg)
341
342 exc_info = sys.exc_info()
343 self.manager.game_ctx.send_connection_refused(str(exc_info[1]))
344
345 def on_package(self, cmd: str, args: dict):
346 if cmd == "RoomInfo":
347 self.seed_name = args.get("seed_name", None)
348 elif cmd == "Connected":
349 self.slot_data = args.get("slot_data", None)
350
351 self.manager.reset()
352
353 self.manager.game_ctx.send_connected()
354
355 self.manager.tracker.setup_slot(self.slot_data)
356 self.manager.tracker.set_checked_locations(self.checked_locations)
357 self.manager.game_ctx.send_accessible_locations()
358
359 self.hints_data_storage_key = f"_read_hints_{self.team}_{self.slot}"
360 self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}"
361
362 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"),
363 self.victory_data_storage_key, self.get_datastorage_key("latches"),
364 self.get_datastorage_key("ignored_locations"))
365 msg_batch = [{
366 "cmd": "Set",
367 "key": self.get_datastorage_key("keyboard1"),
368 "default": 0,
369 "want_reply": True,
370 "operations": [{"operation": "default", "value": 0}]
371 }, {
372 "cmd": "Set",
373 "key": self.get_datastorage_key("keyboard2"),
374 "default": 0,
375 "want_reply": True,
376 "operations": [{"operation": "default", "value": 0}]
377 }, {
378 "cmd": "Set",
379 "key": self.get_datastorage_key("latches"),
380 "default": [],
381 "want_reply": True,
382 "operations": [{"operation": "default", "value": []}]
383 }, {
384 "cmd": "Set",
385 "key": self.get_datastorage_key("ignored_locations"),
386 "default": [],
387 "want_reply": True,
388 "operations": [{"operation": "default", "value": []}]
389 }]
390
391 if self.slot_data.get("shuffle_worldports", False):
392 self.set_notify(self.get_datastorage_key("worldports"))
393 msg_batch.append({
394 "cmd": "Set",
395 "key": self.get_datastorage_key("worldports"),
396 "default": [],
397 "want_reply": True,
398 "operations": [{"operation": "default", "value": []}]
399 })
400
401 async_start(self.send_msgs(msg_batch), name="default keys")
402 elif cmd == "RoomUpdate":
403 if "checked_locations" in args:
404 self.manager.tracker.set_checked_locations(self.checked_locations)
405 self.manager.game_ctx.send_update_locations(args["checked_locations"])
406 elif cmd == "ReceivedItems":
407 self.manager.tracker.set_collected_items(self.items_received)
408
409 cur_index = 0
410 items = []
411
412 for item in args["items"]:
413 index = cur_index + args["index"]
414 cur_index += 1
415
416 item_msg = {
417 "id": item.item,
418 "index": index,
419 "flags": item.flags,
420 "text": self.item_names.lookup_in_slot(item.item, self.slot),
421 }
422
423 if item.player != self.slot:
424 item_msg["sender"] = self.player_names.get(item.player)
425
426 items.append(item_msg)
427
428 self.manager.game_ctx.send_item_received(items)
429
430 if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]):
431 self.manager.game_ctx.send_accessible_locations()
432 elif cmd == "PrintJSON":
433 if "receiving" in args and "item" in args and args["item"].player == self.slot:
434 item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"])
435 location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player)
436 receiver_name = self.player_names.get(args["receiving"])
437
438 if args["type"] == "Hint" and not args.get("found", False):
439 self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags,
440 int(args["receiving"]) == self.slot)
441 elif args["receiving"] != self.slot:
442 self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags)
443
444 parts = []
445 for message_part in args["data"]:
446 if "type" not in message_part and "text" in message_part:
447 parts.append({"type": "text", "text": message_part["text"]})
448 elif message_part["type"] == "player_id":
449 parts.append({
450 "type": "player",
451 "text": self.player_names.get(int(message_part["text"])),
452 "self": int(int(message_part["text"]) == self.slot),
453 })
454 elif message_part["type"] == "item_id":
455 parts.append({
456 "type": "item",
457 "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]),
458 "flags": message_part["flags"],
459 })
460 elif message_part["type"] == "location_id":
461 parts.append({
462 "type": "location",
463 "text": self.location_names.lookup_in_slot(int(message_part["text"]),
464 message_part["player"])
465 })
466 elif "text" in message_part:
467 parts.append({"type": "text", "text": message_part["text"]})
468
469 self.manager.game_ctx.send_text_message(parts)
470 elif cmd == "LocationInfo":
471 locations = []
472
473 for location in args["locations"]:
474 locations.append({
475 "id": location.location,
476 "item": self.item_names.lookup_in_slot(location.item, location.player),
477 "player": self.player_names.get(location.player),
478 "flags": location.flags,
479 "self": int(location.player) == self.slot,
480 })
481
482 self.manager.game_ctx.send_location_info(locations)
483 elif cmd == "Retrieved":
484 for k, v in args["keys"].items():
485 if k == self.victory_data_storage_key:
486 self.handle_status_update(v)
487 elif k == self.hints_data_storage_key:
488 self.update_hints()
489 elif cmd == "SetReply":
490 if args["key"] == self.get_datastorage_key("keyboard1"):
491 self.handle_keyboard_update(1, args)
492 elif args["key"] == self.get_datastorage_key("keyboard2"):
493 self.handle_keyboard_update(2, args)
494 elif args["key"] == self.get_datastorage_key("worldports"):
495 port_ids = set(Lingo2World.static_logic.port_id_by_ap_id[ap_id] for ap_id in args["value"]
496 if ap_id in Lingo2World.static_logic.port_id_by_ap_id)
497 updates = self.manager.update_worldports(port_ids)
498 if len(updates) > 0:
499 self.manager.game_ctx.send_update_worldports(updates)
500 elif args["key"] == self.victory_data_storage_key:
501 self.handle_status_update(args["value"])
502 elif args["key"] == self.get_datastorage_key("latches"):
503 door_ids = set(Lingo2World.static_logic.door_id_by_ap_id[ap_id] for ap_id in args["value"]
504 if ap_id in Lingo2World.static_logic.door_id_by_ap_id)
505 updates = self.manager.update_latches(door_ids)
506 if len(updates) > 0:
507 self.manager.game_ctx.send_update_latches(updates)
508 elif args["key"] == self.get_datastorage_key("ignored_locations"):
509 self.manager.game_ctx.send_ignored_locations(args["value"])
510 elif args["key"] == self.hints_data_storage_key:
511 self.update_hints()
512
513 def get_datastorage_key(self, name: str):
514 return f"Lingo2_{self.slot}_{name}"
515
516 async def update_keyboard(self, updates: dict[str, int]):
517 kb1 = 0
518 kb2 = 0
519
520 for k, v in updates.items():
521 if v == 0:
522 continue
523
524 effect = 0
525 if v >= 1:
526 effect |= 1
527 if v == 2:
528 effect |= 2
529
530 pos = KEY_STORAGE_MAPPING[k]
531 if pos[0] == 1:
532 kb1 |= (effect << pos[1] * 2)
533 else:
534 kb2 |= (effect << pos[1] * 2)
535
536 msgs = []
537
538 if kb1 != 0:
539 msgs.append({
540 "cmd": "Set",
541 "key": self.get_datastorage_key("keyboard1"),
542 "want_reply": True,
543 "operations": [{
544 "operation": "or",
545 "value": kb1
546 }]
547 })
548
549 if kb2 != 0:
550 msgs.append({
551 "cmd": "Set",
552 "key": self.get_datastorage_key("keyboard2"),
553 "want_reply": True,
554 "operations": [{
555 "operation": "or",
556 "value": kb2
557 }]
558 })
559
560 if len(msgs) > 0:
561 await self.send_msgs(msgs)
562
563 def handle_keyboard_update(self, field: int, args: dict[str, Any]):
564 keys = {}
565 value = args["value"]
566
567 for i in range(0, 13):
568 if (value & (1 << (i * 2))) != 0:
569 keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1
570 if (value & (1 << (i * 2 + 1))) != 0:
571 keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2
572
573 updates = self.manager.update_keyboard(keys)
574 if len(updates) > 0:
575 self.manager.game_ctx.send_update_keyboard(updates)
576
577 # Input should be real IDs, not AP IDs
578 async def update_worldports(self, updates: set[int]):
579 port_ap_ids = [Lingo2World.static_logic.objects.ports[port_id].ap_id for port_id in updates]
580 await self.send_msgs([{
581 "cmd": "Set",
582 "key": self.get_datastorage_key("worldports"),
583 "want_reply": True,
584 "operations": [{
585 "operation": "update",
586 "value": port_ap_ids
587 }]
588 }])
589
590 def handle_status_update(self, value: int):
591 self.manager.goaled = (value == ClientStatus.CLIENT_GOAL)
592 self.manager.tracker.refresh_state()
593 self.manager.game_ctx.send_accessible_locations()
594
595 async def update_latches(self, updates: set[int]):
596 door_ap_ids = [Lingo2World.static_logic.objects.doors[door_id].ap_id for door_id in updates]
597 await self.send_msgs([{
598 "cmd": "Set",
599 "key": self.get_datastorage_key("latches"),
600 "want_reply": True,
601 "operations": [{
602 "operation": "update",
603 "value": door_ap_ids
604 }]
605 }])
606
607 async def add_ignored_location(self, loc_id: int):
608 await self.send_msgs([{
609 "cmd": "Set",
610 "key": self.get_datastorage_key("ignored_locations"),
611 "want_reply": True,
612 "operations": [{
613 "operation": "update",
614 "value": [loc_id]
615 }]
616 }])
617
618 async def remove_ignored_location(self, loc_id: int):
619 await self.send_msgs([{
620 "cmd": "Set",
621 "key": self.get_datastorage_key("ignored_locations"),
622 "want_reply": True,
623 "operations": [{
624 "operation": "remove",
625 "value": loc_id
626 }]
627 }])
628
629 def update_hints(self):
630 hints = self.stored_data.get(self.hints_data_storage_key, [])
631
632 hinted_locations = set(hint["location"] for hint in hints if hint["finding_player"] == self.slot)
633 updates = self.manager.update_hinted_locations(hinted_locations)
634 if len(updates) > 0:
635 self.manager.game_ctx.send_update_hinted_locations(updates)
636
637
638async def pipe_loop(manager: Lingo2Manager):
639 while not manager.client_ctx.exit_event.is_set():
640 try:
641 socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None,
642 max_size=MESSAGE_MAX_SIZE)
643 manager.game_ctx.server = Endpoint(socket)
644 logger.info("Connected to Lingo 2!")
645 if manager.client_ctx.auth is not None:
646 manager.game_ctx.send_connected()
647 manager.game_ctx.send_accessible_locations()
648 async for data in manager.game_ctx.server.socket:
649 for msg in decode(data):
650 await process_game_cmd(manager, msg)
651 except ConnectionRefusedError:
652 logger.info("Could not connect to Lingo 2.")
653 finally:
654 manager.game_ctx.server = None
655
656
657async def process_game_cmd(manager: Lingo2Manager, args: dict):
658 cmd = args["cmd"]
659
660 if cmd == "Connect":
661 manager.client_ctx.seed_name = None
662
663 server = args.get("server")
664 player = args.get("player")
665 password = args.get("password")
666
667 if password != "":
668 server_address = f"{player}:{password}@{server}"
669 else:
670 server_address = f"{player}:None@{server}"
671
672 async_start(manager.client_ctx.connect(server_address), name="client connect")
673 elif cmd == "Disconnect":
674 manager.client_ctx.seed_name = None
675
676 async_start(manager.client_ctx.disconnect(), name="client disconnect")
677 elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]:
678 async_start(manager.client_ctx.send_msgs([args]), name="client forward")
679 elif cmd == "UpdateKeyboard":
680 updates = manager.update_keyboard(args["keyboard"])
681 if len(updates) > 0:
682 async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard")
683 elif cmd == "CheckWorldport":
684 port_id = args["port_id"]
685 port_ap_id = Lingo2World.static_logic.objects.ports[port_id].ap_id
686 worldports = {port_id}
687
688 # Also check the reverse port if it's a two-way connection.
689 port_pairings = manager.client_ctx.slot_data["port_pairings"]
690 if str(port_ap_id) in port_pairings and\
691 port_pairings.get(str(port_pairings[str(port_ap_id)]), None) == port_ap_id:
692 worldports.add(Lingo2World.static_logic.port_id_by_ap_id[port_pairings[str(port_ap_id)]])
693
694 updates = manager.update_worldports(worldports)
695 if len(updates) > 0:
696 async_start(manager.client_ctx.update_worldports(updates), name="client update worldports")
697 manager.game_ctx.send_update_worldports(updates)
698 elif cmd == "GetPath":
699 path = None
700
701 if args["type"] == "location":
702 path = manager.tracker.get_path_to_location(args["id"])
703 elif args["type"] == "worldport":
704 path = manager.tracker.get_path_to_port(args["id"])
705 elif args["type"] == "goal":
706 path = manager.tracker.get_path_to_goal()
707
708 manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path)
709 elif cmd == "LatchDoor":
710 updates = manager.update_latches({args["door"]})
711 if len(updates) > 0:
712 async_start(manager.client_ctx.update_latches(updates), name="client update latches")
713 elif cmd == "IgnoreLocation":
714 async_start(manager.client_ctx.add_ignored_location(args["id"]), name="client ignore loc")
715 elif cmd == "UnignoreLocation":
716 async_start(manager.client_ctx.remove_ignored_location(args["id"]), name="client unignore loc")
717 elif cmd == "Quit":
718 manager.client_ctx.exit_event.set()
719
720
721async def run_game():
722 exe_file = settings.get_settings().lingo2_options.exe_file
723
724 # This ensures we can use Steam features without having to open the game
725 # through steam.
726 steam_appid_path = os.path.join(os.path.dirname(exe_file), "steam_appid.txt")
727 with open(steam_appid_path, "w") as said_handle:
728 said_handle.write("2523310")
729
730 if Lingo2World.zip_path is not None:
731 # This is a packaged apworld.
732 init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn")
733 init_path = Utils.local_path("data", "lingo2_init.tscn")
734
735 with open(init_path, "wb") as file_handle:
736 file_handle.write(init_scene)
737
738 subprocess.Popen(
739 [
740 exe_file,
741 "--scene",
742 init_path,
743 "--",
744 str(Lingo2World.zip_path.absolute()),
745 ],
746 cwd=os.path.dirname(exe_file),
747 )
748 else:
749 # The world is unzipped and being run in source.
750 subprocess.Popen(
751 [
752 exe_file,
753 "--scene",
754 Utils.local_path("worlds", "lingo2", "client", "run_from_source.tscn"),
755 "--",
756 Utils.local_path("worlds", "lingo2", "client"),
757 ],
758 cwd=os.path.dirname(exe_file),
759 )
760
761
762def client_main(*launch_args: str) -> None:
763 async def main(args):
764 if settings.get_settings().lingo2_options.start_game:
765 async_start(run_game())
766
767 client_ctx = Lingo2ClientContext(args.connect, args.password)
768 client_ctx.auth = args.name
769
770 game_ctx = Lingo2GameContext()
771 manager = Lingo2Manager(game_ctx, client_ctx)
772
773 client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop")
774
775 if gui_enabled:
776 client_ctx.run_gui()
777 client_ctx.run_cli()
778
779 pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher")
780
781 try:
782 await pipe_task
783 except Exception as e:
784 logger.exception(e)
785
786 await client_ctx.exit_event.wait()
787 client_ctx.ui.stop()
788 await client_ctx.shutdown()
789
790 Utils.init_logging("Lingo2Client", exception_logger="Client")
791 import colorama
792
793 parser = get_base_parser(description="Lingo 2 Archipelago Client")
794 parser.add_argument('--name', default=None, help="Slot Name to connect as.")
795 parser.add_argument("url", nargs="?", help="Archipelago connection url")
796 args = parser.parse_args(launch_args)
797
798 args = handle_url_arg(args, parser=parser)
799
800 colorama.just_fix_windows_console()
801 asyncio.run(main(args))
802 colorama.deinit()