From f3b490b10aeac32ba859b929ff13ff882d818a17 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 1 Nov 2025 14:32:04 -0400 Subject: Store stable IDs in multiworld state --- apworld/__init__.py | 11 +++++++++-- apworld/client/gamedata.gd | 4 ++++ apworld/client/manager.gd | 4 +++- apworld/context.py | 31 +++++++++++++++++++++++++------ apworld/static_logic.py | 6 ++++++ apworld/tracker.py | 5 ++++- 6 files changed, 51 insertions(+), 10 deletions(-) (limited to 'apworld') diff --git a/apworld/__init__.py b/apworld/__init__.py index f5774c6..3d2f075 100644 --- a/apworld/__init__.py +++ b/apworld/__init__.py @@ -77,7 +77,10 @@ class Lingo2World(World): if self.options.shuffle_worldports: if hasattr(self.multiworld, "re_gen_passthrough") and "Lingo 2" in self.multiworld.re_gen_passthrough: slot_value = self.multiworld.re_gen_passthrough["Lingo 2"]["port_pairings"] - self.port_pairings = {int(fp): int(tp) for fp, tp in slot_value.items()} + self.port_pairings = { + self.static_logic.port_id_by_ap_id[int(fp)]: self.static_logic.port_id_by_ap_id[int(tp)] + for fp, tp in slot_value.items() + } connect_ports_from_ut(self.port_pairings, self) else: @@ -152,7 +155,11 @@ class Lingo2World(World): } if self.options.shuffle_worldports: - slot_data["port_pairings"] = self.port_pairings + def get_port_ap_id(port_id): + return self.static_logic.objects.ports[port_id].ap_id + + slot_data["port_pairings"] = {get_port_ap_id(from_id): get_port_ap_id(to_id) + for from_id, to_id in self.port_pairings.items()} return slot_data diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 3a35125..d7e3136 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd @@ -15,6 +15,7 @@ var symbol_item_ids = [] var anti_trap_ids = {} var location_name_by_id = {} var ending_display_name_by_name = {} +var port_id_by_ap_id = {} var kSYMBOL_ITEMS @@ -99,6 +100,9 @@ func load(data_bytes): var map_data = port_id_by_map_node_path[map.get_name()] map_data[port.get_path()] = port.get_id() + if port.has_ap_id(): + port_id_by_ap_id[port.get_ap_id()] = port.get_id() + for progressive in objects.get_progressives(): progressive_id_by_ap_id[progressive.get_ap_id()] = progressive.get_id() diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index aa07559..727d17a 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -472,7 +472,9 @@ func _client_connected(slot_data): var raw_pp = slot_data.get("port_pairings") for p1 in raw_pp.keys(): - port_pairings[int(p1)] = int(raw_pp[p1]) + port_pairings[gamedata.port_id_by_ap_id[int(p1)]] = gamedata.port_id_by_ap_id[int( + raw_pp[p1] + )] # Set up item locks. _item_locks = {} diff --git a/apworld/context.py b/apworld/context.py index e2d80cd..52b04ae 100644 --- a/apworld/context.py +++ b/apworld/context.py @@ -30,6 +30,16 @@ KEY_STORAGE_MAPPING = { REVERSE_KEY_STORAGE_MAPPING = {t: k for k, t in KEY_STORAGE_MAPPING.items()} +# There is a distinction between an object's ID and its AP ID. The latter is stable between releases, whereas the former +# can change and is also namespaced based on the object type. We should only store AP IDs in multiworld state (such as +# slot data and data storage) to increase compatability between releases. The data we currently store is: +# - Port pairings for worldport shuffle (slot data) +# - Checked worldports for worldport shuffle (data storage) +# - Latched doors (data storage) +# The client generally deals in the actual object IDs rather than the stable IDs, although it does have to convert the +# port pairing IDs when reading them from slot data. The context (this file here) does the work of converting back and +# forth between the values. AP IDs are converted to IDs after reading them from data storage, and IDs are converted to +# AP IDs before sending them to data storage. class Lingo2Manager: game_ctx: "Lingo2GameContext" client_ctx: "Lingo2ClientContext" @@ -74,6 +84,7 @@ class Lingo2Manager: return ret + # Input should be real IDs, not AP IDs def update_worldports(self, new_worldports: set[int]) -> set[int]: ret = new_worldports.difference(self.worldports) self.worldports.update(new_worldports) @@ -227,6 +238,7 @@ class Lingo2GameContext: async_start(self.send_msgs([msg]), name="update keyboard") + # Input should be real IDs, not AP IDs def send_update_worldports(self, worldports): if self.server is None: return @@ -441,13 +453,15 @@ class Lingo2ClientContext(CommonContext): elif args["key"] == self.get_datastorage_key("keyboard2"): self.handle_keyboard_update(2, args) elif args["key"] == self.get_datastorage_key("worldports"): - updates = self.manager.update_worldports(set(args["value"])) + port_ids = set(Lingo2World.static_logic.port_id_by_ap_id[ap_id] for ap_id in args["value"]) + updates = self.manager.update_worldports(port_ids) if len(updates) > 0: self.manager.game_ctx.send_update_worldports(updates) elif args["key"] == self.victory_data_storage_key: self.handle_status_update(args["value"]) elif args["key"] == self.get_datastorage_key("latches"): - updates = self.manager.update_latches(set(args["value"])) + door_ids = set(Lingo2World.static_logic.door_id_by_ap_id[ap_id] for ap_id in args["value"]) + updates = self.manager.update_latches(door_ids) if len(updates) > 0: self.manager.game_ctx.send_update_latches(updates) @@ -515,14 +529,16 @@ class Lingo2ClientContext(CommonContext): if len(updates) > 0: self.manager.game_ctx.send_update_keyboard(updates) + # Input should be real IDs, not AP IDs async def update_worldports(self, updates: set[int]): + port_ap_ids = [Lingo2World.static_logic.objects.ports[port_id].ap_id for port_id in updates] await self.send_msgs([{ "cmd": "Set", "key": self.get_datastorage_key("worldports"), "want_reply": True, "operations": [{ "operation": "update", - "value": updates + "value": port_ap_ids }] }]) @@ -532,13 +548,14 @@ class Lingo2ClientContext(CommonContext): self.manager.game_ctx.send_accessible_locations() async def update_latches(self, updates: set[int]): + door_ap_ids = [Lingo2World.static_logic.objects.doors[door_id].ap_id for door_id in updates] await self.send_msgs([{ "cmd": "Set", "key": self.get_datastorage_key("latches"), "want_reply": True, "operations": [{ "operation": "update", - "value": updates + "value": door_ap_ids }] }]) @@ -590,12 +607,14 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") elif cmd == "CheckWorldport": port_id = args["port_id"] + port_ap_id = Lingo2World.static_logic.objects.ports[port_id].ap_id worldports = {port_id} # Also check the reverse port if it's a two-way connection. port_pairings = manager.client_ctx.slot_data["port_pairings"] - if str(port_id) in port_pairings and port_pairings.get(str(port_pairings[str(port_id)]), None) == port_id: - worldports.add(port_pairings[str(port_id)]) + if str(port_ap_id) in port_pairings and\ + port_pairings.get(str(port_pairings[str(port_ap_id)]), None) == port_ap_id: + worldports.add(Lingo2World.static_logic.port_id_by_ap_id[port_pairings[str(port_ap_id)]]) updates = manager.update_worldports(worldports) if len(updates) > 0: diff --git a/apworld/static_logic.py b/apworld/static_logic.py index 8e07b82..8a84111 100644 --- a/apworld/static_logic.py +++ b/apworld/static_logic.py @@ -15,6 +15,9 @@ class Lingo2StaticLogic: letter_weights: dict[str, int] + door_id_by_ap_id: dict[int, int] + port_id_by_ap_id: dict[int, int] + def __init__(self): self.item_id_to_name = {} self.location_id_to_name = {} @@ -83,6 +86,9 @@ class Lingo2StaticLogic: for letter in panel.answer.upper(): self.letter_weights[letter] = self.letter_weights.get(letter, 0) + 1 + self.door_id_by_ap_id = {door.ap_id: door.id for door in self.objects.doors if door.HasField("ap_id")} + self.port_id_by_ap_id = {port.ap_id: port.id for port in self.objects.ports if port.HasField("ap_id")} + def get_door_item_name(self, door: data_pb2.Door) -> str: return f"{self.get_map_object_map_name(door)} - {door.name}" diff --git a/apworld/tracker.py b/apworld/tracker.py index c65317c..d473af4 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py @@ -47,7 +47,10 @@ class Tracker: self.world.create_regions() if self.world.options.shuffle_worldports: - port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} + port_pairings = { + self.world.static_logic.port_id_by_ap_id[int(fp)]: self.world.static_logic.port_id_by_ap_id[int(tp)] + for fp, tp in slot_data["port_pairings"].items() + } connect_ports_from_ut(port_pairings, self.world) self.refresh_state() -- cgit 1.4.1