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.py388
1 files changed, 276 insertions, 112 deletions
diff --git a/apworld/context.py b/apworld/context.py index 0a058e5..bc3b1bf 100644 --- a/apworld/context.py +++ b/apworld/context.py
@@ -15,35 +15,85 @@ from Utils import async_start
15from . import Lingo2World 15from . import Lingo2World
16from .tracker import Tracker 16from .tracker import Tracker
17 17
18PORT = 43182 18ALL_LETTERS = "abcdefghijklmnopqrstuvwxyz"
19MESSAGE_MAX_SIZE = 16*1024*1024 19MESSAGE_MAX_SIZE = 16*1024*1024
20PORT = 43182
21
22KEY_STORAGE_MAPPING = {
23 "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),
24 "j": (1, 9), "k": (1, 10), "l": (1, 11), "m": (1, 12), "n": (2, 0), "o": (2, 1), "p": (2, 2), "q": (2, 3),
25 "r": (2, 4), "s": (2, 5), "t": (2, 6), "u": (2, 7), "v": (2, 8), "w": (2, 9), "x": (2, 10), "y": (2, 11),
26 "z": (2, 12),
27}
28
29REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()}
30
31
32class Lingo2Manager:
33 game_ctx: "Lingo2GameContext"
34 client_ctx: "Lingo2ClientContext"
35 tracker: Tracker
36
37 keyboard: dict[str, int]
38
39 def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"):
40 self.game_ctx = game_ctx
41 self.game_ctx.manager = self
42 self.client_ctx = client_ctx
43 self.client_ctx.manager = self
44 self.tracker = Tracker(self)
45 self.keyboard = {}
46
47 self.reset()
48
49 def reset(self):
50 for k in ALL_LETTERS:
51 self.keyboard[k] = 0
52
53 def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]:
54 ret: dict[str, int] = {}
55
56 for k, v in new_keyboard.items():
57 if v > self.keyboard.get(k, 0):
58 self.keyboard[k] = v
59 ret[k] = v
60
61 if len(ret) > 0:
62 self.tracker.refresh_state()
63 self.game_ctx.send_accessible_locations()
64
65 return ret
20 66
21 67
22class Lingo2GameContext: 68class Lingo2GameContext:
23 server: Endpoint | None 69 server: Endpoint | None
24 client: "Lingo2ClientContext" 70 manager: Lingo2Manager
25 tracker: Tracker
26 71
27 def __init__(self): 72 def __init__(self):
28 self.server = None 73 self.server = None
29 self.tracker = Tracker()
30 74
31 def send_connected(self): 75 def send_connected(self):
76 if self.server is None:
77 return
78
32 msg = { 79 msg = {
33 "cmd": "Connected", 80 "cmd": "Connected",
34 "user": self.client.username, 81 "user": self.manager.client_ctx.username,
35 "seed_name": self.client.seed_name, 82 "seed_name": self.manager.client_ctx.seed_name,
36 "version": self.client.server_version, 83 "version": self.manager.client_ctx.server_version,
37 "generator_version": self.client.generator_version, 84 "generator_version": self.manager.client_ctx.generator_version,
38 "team": self.client.team, 85 "team": self.manager.client_ctx.team,
39 "slot": self.client.slot, 86 "slot": self.manager.client_ctx.slot,
40 "checked_locations": self.client.checked_locations, 87 "checked_locations": self.manager.client_ctx.checked_locations,
41 "slot_data": self.client.slot_data, 88 "slot_data": self.manager.client_ctx.slot_data,
42 } 89 }
43 90
44 async_start(self.send_msgs([msg]), name="game Connected") 91 async_start(self.send_msgs([msg]), name="game Connected")
45 92
46 def send_item_sent_notification(self, item_name, receiver_name, item_flags): 93 def send_item_sent_notification(self, item_name, receiver_name, item_flags):
94 if self.server is None:
95 return
96
47 msg = { 97 msg = {
48 "cmd": "ItemSentNotif", 98 "cmd": "ItemSentNotif",
49 "item_name": item_name, 99 "item_name": item_name,
@@ -54,6 +104,9 @@ class Lingo2GameContext:
54 async_start(self.send_msgs([msg]), name="item sent notif") 104 async_start(self.send_msgs([msg]), name="item sent notif")
55 105
56 def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self): 106 def send_hint_received(self, item_name, location_name, receiver_name, item_flags, for_self):
107 if self.server is None:
108 return
109
57 msg = { 110 msg = {
58 "cmd": "HintReceived", 111 "cmd": "HintReceived",
59 "item_name": item_name, 112 "item_name": item_name,
@@ -66,6 +119,9 @@ class Lingo2GameContext:
66 async_start(self.send_msgs([msg]), name="hint received notif") 119 async_start(self.send_msgs([msg]), name="hint received notif")
67 120
68 def send_item_received(self, items): 121 def send_item_received(self, items):
122 if self.server is None:
123 return
124
69 msg = { 125 msg = {
70 "cmd": "ItemReceived", 126 "cmd": "ItemReceived",
71 "items": items, 127 "items": items,
@@ -74,6 +130,9 @@ class Lingo2GameContext:
74 async_start(self.send_msgs([msg]), name="item received") 130 async_start(self.send_msgs([msg]), name="item received")
75 131
76 def send_location_info(self, locations): 132 def send_location_info(self, locations):
133 if self.server is None:
134 return
135
77 msg = { 136 msg = {
78 "cmd": "LocationInfo", 137 "cmd": "LocationInfo",
79 "locations": locations, 138 "locations": locations,
@@ -82,6 +141,9 @@ class Lingo2GameContext:
82 async_start(self.send_msgs([msg]), name="location info") 141 async_start(self.send_msgs([msg]), name="location info")
83 142
84 def send_text_message(self, parts): 143 def send_text_message(self, parts):
144 if self.server is None:
145 return
146
85 msg = { 147 msg = {
86 "cmd": "TextMessage", 148 "cmd": "TextMessage",
87 "data": parts, 149 "data": parts,
@@ -90,14 +152,20 @@ class Lingo2GameContext:
90 async_start(self.send_msgs([msg]), name="notif") 152 async_start(self.send_msgs([msg]), name="notif")
91 153
92 def send_accessible_locations(self): 154 def send_accessible_locations(self):
155 if self.server is None:
156 return
157
93 msg = { 158 msg = {
94 "cmd": "AccessibleLocations", 159 "cmd": "AccessibleLocations",
95 "locations": list(self.tracker.accessible_locations), 160 "locations": list(self.manager.tracker.accessible_locations),
96 } 161 }
97 162
98 async_start(self.send_msgs([msg]), name="accessible locations") 163 async_start(self.send_msgs([msg]), name="accessible locations")
99 164
100 def send_update_locations(self, locations): 165 def send_update_locations(self, locations):
166 if self.server is None:
167 return
168
101 msg = { 169 msg = {
102 "cmd": "UpdateLocations", 170 "cmd": "UpdateLocations",
103 "locations": locations, 171 "locations": locations,
@@ -105,6 +173,17 @@ class Lingo2GameContext:
105 173
106 async_start(self.send_msgs([msg]), name="update locations") 174 async_start(self.send_msgs([msg]), name="update locations")
107 175
176 def send_update_keyboard(self, updates):
177 if self.server is None:
178 return
179
180 msg = {
181 "cmd": "UpdateKeyboard",
182 "updates": updates,
183 }
184
185 async_start(self.send_msgs([msg]), name="update keyboard")
186
108 async def send_msgs(self, msgs: list[Any]) -> None: 187 async def send_msgs(self, msgs: list[Any]) -> None:
109 """ `msgs` JSON serializable """ 188 """ `msgs` JSON serializable """
110 if not self.server or not self.server.socket.open or self.server.socket.closed: 189 if not self.server or not self.server.socket.open or self.server.socket.closed:
@@ -113,7 +192,7 @@ class Lingo2GameContext:
113 192
114 193
115class Lingo2ClientContext(CommonContext): 194class Lingo2ClientContext(CommonContext):
116 game_ctx: Lingo2GameContext 195 manager: Lingo2Manager
117 196
118 game = "Lingo 2" 197 game = "Lingo 2"
119 items_handling = 0b111 198 items_handling = 0b111
@@ -138,118 +217,201 @@ class Lingo2ClientContext(CommonContext):
138 elif cmd == "Connected": 217 elif cmd == "Connected":
139 self.slot_data = args.get("slot_data", None) 218 self.slot_data = args.get("slot_data", None)
140 219
141 if self.game_ctx.server is not None: 220 self.manager.reset()
142 self.game_ctx.send_connected() 221
143 222 self.manager.game_ctx.send_connected()
144 self.game_ctx.tracker.setup_slot(self.slot_data) 223
224 self.manager.tracker.setup_slot(self.slot_data)
225 self.manager.tracker.set_checked_locations(self.checked_locations)
226 self.manager.game_ctx.send_accessible_locations()
227
228 self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"))
229 async_start(self.send_msgs([{
230 "cmd": "Set",
231 "key": self.get_datastorage_key("keyboard1"),
232 "default": 0,
233 "want_reply": True,
234 "operations": [{"operation": "default", "value": 0}]
235 }, {
236 "cmd": "Set",
237 "key": self.get_datastorage_key("keyboard2"),
238 "default": 0,
239 "want_reply": True,
240 "operations": [{"operation": "default", "value": 0}]
241 }]), name="default keys")
145 elif cmd == "RoomUpdate": 242 elif cmd == "RoomUpdate":
146 if self.game_ctx.server is not None: 243 self.manager.tracker.set_checked_locations(self.checked_locations)
147 self.game_ctx.send_update_locations(args["checked_locations"]) 244 self.manager.game_ctx.send_update_locations(args["checked_locations"])
148 elif cmd == "ReceivedItems": 245 elif cmd == "ReceivedItems":
149 self.game_ctx.tracker.set_collected_items(self.items_received) 246 self.manager.tracker.set_collected_items(self.items_received)
150 247
151 if self.game_ctx.server is not None: 248 cur_index = 0
152 cur_index = 0 249 items = []
153 items = []
154 250
155 for item in args["items"]: 251 for item in args["items"]:
156 index = cur_index + args["index"] 252 index = cur_index + args["index"]
157 cur_index += 1 253 cur_index += 1
158 254
159 item_msg = { 255 item_msg = {
160 "id": item.item, 256 "id": item.item,
161 "index": index, 257 "index": index,
162 "flags": item.flags, 258 "flags": item.flags,
163 "text": self.item_names.lookup_in_slot(item.item, self.slot), 259 "text": self.item_names.lookup_in_slot(item.item, self.slot),
164 } 260 }
165 261
166 if item.player != self.slot: 262 if item.player != self.slot:
167 item_msg["sender"] = self.player_names.get(item.player) 263 item_msg["sender"] = self.player_names.get(item.player)
168 264
169 items.append(item_msg) 265 items.append(item_msg)
170 266
171 self.game_ctx.send_item_received(items) 267 self.manager.game_ctx.send_item_received(items)
172 268
173 if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): 269 if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]):
174 self.game_ctx.send_accessible_locations() 270 self.manager.game_ctx.send_accessible_locations()
175 elif cmd == "PrintJSON": 271 elif cmd == "PrintJSON":
176 if self.game_ctx.server is not None: 272 if "receiving" in args and "item" in args and args["item"].player == self.slot:
177 if "receiving" in args and "item" in args and args["item"].player == self.slot: 273 item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"])
178 item_name = self.item_names.lookup_in_slot(args["item"].item, args["receiving"]) 274 location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player)
179 location_name = self.location_names.lookup_in_slot(args["item"].location, args["item"].player) 275 receiver_name = self.player_names.get(args["receiving"])
180 receiver_name = self.player_names.get(args["receiving"]) 276
181 277 if args["type"] == "Hint" and not args.get("found", False):
182 if args["type"] == "Hint" and not args.get("found", False): 278 self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags,
183 self.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, 279 int(args["receiving"]) == self.slot)
184 int(args["receiving"]) == self.slot) 280 elif args["receiving"] != self.slot:
185 elif args["receiving"] != self.slot: 281 self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags)
186 self.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) 282
187 283 parts = []
188 parts = [] 284 for message_part in args["data"]:
189 for message_part in args["data"]: 285 if "type" not in message_part and "text" in message_part:
190 if "type" not in message_part and "text" in message_part: 286 parts.append({"type": "text", "text": message_part["text"]})
191 parts.append({"type": "text", "text": message_part["text"]}) 287 elif message_part["type"] == "player_id":
192 elif message_part["type"] == "player_id": 288 parts.append({
193 parts.append({ 289 "type": "player",
194 "type": "player", 290 "text": self.player_names.get(int(message_part["text"])),
195 "text": self.player_names.get(int(message_part["text"])), 291 "self": int(int(message_part["text"]) == self.slot),
196 "self": int(int(message_part["text"]) == self.slot),
197 })
198 elif message_part["type"] == "item_id":
199 parts.append({
200 "type": "item",
201 "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]),
202 "flags": message_part["flags"],
203 })
204 elif message_part["type"] == "location_id":
205 parts.append({
206 "type": "location",
207 "text": self.location_names.lookup_in_slot(int(message_part["text"]),
208 message_part["player"])
209 })
210 elif "text" in message_part:
211 parts.append({"type": "text", "text": message_part["text"]})
212
213 self.game_ctx.send_text_message(parts)
214 elif cmd == "LocationInfo":
215 if self.game_ctx.server is not None:
216 locations = []
217
218 for location in args["locations"]:
219 locations.append({
220 "id": location.location,
221 "item": self.item_names.lookup_in_slot(location.item, location.player),
222 "player": self.player_names.get(location.player),
223 "flags": location.flags,
224 "self": int(location.player) == self.slot,
225 }) 292 })
293 elif message_part["type"] == "item_id":
294 parts.append({
295 "type": "item",
296 "text": self.item_names.lookup_in_slot(int(message_part["text"]), message_part["player"]),
297 "flags": message_part["flags"],
298 })
299 elif message_part["type"] == "location_id":
300 parts.append({
301 "type": "location",
302 "text": self.location_names.lookup_in_slot(int(message_part["text"]),
303 message_part["player"])
304 })
305 elif "text" in message_part:
306 parts.append({"type": "text", "text": message_part["text"]})
226 307
227 self.game_ctx.send_location_info(locations) 308 self.manager.game_ctx.send_text_message(parts)
228 309 elif cmd == "LocationInfo":
229 if cmd in ["Connected", "RoomUpdate"]: 310 locations = []
230 self.game_ctx.tracker.set_checked_locations(self.checked_locations) 311
231 312 for location in args["locations"]:
232 313 locations.append({
233async def pipe_loop(ctx: Lingo2GameContext): 314 "id": location.location,
234 while not ctx.client.exit_event.is_set(): 315 "item": self.item_names.lookup_in_slot(location.item, location.player),
316 "player": self.player_names.get(location.player),
317 "flags": location.flags,
318 "self": int(location.player) == self.slot,
319 })
320
321 self.manager.game_ctx.send_location_info(locations)
322 elif cmd == "SetReply":
323 if args["key"] == self.get_datastorage_key("keyboard1"):
324 self.handle_keyboard_update(1, args)
325 elif args["key"] == self.get_datastorage_key("keyboard2"):
326 self.handle_keyboard_update(2, args)
327
328 def get_datastorage_key(self, name: str):
329 return f"Lingo2_{self.slot}_{name}"
330
331 async def update_keyboard(self, updates: dict[str, int]):
332 kb1 = 0
333 kb2 = 0
334
335 for k, v in updates.items():
336 if v == 0:
337 continue
338
339 effect = 0
340 if v >= 1:
341 effect |= 1
342 if v == 2:
343 effect |= 2
344
345 pos = KEY_STORAGE_MAPPING[k]
346 if pos[0] == 1:
347 kb1 |= (effect << pos[1] * 2)
348 else:
349 kb2 |= (effect << pos[1] * 2)
350
351 msgs = []
352
353 if kb1 != 0:
354 msgs.append({
355 "cmd": "Set",
356 "key": self.get_datastorage_key("keyboard1"),
357 "want_reply": True,
358 "operations": [{
359 "operation": "or",
360 "value": kb1
361 }]
362 })
363
364 if kb2 != 0:
365 msgs.append({
366 "cmd": "Set",
367 "key": self.get_datastorage_key("keyboard2"),
368 "want_reply": True,
369 "operations": [{
370 "operation": "or",
371 "value": kb2
372 }]
373 })
374
375 if len(msgs) > 0:
376 print(updates)
377 print(msgs)
378 await self.send_msgs(msgs)
379
380 def handle_keyboard_update(self, field: int, args: dict[str, Any]):
381 keys = {}
382 value = args["value"]
383
384 for i in range(0, 13):
385 if (value & (1 << (i * 2))) != 0:
386 keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 1
387 if (value & (1 << (i * 2 + 1))) != 0:
388 keys[REVERSE_KEY_STORAGE_MAPPING[(field, i)]] = 2
389
390 updates = self.manager.update_keyboard(keys)
391 if len(updates) > 0:
392 self.manager.game_ctx.send_update_keyboard(updates)
393
394
395async def pipe_loop(manager: Lingo2Manager):
396 while not manager.client_ctx.exit_event.is_set():
235 try: 397 try:
236 socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None, 398 socket = await websockets.connect("ws://localhost", port=PORT, ping_timeout=None, ping_interval=None,
237 max_size=MESSAGE_MAX_SIZE) 399 max_size=MESSAGE_MAX_SIZE)
238 ctx.server = Endpoint(socket) 400 manager.game_ctx.server = Endpoint(socket)
239 logger.info("Connected to Lingo 2!") 401 logger.info("Connected to Lingo 2!")
240 if ctx.client.auth is not None: 402 if manager.client_ctx.auth is not None:
241 ctx.send_connected() 403 manager.game_ctx.send_connected()
242 ctx.send_accessible_locations() 404 manager.game_ctx.send_accessible_locations()
243 async for data in ctx.server.socket: 405 async for data in manager.game_ctx.server.socket:
244 for msg in decode(data): 406 for msg in decode(data):
245 await process_game_cmd(ctx, msg) 407 await process_game_cmd(manager, msg)
246 except ConnectionRefusedError: 408 except ConnectionRefusedError:
247 logger.info("Could not connect to Lingo 2.") 409 logger.info("Could not connect to Lingo 2.")
248 finally: 410 finally:
249 ctx.server = None 411 manager.game_ctx.server = None
250 412
251 413
252async def process_game_cmd(ctx: Lingo2GameContext, args: dict): 414async def process_game_cmd(manager: Lingo2Manager, args: dict):
253 cmd = args["cmd"] 415 cmd = args["cmd"]
254 416
255 if cmd == "Connect": 417 if cmd == "Connect":
@@ -262,13 +424,17 @@ async def process_game_cmd(ctx: Lingo2GameContext, args: dict):
262 else: 424 else:
263 server_address = f"{player}:None@{server}" 425 server_address = f"{player}:None@{server}"
264 426
265 async_start(ctx.client.connect(server_address), name="client connect") 427 async_start(manager.client_ctx.connect(server_address), name="client connect")
266 elif cmd == "Disconnect": 428 elif cmd == "Disconnect":
267 async_start(ctx.client.disconnect(), name="client disconnect") 429 async_start(manager.client_ctx.disconnect(), name="client disconnect")
268 elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]: 430 elif cmd in ["Sync", "LocationChecks", "Say", "StatusUpdate", "LocationScouts"]:
269 async_start(ctx.client.send_msgs([args]), name="client forward") 431 async_start(manager.client_ctx.send_msgs([args]), name="client forward")
432 elif cmd == "UpdateKeyboard":
433 updates = manager.update_keyboard(args["keyboard"])
434 if len(updates) > 0:
435 async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard")
270 elif cmd == "Quit": 436 elif cmd == "Quit":
271 ctx.client.exit_event.set() 437 manager.client_ctx.exit_event.set()
272 438
273 439
274async def run_game(): 440async def run_game():
@@ -318,9 +484,7 @@ def client_main(*launch_args: str) -> None:
318 484
319 client_ctx = Lingo2ClientContext(args.connect, args.password) 485 client_ctx = Lingo2ClientContext(args.connect, args.password)
320 game_ctx = Lingo2GameContext() 486 game_ctx = Lingo2GameContext()
321 487 manager = Lingo2Manager(game_ctx, client_ctx)
322 client_ctx.game_ctx = game_ctx
323 game_ctx.client = client_ctx
324 488
325 client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop") 489 client_ctx.server_task = asyncio.create_task(server_loop(client_ctx), name="ServerLoop")
326 490
@@ -328,7 +492,7 @@ def client_main(*launch_args: str) -> None:
328 client_ctx.run_gui() 492 client_ctx.run_gui()
329 client_ctx.run_cli() 493 client_ctx.run_cli()
330 494
331 pipe_task = asyncio.create_task(pipe_loop(game_ctx), name="GameWatcher") 495 pipe_task = asyncio.create_task(pipe_loop(manager), name="GameWatcher")
332 496
333 try: 497 try:
334 await pipe_task 498 await pipe_task