diff options
| author | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-25 18:26:53 -0400 |
|---|---|---|
| committer | Star Rauchenberger <fefferburbia@gmail.com> | 2025-09-25 18:26:53 -0400 |
| commit | 05827d25733698a26cc0f305966e6a8a03be4684 (patch) | |
| tree | 75dbec594a0bd0c2494df31b712bb6435730197c /apworld/context.py | |
| parent | cb2eca4fed1eb3692eaa13715f65ebcaf8472b64 (diff) | |
| download | lingo2-archipelago-05827d25733698a26cc0f305966e6a8a03be4684.tar.gz lingo2-archipelago-05827d25733698a26cc0f305966e6a8a03be4684.tar.bz2 lingo2-archipelago-05827d25733698a26cc0f305966e6a8a03be4684.zip | |
Game talks through CommonClient now
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() | ||
