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.py304
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 @@
1import asyncio
2import os
3import pkgutil
4import subprocess
5from typing import Any
6
7import websockets
8
9import Utils
10import settings
11from CommonClient import CommonContext, server_loop, gui_enabled, logger
12from NetUtils import Endpoint, decode, encode
13from Utils import async_start
14
15PORT = 43182
16MESSAGE_MAX_SIZE = 16*1024*1024
17
18
19class 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
94class 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
198async 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
216async 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
236async 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
274def 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()