diff options
Diffstat (limited to 'apworld/context.py')
| -rw-r--r-- | apworld/context.py | 114 |
1 files changed, 105 insertions, 9 deletions
| diff --git a/apworld/context.py b/apworld/context.py index e2d80cd..86392f9 100644 --- a/apworld/context.py +++ b/apworld/context.py | |||
| @@ -30,6 +30,16 @@ KEY_STORAGE_MAPPING = { | |||
| 30 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} | 30 | REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} |
| 31 | 31 | ||
| 32 | 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. | ||
| 33 | class Lingo2Manager: | 43 | class Lingo2Manager: |
| 34 | game_ctx: "Lingo2GameContext" | 44 | game_ctx: "Lingo2GameContext" |
| 35 | client_ctx: "Lingo2ClientContext" | 45 | client_ctx: "Lingo2ClientContext" |
| @@ -39,6 +49,7 @@ class Lingo2Manager: | |||
| 39 | worldports: set[int] | 49 | worldports: set[int] |
| 40 | goaled: bool | 50 | goaled: bool |
| 41 | latches: set[int] | 51 | latches: set[int] |
| 52 | hinted_locations: set[int] | ||
| 42 | 53 | ||
| 43 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): | 54 | def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): |
| 44 | self.game_ctx = game_ctx | 55 | self.game_ctx = game_ctx |
| @@ -47,8 +58,6 @@ class Lingo2Manager: | |||
| 47 | self.client_ctx.manager = self | 58 | self.client_ctx.manager = self |
| 48 | self.tracker = Tracker(self) | 59 | self.tracker = Tracker(self) |
| 49 | self.keyboard = {} | 60 | self.keyboard = {} |
| 50 | self.worldports = set() | ||
| 51 | self.latches = set() | ||
| 52 | 61 | ||
| 53 | self.reset() | 62 | self.reset() |
| 54 | 63 | ||
| @@ -59,6 +68,7 @@ class Lingo2Manager: | |||
| 59 | self.worldports = set() | 68 | self.worldports = set() |
| 60 | self.goaled = False | 69 | self.goaled = False |
| 61 | self.latches = set() | 70 | self.latches = set() |
| 71 | self.hinted_locations = set() | ||
| 62 | 72 | ||
| 63 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: | 73 | def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: |
| 64 | ret: dict[str, int] = {} | 74 | ret: dict[str, int] = {} |
| @@ -74,6 +84,7 @@ class Lingo2Manager: | |||
| 74 | 84 | ||
| 75 | return ret | 85 | return ret |
| 76 | 86 | ||
| 87 | # Input should be real IDs, not AP IDs | ||
| 77 | def update_worldports(self, new_worldports: set[int]) -> set[int]: | 88 | def update_worldports(self, new_worldports: set[int]) -> set[int]: |
| 78 | ret = new_worldports.difference(self.worldports) | 89 | ret = new_worldports.difference(self.worldports) |
| 79 | self.worldports.update(new_worldports) | 90 | self.worldports.update(new_worldports) |
| @@ -90,6 +101,12 @@ class Lingo2Manager: | |||
| 90 | 101 | ||
| 91 | return ret | 102 | return ret |
| 92 | 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 | |||
| 93 | 110 | ||
| 94 | class Lingo2GameContext: | 111 | class Lingo2GameContext: |
| 95 | server: Endpoint | None | 112 | server: Endpoint | None |
| @@ -227,6 +244,7 @@ class Lingo2GameContext: | |||
| 227 | 244 | ||
| 228 | async_start(self.send_msgs([msg]), name="update keyboard") | 245 | async_start(self.send_msgs([msg]), name="update keyboard") |
| 229 | 246 | ||
| 247 | # Input should be real IDs, not AP IDs | ||
| 230 | def send_update_worldports(self, worldports): | 248 | def send_update_worldports(self, worldports): |
| 231 | if self.server is None: | 249 | if self.server is None: |
| 232 | return | 250 | return |
| @@ -264,6 +282,28 @@ class Lingo2GameContext: | |||
| 264 | 282 | ||
| 265 | async_start(self.send_msgs([msg]), name="update latches") | 283 | async_start(self.send_msgs([msg]), name="update latches") |
| 266 | 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 | |||
| 267 | async def send_msgs(self, msgs: list[Any]) -> None: | 307 | async def send_msgs(self, msgs: list[Any]) -> None: |
| 268 | """ `msgs` JSON serializable """ | 308 | """ `msgs` JSON serializable """ |
| 269 | if not self.server or not self.server.socket.open or self.server.socket.closed: | 309 | if not self.server or not self.server.socket.open or self.server.socket.closed: |
| @@ -278,6 +318,7 @@ class Lingo2ClientContext(CommonContext): | |||
| 278 | items_handling = 0b111 | 318 | items_handling = 0b111 |
| 279 | 319 | ||
| 280 | slot_data: dict[str, Any] | None | 320 | slot_data: dict[str, Any] | None |
| 321 | hints_data_storage_key: str | ||
| 281 | victory_data_storage_key: str | 322 | victory_data_storage_key: str |
| 282 | 323 | ||
| 283 | def __init__(self, server_address: str | None = None, password: str | None = None): | 324 | def __init__(self, server_address: str | None = None, password: str | None = None): |
| @@ -315,10 +356,12 @@ class Lingo2ClientContext(CommonContext): | |||
| 315 | self.manager.tracker.set_checked_locations(self.checked_locations) | 356 | self.manager.tracker.set_checked_locations(self.checked_locations) |
| 316 | self.manager.game_ctx.send_accessible_locations() | 357 | self.manager.game_ctx.send_accessible_locations() |
| 317 | 358 | ||
| 359 | self.hints_data_storage_key = f"_read_hints_{self.team}_{self.slot}" | ||
| 318 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" | 360 | self.victory_data_storage_key = f"_read_client_status_{self.team}_{self.slot}" |
| 319 | 361 | ||
| 320 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), | 362 | self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2"), |
| 321 | self.victory_data_storage_key, self.get_datastorage_key("latches")) | 363 | self.victory_data_storage_key, self.get_datastorage_key("latches"), |
| 364 | self.get_datastorage_key("ignored_locations")) | ||
| 322 | msg_batch = [{ | 365 | msg_batch = [{ |
| 323 | "cmd": "Set", | 366 | "cmd": "Set", |
| 324 | "key": self.get_datastorage_key("keyboard1"), | 367 | "key": self.get_datastorage_key("keyboard1"), |
| @@ -337,6 +380,12 @@ class Lingo2ClientContext(CommonContext): | |||
| 337 | "default": [], | 380 | "default": [], |
| 338 | "want_reply": True, | 381 | "want_reply": True, |
| 339 | "operations": [{"operation": "default", "value": []}] | 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": []}] | ||
| 340 | }] | 389 | }] |
| 341 | 390 | ||
| 342 | if self.slot_data.get("shuffle_worldports", False): | 391 | if self.slot_data.get("shuffle_worldports", False): |
| @@ -435,21 +484,29 @@ class Lingo2ClientContext(CommonContext): | |||
| 435 | for k, v in args["keys"].items(): | 484 | for k, v in args["keys"].items(): |
| 436 | if k == self.victory_data_storage_key: | 485 | if k == self.victory_data_storage_key: |
| 437 | self.handle_status_update(v) | 486 | self.handle_status_update(v) |
| 487 | elif k == self.hints_data_storage_key: | ||
| 488 | self.update_hints() | ||
| 438 | elif cmd == "SetReply": | 489 | elif cmd == "SetReply": |
| 439 | if args["key"] == self.get_datastorage_key("keyboard1"): | 490 | if args["key"] == self.get_datastorage_key("keyboard1"): |
| 440 | self.handle_keyboard_update(1, args) | 491 | self.handle_keyboard_update(1, args) |
| 441 | elif args["key"] == self.get_datastorage_key("keyboard2"): | 492 | elif args["key"] == self.get_datastorage_key("keyboard2"): |
| 442 | self.handle_keyboard_update(2, args) | 493 | self.handle_keyboard_update(2, args) |
| 443 | elif args["key"] == self.get_datastorage_key("worldports"): | 494 | elif args["key"] == self.get_datastorage_key("worldports"): |
| 444 | updates = self.manager.update_worldports(set(args["value"])) | 495 | port_ids = set(Lingo2World.static_logic.port_id_by_ap_id[ap_id] for ap_id in args["value"]) |
| 496 | updates = self.manager.update_worldports(port_ids) | ||
| 445 | if len(updates) > 0: | 497 | if len(updates) > 0: |
| 446 | self.manager.game_ctx.send_update_worldports(updates) | 498 | self.manager.game_ctx.send_update_worldports(updates) |
| 447 | elif args["key"] == self.victory_data_storage_key: | 499 | elif args["key"] == self.victory_data_storage_key: |
| 448 | self.handle_status_update(args["value"]) | 500 | self.handle_status_update(args["value"]) |
| 449 | elif args["key"] == self.get_datastorage_key("latches"): | 501 | elif args["key"] == self.get_datastorage_key("latches"): |
| 450 | updates = self.manager.update_latches(set(args["value"])) | 502 | door_ids = set(Lingo2World.static_logic.door_id_by_ap_id[ap_id] for ap_id in args["value"]) |
| 503 | updates = self.manager.update_latches(door_ids) | ||
| 451 | if len(updates) > 0: | 504 | if len(updates) > 0: |
| 452 | self.manager.game_ctx.send_update_latches(updates) | 505 | self.manager.game_ctx.send_update_latches(updates) |
| 506 | elif args["key"] == self.get_datastorage_key("ignored_locations"): | ||
| 507 | self.manager.game_ctx.send_ignored_locations(args["value"]) | ||
| 508 | elif args["key"] == self.hints_data_storage_key: | ||
| 509 | self.update_hints() | ||
| 453 | 510 | ||
| 454 | def get_datastorage_key(self, name: str): | 511 | def get_datastorage_key(self, name: str): |
| 455 | return f"Lingo2_{self.slot}_{name}" | 512 | return f"Lingo2_{self.slot}_{name}" |
| @@ -515,14 +572,16 @@ class Lingo2ClientContext(CommonContext): | |||
| 515 | if len(updates) > 0: | 572 | if len(updates) > 0: |
| 516 | self.manager.game_ctx.send_update_keyboard(updates) | 573 | self.manager.game_ctx.send_update_keyboard(updates) |
| 517 | 574 | ||
| 575 | # Input should be real IDs, not AP IDs | ||
| 518 | async def update_worldports(self, updates: set[int]): | 576 | async def update_worldports(self, updates: set[int]): |
| 577 | port_ap_ids = [Lingo2World.static_logic.objects.ports[port_id].ap_id for port_id in updates] | ||
| 519 | await self.send_msgs([{ | 578 | await self.send_msgs([{ |
| 520 | "cmd": "Set", | 579 | "cmd": "Set", |
| 521 | "key": self.get_datastorage_key("worldports"), | 580 | "key": self.get_datastorage_key("worldports"), |
| 522 | "want_reply": True, | 581 | "want_reply": True, |
| 523 | "operations": [{ | 582 | "operations": [{ |
| 524 | "operation": "update", | 583 | "operation": "update", |
| 525 | "value": updates | 584 | "value": port_ap_ids |
| 526 | }] | 585 | }] |
| 527 | }]) | 586 | }]) |
| 528 | 587 | ||
| @@ -532,16 +591,47 @@ class Lingo2ClientContext(CommonContext): | |||
| 532 | self.manager.game_ctx.send_accessible_locations() | 591 | self.manager.game_ctx.send_accessible_locations() |
| 533 | 592 | ||
| 534 | async def update_latches(self, updates: set[int]): | 593 | async def update_latches(self, updates: set[int]): |
| 594 | door_ap_ids = [Lingo2World.static_logic.objects.doors[door_id].ap_id for door_id in updates] | ||
| 535 | await self.send_msgs([{ | 595 | await self.send_msgs([{ |
| 536 | "cmd": "Set", | 596 | "cmd": "Set", |
| 537 | "key": self.get_datastorage_key("latches"), | 597 | "key": self.get_datastorage_key("latches"), |
| 538 | "want_reply": True, | 598 | "want_reply": True, |
| 539 | "operations": [{ | 599 | "operations": [{ |
| 540 | "operation": "update", | 600 | "operation": "update", |
| 541 | "value": updates | 601 | "value": door_ap_ids |
| 602 | }] | ||
| 603 | }]) | ||
| 604 | |||
| 605 | async def add_ignored_location(self, loc_id: int): | ||
| 606 | await self.send_msgs([{ | ||
| 607 | "cmd": "Set", | ||
| 608 | "key": self.get_datastorage_key("ignored_locations"), | ||
| 609 | "want_reply": True, | ||
| 610 | "operations": [{ | ||
| 611 | "operation": "update", | ||
| 612 | "value": [loc_id] | ||
| 542 | }] | 613 | }] |
| 543 | }]) | 614 | }]) |
| 544 | 615 | ||
| 616 | async def remove_ignored_location(self, loc_id: int): | ||
| 617 | await self.send_msgs([{ | ||
| 618 | "cmd": "Set", | ||
| 619 | "key": self.get_datastorage_key("ignored_locations"), | ||
| 620 | "want_reply": True, | ||
| 621 | "operations": [{ | ||
| 622 | "operation": "remove", | ||
| 623 | "value": loc_id | ||
| 624 | }] | ||
| 625 | }]) | ||
| 626 | |||
| 627 | def update_hints(self): | ||
| 628 | hints = self.stored_data.get(self.hints_data_storage_key, []) | ||
| 629 | |||
| 630 | hinted_locations = set(hint["location"] for hint in hints if hint["finding_player"] == self.slot) | ||
| 631 | updates = self.manager.update_hinted_locations(hinted_locations) | ||
| 632 | if len(updates) > 0: | ||
| 633 | self.manager.game_ctx.send_update_hinted_locations(updates) | ||
| 634 | |||
| 545 | 635 | ||
| 546 | async def pipe_loop(manager: Lingo2Manager): | 636 | async def pipe_loop(manager: Lingo2Manager): |
| 547 | while not manager.client_ctx.exit_event.is_set(): | 637 | while not manager.client_ctx.exit_event.is_set(): |
| @@ -590,12 +680,14 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): | |||
| 590 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") | 680 | async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") |
| 591 | elif cmd == "CheckWorldport": | 681 | elif cmd == "CheckWorldport": |
| 592 | port_id = args["port_id"] | 682 | port_id = args["port_id"] |
| 683 | port_ap_id = Lingo2World.static_logic.objects.ports[port_id].ap_id | ||
| 593 | worldports = {port_id} | 684 | worldports = {port_id} |
| 594 | 685 | ||
| 595 | # Also check the reverse port if it's a two-way connection. | 686 | # Also check the reverse port if it's a two-way connection. |
| 596 | port_pairings = manager.client_ctx.slot_data["port_pairings"] | 687 | port_pairings = manager.client_ctx.slot_data["port_pairings"] |
| 597 | if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id: | 688 | if str(port_ap_id) in port_pairings and\ |
| 598 | worldports.add(port_pairings[str(port_id)]) | 689 | port_pairings.get(str(port_pairings[str(port_ap_id)]), None) == port_ap_id: |
| 690 | worldports.add(Lingo2World.static_logic.port_id_by_ap_id[port_pairings[str(port_ap_id)]]) | ||
| 599 | 691 | ||
| 600 | updates = manager.update_worldports(worldports) | 692 | updates = manager.update_worldports(worldports) |
| 601 | if len(updates) > 0: | 693 | if len(updates) > 0: |
| @@ -616,6 +708,10 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): | |||
| 616 | updates = manager.update_latches({args["door"]}) | 708 | updates = manager.update_latches({args["door"]}) |
| 617 | if len(updates) > 0: | 709 | if len(updates) > 0: |
| 618 | async_start(manager.client_ctx.update_latches(updates), name="client update latches") | 710 | async_start(manager.client_ctx.update_latches(updates), name="client update latches") |
| 711 | elif cmd == "IgnoreLocation": | ||
| 712 | async_start(manager.client_ctx.add_ignored_location(args["id"]), name="client ignore loc") | ||
| 713 | elif cmd == "UnignoreLocation": | ||
| 714 | async_start(manager.client_ctx.remove_ignored_location(args["id"]), name="client unignore loc") | ||
| 619 | elif cmd == "Quit": | 715 | elif cmd == "Quit": |
| 620 | manager.client_ctx.exit_event.set() | 716 | manager.client_ctx.exit_event.set() |
| 621 | 717 | ||
