diff options
Diffstat (limited to 'apworld/context.py')
| -rw-r--r-- | apworld/context.py | 630 |
1 files changed, 630 insertions, 0 deletions
| diff --git a/apworld/context.py b/apworld/context.py new file mode 100644 index 0000000..63645a4 --- /dev/null +++ b/apworld/context.py | |||
| @@ -0,0 +1,630 @@ | |||
| 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 | async def send_msgs(self, msgs: list[Any]) -> None: | ||
| 233 | """ `msgs` JSON serializable """ | ||
| 234 | if not self.server or not self.server.socket.open or self.server.socket.closed: | ||
| 235 | return | ||
| 236 | await self.server.socket.send(encode(msgs)) | ||
| 237 | |||
| 238 | |||
| 239 | class Lingo2ClientContext(CommonContext): | ||
| 240 | manager: Lingo2Manager | ||
| 241 | |||
| 242 | game = "Lingo 2" | ||
| 243 | items_handling = 0b111 | ||
| 244 | |||
| 245 | slot_data: dict[str, Any] | None | ||
| 246 | victory_data_storage_key: str | ||
| 247 | |||
| 248 | def __init__(self, server_address: str | None = None, password: str | None = None): | ||
| 249 | super().__init__(server_address, password) | ||
| 250 | |||
| 251 | def make_gui(self): | ||
| 252 | ui = super().make_gui() | ||
| 253 | ui.base_title = "Archipelago Lingo 2 Client" | ||
| 254 | return ui | ||
| 255 | |||
| 256 | async def server_auth(self, password_requested: bool = False): | ||
| 257 | if password_requested: | ||
| 258 | if self.password is None: | ||
| 259 | self.manager.game_ctx.send_connection_refused("Slot requires a password.") | ||
| 260 | else: | ||
| 261 | self.manager.game_ctx.send_connection_refused("Invalid password.") | ||
| 262 | else: | ||
| 263 | self.auth = self.username | ||
| 264 | await self.send_connect() | ||
| 265 | |||
| 266 | def handle_connection_loss(self, msg: str): | ||
| 267 | super().handle_connection_loss(msg) | ||
| 268 | |||
| 269 | exc_info = sys.exc_info() | ||
| 270 | self.manager.game_ctx.send_connection_refused(str(exc_info[1])) | ||
| 271 | |||
| 272 | def on_package(self, cmd: str, args: dict): | ||
| 273 | if cmd == "RoomInfo": | ||
| 274 | self.seed_name = args.get("seed_name", None) | ||
| 275 | elif cmd == "Connected": | ||
| 276 | self.slot_data = args.get("slot_data", None) | ||
| 277 | |||
| 278 | self.manager.reset() | ||
| 279 | |||
| 280 | self.manager.game_ctx.send_connected() | ||
| 281 | |||
| 282 | self.manager.tracker.setup_slot(self.slot_data) | ||
| 283 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
| 284 | self.manager.game_ctx.send_accessible_locations() | ||
| 285 | |||
| 286 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" | ||
| 287 | |||
| 288 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), | ||
| 289 | self.victory_data_storage_key) | ||
| 290 | msg_batch = [{ | ||
| 291 | "cmd": "Set", | ||
| 292 | "key": self.get_datastorage_key("keyboard1"), | ||
| 293 | "default": 0, | ||
| 294 | "want_reply": True, | ||
| 295 | "operations": [{"operation": "default", "value": 0}] | ||
| 296 | }, { | ||
| 297 | "cmd": "Set", | ||
| 298 | "key": self.get_datastorage_key("keyboard2"), | ||
| 299 | "default": 0, | ||
| 300 | "want_reply": True, | ||
| 301 | "operations": [{"operation": "default", "value": 0}] | ||
| 302 | }] | ||
| 303 | |||
| 304 | if self.slot_data.get("shuffle_worldports", False): | ||
| 305 | self.set_notify(self.get_datastorage_key("worldports")) | ||
| 306 | msg_batch.append({ | ||
| 307 | "cmd": "Set", | ||
| 308 | "key": self.get_datastorage_key("worldports"), | ||
| 309 | "default": [], | ||
| 310 | "want_reply": True, | ||
| 311 | "operations": [{"operation": "default", "value": []}] | ||
| 312 | }) | ||
| 313 | |||
| 314 | async_start(self.send_msgs(msg_batch), name="default keys") | ||
| 315 | elif cmd == "RoomUpdate": | ||
| 316 | if "checked_locations" in args: | ||
| 317 | self.manager.tracker.set_checked_locations(self.checked_locations) | ||
| 318 | self.manager.game_ctx.send_update_locations(args["checked_locations"]) | ||
| 319 | elif cmd == "ReceivedItems": | ||
| 320 | self.manager.tracker.set_collected_items(self.items_received) | ||
| 321 | |||
| 322 | cur_index = 0 | ||
| 323 | items = [] | ||
| 324 | |||
| 325 | for item in args["items"]: | ||
| 326 | index = cur_index + args["index"] | ||
| 327 | cur_index += 1 | ||
| 328 | |||
| 329 | item_msg = { | ||
| 330 | "id": item.item, | ||
| 331 | "index": index, | ||
| 332 | "flags": item.flags, | ||
| 333 | "text": self.item_names.lookup_in_slot(item.item, self.slot), | ||
| 334 | } | ||
| 335 | |||
| 336 | if item.player != self.slot: | ||
| 337 | item_msg["sender"] = self.player_names.get(item.player) | ||
| 338 | |||
| 339 | items.append(item_msg) | ||
| 340 | |||
| 341 | self.manager.game_ctx.send_item_received(items) | ||
| 342 | |||
| 343 | if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): | ||
| 344 | self.manager.game_ctx.send_accessible_locations() | ||
| 345 | elif cmd == "PrintJSON": | ||
| 346 | if "receiving" in args and "item" in args and args["item"].player == self.slot: | ||
| 347 | item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) | ||
| 348 | location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) | ||
| 349 | receiver_name = self.player_names.get(args["receiving"]) | ||
| 350 | |||
| 351 | if args["type"] == "Hint" and not args.get("found", False): | ||
| 352 | self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, | ||
| 353 | int(args["receiving"]) == self.slot) | ||
| 354 | elif args["receiving"] != self.slot: | ||
| 355 | self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) | ||
| 356 | |||
| 357 | parts = [] | ||
| 358 | for message_part in args["data"]: | ||
| 359 | if "type" not in message_part and "text" in message_part: | ||
| 360 | parts.append({"type": "text", "text": message_part["text"]}) | ||
| 361 | elif message_part["type"] == "player_id": | ||
| 362 | parts.append({ | ||
| 363 | "type": "player", | ||
| 364 | "text": self.player_names.get(int(message_part["text"])), | ||
| 365 | "self": int(int(message_part["text"]) == self.slot), | ||
| 366 | }) | ||
| 367 | elif message_part["type"] == "item_id": | ||
| 368 | parts.append({ | ||
| 369 | "type": "item", | ||
| 370 | "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]), | ||
| 371 | "flags": message_part["flags"], | ||
| 372 | }) | ||
| 373 | elif message_part["type"] == "location_id": | ||
| 374 | parts.append({ | ||
| 375 | "type": "location", | ||
| 376 | "text": self.location_names.lookup_in_slot(int(message_part["text"]), | ||
| 377 | message_part["player"]) | ||
| 378 | }) | ||
| 379 | elif "text" in message_part: | ||
| 380 | parts.append({"type": "text", "text": message_part["text"]}) | ||
| 381 | |||
| 382 | self.manager.game_ctx.send_text_message(parts) | ||
| 383 | elif cmd == "LocationInfo": | ||
| 384 | locations = [] | ||
| 385 | |||
| 386 | for location in args["locations"]: | ||
| 387 | locations.append({ | ||
| 388 | "id": location.location, | ||
| 389 | "item": self.item_names.lookup_in_slot(location.item, location.player), | ||
| 390 | "player": self.player_names.get(location.player), | ||
| 391 | "flags": location.flags, | ||
| 392 | "self": int(location.player) == self.slot, | ||
| 393 | }) | ||
| 394 | |||
| 395 | self.manager.game_ctx.send_location_info(locations) | ||
| 396 | elif cmd == "Retrieved": | ||
| 397 | for k, v in args["keys"].items(): | ||
| 398 | if k == self.victory_data_storage_key: | ||
| 399 | self.handle_status_update(v) | ||
| 400 | elif cmd == "SetReply": | ||
| 401 | if args["key"] == self.get_datastorage_key("keyboard1"): | ||
| 402 | self.handle_keyboard_update(1, args) | ||
| 403 | elif args["key"] == self.get_datastorage_key("keyboard2"): | ||
| 404 | self.handle_keyboard_update(2, args) | ||
| 405 | elif args["key"] == self.get_datastorage_key("worldports"): | ||
| 406 | updates = self.manager.update_worldports(set(args["value"])) | ||
| 407 | if len(updates) > 0: | ||
| 408 | self.manager.game_ctx.send_update_worldports(updates) | ||
| 409 | elif args["key"] == self.victory_data_storage_key: | ||
| 410 | self.handle_status_update(args["value"]) | ||
| 411 | |||
| 412 | def get_datastorage_key(self, name: str): | ||
| 413 | return f"Lingo2_{self.slot}_{name}" | ||
| 414 | |||
| 415 | async def update_keyboard(self, updates: dict[str, int]): | ||
| 416 | kb1 = 0 | ||
| 417 | kb2 = 0 | ||
| 418 | |||
| 419 | for k, v in updates.items(): | ||
| 420 | if v == 0: | ||
| 421 | continue | ||
| 422 | |||
| 423 | effect = 0 | ||
| 424 | if v >= 1: | ||
| 425 | effect |= 1 | ||
| 426 | if v == 2: | ||
| 427 | effect |= 2 | ||
| 428 | |||
| 429 | pos = KEY_STORAGE_MAPPING[k] | ||
| 430 | if pos[0] == 1: | ||
| 431 | kb1 |= (effect << pos[1] * 2) | ||
| 432 | else: | ||
| 433 | kb2 |= (effect << pos[1] * 2) | ||
| 434 | |||
| 435 | msgs = [] | ||
| 436 | |||
| 437 | if kb1 != 0: | ||
| 438 | msgs.append({ | ||
| 439 | "cmd": "Set", | ||
| 440 | "key": self.get_datastorage_key("keyboard1"), | ||
| 441 | "want_reply": True, | ||
| 442 | "operations": [{ | ||
| 443 | "operation": "or", | ||
| 444 | "value": kb1 | ||
| 445 | }] | ||
| 446 | }) | ||
| 447 | |||
| 448 | if kb2 != 0: | ||
| 449 | msgs.append({ | ||
| 450 | "cmd": "Set", | ||
| 451 | "key": self.get_datastorage_key("keyboard2"), | ||
| 452 | "want_reply": True, | ||
| 453 | "operations": [{ | ||
| 454 | "operation": "or", | ||
| 455 | "value": kb2 | ||
| 456 | }] | ||
| 457 | }) | ||
| 458 | |||
| 459 | if len(msgs) > 0: | ||
| 460 | await self.send_msgs(msgs) | ||
| 461 | |||
| 462 | def handle_keyboard_update(self, field: int, args: dict[str, Any]): | ||
| 463 | keys = {} | ||
| 464 | value = args["value"] | ||
| 465 | |||
| 466 | for i in range(0, 13): | ||
| 467 | if (value & (1 << (i * 2))) != 0: | ||
| 468 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1 | ||
| 469 | if (value & (1 << (i * 2 + 1))) != 0: | ||
| 470 | keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2 | ||
| 471 | |||
| 472 | updates = self.manager.update_keyboard(keys) | ||
| 473 | if len(updates) > 0: | ||
| 474 | self.manager.game_ctx.send_update_keyboard(updates) | ||
| 475 | |||
| 476 | async def update_worldports(self, updates: set[int]): | ||
| 477 | await self.send_msgs([{ | ||
| 478 | "cmd": "Set", | ||
| 479 | "key": self.get_datastorage_key("worldports"), | ||
| 480 | "want_reply": True, | ||
| 481 | "operations": [{ | ||
| 482 | "operation": "update", | ||
| 483 | "value": updates | ||
| 484 | }] | ||
| 485 | }]) | ||
| 486 | |||
| 487 | def handle_status_update(self, value: int): | ||
| 488 | self.manager.goaled = (value == ClientStatus.CLIENT_GOAL) | ||
| 489 | self.manager.tracker.refresh_state() | ||
| 490 | self.manager.game_ctx.send_accessible_locations() | ||
| 491 | |||
| 492 | |||
| 493 | async def pipe_loop(manager: Lingo2Manager): | ||
| 494 | while not manager.client_ctx.exit_event.is_set(): | ||
| 495 | try: | ||
| 496 | socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, | ||
| 497 | max_size=MESSAGE_MAX_SIZE) | ||
| 498 | manager.game_ctx.server = Endpoint(socket) | ||
| 499 | logger.info("Connected to Lingo 2!") | ||
| 500 | if manager.client_ctx.auth is not None: | ||
| 501 | manager.game_ctx.send_connected() | ||
| 502 | manager.game_ctx.send_accessible_locations() | ||
| 503 | async for data in manager.game_ctx.server.socket: | ||
| 504 | for msg in decode(data): | ||
| 505 | await process_game_cmd(manager, msg) | ||
| 506 | except ConnectionRefusedError: | ||
| 507 | logger.info("Could not connect to Lingo 2.") | ||
| 508 | finally: | ||
| 509 | manager.game_ctx.server = None | ||
| 510 | |||
| 511 | |||
| 512 | async def process_game_cmd(manager: Lingo2Manager, args: dict): | ||
| 513 | cmd = args["cmd"] | ||
| 514 | |||
| 515 | if cmd == "Connect": | ||
| 516 | manager.client_ctx.seed_name = None | ||
| 517 | |||
| 518 | server = args.get("server") | ||
| 519 | player = args.get("player") | ||
| 520 | password = args.get("password") | ||
| 521 | |||
| 522 | if password != "": | ||
| 523 | server_address = f"{player}:{password}@{server}" | ||
| 524 | else: | ||
| 525 | server_address = f"{player}:None@{server}" | ||
| 526 | |||
| 527 | async_start(manager.client_ctx.connect(server_address), name="client connect") | ||
| 528 | elif cmd == "Disconnect": | ||
| 529 | manager.client_ctx.seed_name = None | ||
| 530 | |||
| 531 | async_start(manager.client_ctx.disconnect(), name="client disconnect") | ||
| 532 | elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: | ||
| 533 | async_start(manager.client_ctx.send_msgs([args]), name="client forward") | ||
| 534 | elif cmd == "UpdateKeyboard": | ||
| 535 | updates = manager.update_keyboard(args["keyboard"]) | ||
| 536 | if len(updates) > 0: | ||
| 537 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | ||
| 538 | elif cmd == "CheckWorldport": | ||
| 539 | port_id = args["port_id"] | ||
| 540 | worldports = {port_id} | ||
| 541 | if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: | ||
| 542 | worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) | ||
| 543 | |||
| 544 | updates = manager.update_worldports(worldports) | ||
| 545 | if len(updates) > 0: | ||
| 546 | async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") | ||
| 547 | manager.game_ctx.send_update_worldports(updates) | ||
| 548 | elif cmd == "Quit": | ||
| 549 | manager.client_ctx.exit_event.set() | ||
| 550 | |||
| 551 | |||
| 552 | async def run_game(): | ||
| 553 | exe_file = settings.get_settings().lingo2_options.exe_file | ||
| 554 | |||
| 555 | # This ensures we can use Steam features without having to open the game | ||
| 556 | # through steam. | ||
| 557 | steam_appid_path = os.path.join(os.path.dirname(exe_file), "steam_appid.txt") | ||
| 558 | with open(steam_appid_path, "w") as said_handle: | ||
| 559 | said_handle.write("2523310") | ||
| 560 | |||
| 561 | if Lingo2World.zip_path is not None: | ||
| 562 | # This is a packaged apworld. | ||
| 563 | init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") | ||
| 564 | init_path = Utils.local_path("data", "lingo2_init.tscn") | ||
| 565 | |||
| 566 | with open(init_path, "wb") as file_handle: | ||
| 567 | file_handle.write(init_scene) | ||
| 568 | |||
| 569 | subprocess.Popen( | ||
| 570 | [ | ||
| 571 | exe_file, | ||
| 572 | "--scene", | ||
| 573 | init_path, | ||
| 574 | "--", | ||
| 575 | str(Lingo2World.zip_path.absolute()), | ||
| 576 | ], | ||
| 577 | cwd=os.path.dirname(exe_file), | ||
| 578 | ) | ||
| 579 | else: | ||
| 580 | # The world is unzipped and being run in source. | ||
| 581 | subprocess.Popen( | ||
| 582 | [ | ||
| 583 | exe_file, | ||
| 584 | "--scene", | ||
| 585 | Utils.local_path("worlds", "lingo2", "client", "run_from_source.tscn"), | ||
| 586 | "--", | ||
| 587 | Utils.local_path("worlds", "lingo2", "client"), | ||
| 588 | ], | ||
| 589 | cwd=os.path.dirname(exe_file), | ||
| 590 | ) | ||
| 591 | |||
| 592 | |||
| 593 | def client_main(*launch_args: str) -> None: | ||
| 594 | async def main(args): | ||
| 595 | async_start(run_game()) | ||
| 596 | |||
| 597 | client_ctx = Lingo2ClientContext(args.connect, args.password) | ||
| 598 | game_ctx = Lingo2GameContext() | ||
| 599 | manager = Lingo2Manager(game_ctx, client_ctx) | ||
| 600 | |||
| 601 | client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") | ||
| 602 | |||
| 603 | if gui_enabled: | ||
| 604 | client_ctx.run_gui() | ||
| 605 | client_ctx.run_cli() | ||
| 606 | |||
| 607 | pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher") | ||
| 608 | |||
| 609 | try: | ||
| 610 | await pipe_task | ||
| 611 | except Exception as e: | ||
| 612 | logger.exception(e) | ||
| 613 | |||
| 614 | await client_ctx.exit_event.wait() | ||
| 615 | client_ctx.ui.stop() | ||
| 616 | await client_ctx.shutdown() | ||
| 617 | |||
| 618 | Utils.init_logging("Lingo2Client", exception_logger="Client") | ||
| 619 | import colorama | ||
| 620 | |||
| 621 | parser = get_base_parser(description="Lingo 2 Archipelago Client") | ||
| 622 | parser.add_argument('--name', default=None, help="Slot Name to connect as.") | ||
| 623 | parser.add_argument("url", nargs="?", help="Archipelago connection url") | ||
| 624 | args = parser.parse_args(launch_args) | ||
| 625 | |||
| 626 | args = handle_url_arg(args, parser=parser) | ||
| 627 | |||
| 628 | colorama.just_fix_windows_console() | ||
| 629 | asyncio.run(main(args)) | ||
| 630 | colorama.deinit() | ||
