From 53e0509fcb20cc824e3fe5b3d3a826f09fc9c166 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 27 Sep 2025 21:06:32 -0400 Subject: Treat worldports as items for tracker --- apworld/client/client.gd | 23 ++++++++++++++ apworld/client/gamedata.gd | 5 +++ apworld/client/manager.gd | 8 +++++ apworld/client/textclient.gd | 5 +++ apworld/client/worldport.gd | 10 +++++- apworld/context.py | 72 +++++++++++++++++++++++++++++++++++++++++--- apworld/locations.py | 2 ++ apworld/regions.py | 29 +++++++++++++++--- apworld/tracker.py | 15 +++++++-- 9 files changed, 157 insertions(+), 12 deletions(-) diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 3d4096f..a23e85a 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd @@ -18,10 +18,12 @@ var _seed = "" var _team = 0 var _slot = 0 var _checked_locations = [] +var _checked_worldports = [] var _received_indexes = [] var _received_items = {} var _slot_data = {} var _accessible_locations = [] +var _accessible_worldports = [] signal could_not_connect signal connect_status @@ -33,6 +35,7 @@ signal item_sent_notification(message) signal hint_received(message) signal accessible_locations_updated signal checked_locations_updated +signal checked_worldports_updated signal keyboard_update_received @@ -55,7 +58,9 @@ func _reset_state(): _should_process = false _received_items = {} _received_indexes = [] + _checked_worldports = [] _accessible_locations = [] + _accessible_worldports = [] func disconnect_from_ap(): @@ -117,6 +122,14 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo checked_locations_updated.emit() + elif cmd == "UpdateWorldports": + for port_id in message["worldports"]: + var lint = int(port_id) + if not _checked_worldports.has(lint): + _checked_worldports.append(lint) + + checked_worldports_updated.emit() + elif cmd == "ItemReceived": for item in message["items"]: var index = int(item["index"]) @@ -152,10 +165,15 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo elif cmd == "AccessibleLocations": _accessible_locations.clear() + _accessible_worldports.clear() for loc in message["locations"]: _accessible_locations.append(int(loc)) + if "worldports" in message: + for port_id in message["worldports"]: + _accessible_worldports.append(int(port_id)) + accessible_locations_updated.emit() elif cmd == "UpdateKeyboard": @@ -226,6 +244,11 @@ func updateKeyboard(updates): sendMessage([{"cmd": "UpdateKeyboard", "keyboard": updates}]) +func checkWorldport(port_id): + if not _checked_worldports.has(port_id): + sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) + + func sendQuit(): sendMessage([{"cmd": "Quit"}]) diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 13ec568..e44fa17 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd @@ -173,6 +173,11 @@ func get_door_receivers(door_id): return door.get_receivers() +func get_worldport_display_name(port_id): + var port = objects.get_ports()[port_id] + return "%s - %s (Worldport)" % [_get_room_object_map_name(port), port.get_name()] + + func _get_map_object_map_name(obj): return objects.get_maps()[obj.get_map_id()].get_display_name() diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index afa3ebe..3facfba 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -109,6 +109,7 @@ func _ready(): client.hint_received.connect(_process_hint_received) client.accessible_locations_updated.connect(_on_accessible_locations_updated) client.checked_locations_updated.connect(_on_checked_locations_updated) + client.checked_worldports_updated.connect(_on_checked_worldports_updated) client.could_not_connect.connect(_client_could_not_connect) client.connect_status.connect(_client_connect_status) @@ -195,6 +196,7 @@ func _process_item(item, amount): if gamedata.get_door_map_name(lock[0]) != global.map: continue + # TODO: fix doors opening from door groups var receivers = gamedata.get_door_receivers(lock[0]) var scene = get_tree().get_root().get_node_or_null("scene") if scene != null: @@ -327,6 +329,12 @@ func _on_checked_locations_updated(): textclient_node.update_locations() +func _on_checked_worldports_updated(): + var textclient_node = global.get_node("Textclient") + if textclient_node != null: + textclient_node.update_locations() + + func _client_could_not_connect(message): could_not_connect.emit(message) diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 1b36c29..af155fb 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd @@ -150,6 +150,11 @@ func update_locations(): var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") location_names.append(location_name) + for port_id in ap.client._accessible_worldports: + if not ap.client._checked_worldports.has(port_id): + var port_name = gamedata.get_worldport_display_name(port_id) + location_names.append(port_name) + location_names.sort() var count = 0 diff --git a/apworld/client/worldport.gd b/apworld/client/worldport.gd index cdca248..ed9891e 100644 --- a/apworld/client/worldport.gd +++ b/apworld/client/worldport.gd @@ -3,6 +3,8 @@ extends "res://scripts/nodes/worldport.gd" var absolute_rotation = false var target_rotation = 0 +var port_id = null + func _ready(): var node_path = String( @@ -13,7 +15,7 @@ func _ready(): if ap.shuffle_worldports: var gamedata = global.get_node("Gamedata") - var port_id = gamedata.get_port_for_map_node_path(global.map, node_path) + port_id = gamedata.get_port_for_map_node_path(global.map, node_path) if port_id != null: if port_id in ap.port_pairings: var target_port = gamedata.objects.get_ports()[ap.port_pairings[port_id]] @@ -29,6 +31,8 @@ func _ready(): sets_entry_point = true invisible = false fades = true + else: + port_id = null if global.map == "icarus" and exit == "daedalus": if not ap.daedalus_roof_access: @@ -39,6 +43,10 @@ func _ready(): func bodyEntered(body): if body.is_in_group("player"): + if port_id != null: + var ap = global.get_node("Archipelago") + ap.client.checkWorldport(port_id) + if absolute_rotation: entry_rotate.y = target_rotation - body.rotation_degrees.y diff --git a/apworld/context.py b/apworld/context.py index bc3b1bf..4a85868 100644 --- a/apworld/context.py +++ b/apworld/context.py @@ -35,6 +35,7 @@ class Lingo2Manager: tracker: Tracker keyboard: dict[str, int] + worldports: set[int] def __init__(self, game_ctx: "Lingo2GameContext", client_ctx: "Lingo2ClientContext"): self.game_ctx = game_ctx @@ -43,6 +44,7 @@ class Lingo2Manager: self.client_ctx.manager = self self.tracker = Tracker(self) self.keyboard = {} + self.worldports = set() self.reset() @@ -50,6 +52,8 @@ class Lingo2Manager: for k in ALL_LETTERS: self.keyboard[k] = 0 + self.worldports = set() + def update_keyboard(self, new_keyboard: dict[str, int]) -> dict[str, int]: ret: dict[str, int] = {} @@ -64,6 +68,16 @@ class Lingo2Manager: return ret + def update_worldports(self, new_worldports: set[int]) -> set[int]: + ret = new_worldports.difference(self.worldports) + self.worldports.update(new_worldports) + + if len(ret) > 0: + self.tracker.refresh_state() + self.game_ctx.send_accessible_locations() + + return ret + class Lingo2GameContext: server: Endpoint | None @@ -160,6 +174,9 @@ class Lingo2GameContext: "locations": list(self.manager.tracker.accessible_locations), } + if len(self.manager.tracker.accessible_worldports) > 0: + msg["worldports"] = list(self.manager.tracker.accessible_worldports) + async_start(self.send_msgs([msg]), name="accessible locations") def send_update_locations(self, locations): @@ -184,6 +201,17 @@ class Lingo2GameContext: async_start(self.send_msgs([msg]), name="update keyboard") + def send_update_worldports(self, worldports): + if self.server is None: + return + + msg = { + "cmd": "UpdateWorldports", + "worldports": worldports, + } + + async_start(self.send_msgs([msg]), name="update worldports") + async def send_msgs(self, msgs: list[Any]) -> None: """ `msgs` JSON serializable """ if not self.server or not self.server.socket.open or self.server.socket.closed: @@ -226,7 +254,7 @@ class Lingo2ClientContext(CommonContext): self.manager.game_ctx.send_accessible_locations() self.set_notify(self.get_datastorage_key("keyboard1"), self.get_datastorage_key("keyboard2")) - async_start(self.send_msgs([{ + msg_batch = [{ "cmd": "Set", "key": self.get_datastorage_key("keyboard1"), "default": 0, @@ -238,7 +266,19 @@ class Lingo2ClientContext(CommonContext): "default": 0, "want_reply": True, "operations": [{"operation": "default", "value": 0}] - }]), name="default keys") + }] + + if self.slot_data["shuffle_worldports"]: + self.set_notify(self.get_datastorage_key("worldports")) + msg_batch.append({ + "cmd": "Set", + "key": self.get_datastorage_key("worldports"), + "default": [], + "want_reply": True, + "operations": [{"operation": "default", "value": []}] + }) + + async_start(self.send_msgs(msg_batch), name="default keys") elif cmd == "RoomUpdate": self.manager.tracker.set_checked_locations(self.checked_locations) self.manager.game_ctx.send_update_locations(args["checked_locations"]) @@ -276,7 +316,7 @@ class Lingo2ClientContext(CommonContext): if args["type"] == "Hint" and not args.get("found", False): self.manager.game_ctx.send_hint_received(item_name, location_name, receiver_name, args["item"].flags, - int(args["receiving"]) == self.slot) + int(args["receiving"]) == self.slot) elif args["receiving"] != self.slot: self.manager.game_ctx.send_item_sent_notification(item_name, receiver_name, args["item"].flags) @@ -324,6 +364,10 @@ class Lingo2ClientContext(CommonContext): self.handle_keyboard_update(1, args) 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"])) + if len(updates) > 0: + self.manager.game_ctx.send_update_worldports(updates) def get_datastorage_key(self, name: str): return f"Lingo2_{self.slot}_{name}" @@ -373,8 +417,6 @@ class Lingo2ClientContext(CommonContext): }) if len(msgs) > 0: - print(updates) - print(msgs) await self.send_msgs(msgs) def handle_keyboard_update(self, field: int, args: dict[str, Any]): @@ -391,6 +433,17 @@ class Lingo2ClientContext(CommonContext): if len(updates) > 0: self.manager.game_ctx.send_update_keyboard(updates) + async def update_worldports(self, updates: set[int]): + await self.send_msgs([{ + "cmd": "Set", + "key": self.get_datastorage_key("worldports"), + "want_reply": True, + "operations": [{ + "operation": "update", + "value": updates + }] + }]) + async def pipe_loop(manager: Lingo2Manager): while not manager.client_ctx.exit_event.is_set(): @@ -433,6 +486,15 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): updates = manager.update_keyboard(args["keyboard"]) if len(updates) > 0: async_start(manager.client_ctx.update_keyboard(updates), name="client update keyboard") + elif cmd == "CheckWorldport": + port_id = args["port_id"] + worldports = {port_id} + if str(port_id) in manager.client_ctx.slot_data["port_pairings"]: + worldports.add(manager.client_ctx.slot_data["port_pairings"][str(port_id)]) + + updates = manager.update_worldports(worldports) + if len(updates) > 0: + async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") elif cmd == "Quit": manager.client_ctx.exit_event.set() diff --git a/apworld/locations.py b/apworld/locations.py index 108decb..a502931 100644 --- a/apworld/locations.py +++ b/apworld/locations.py @@ -3,3 +3,5 @@ from BaseClasses import Location class Lingo2Location(Location): game: str = "Lingo 2" + + port_id: int diff --git a/apworld/regions.py b/apworld/regions.py index a7d9a1c..9f44682 100644 --- a/apworld/regions.py +++ b/apworld/regions.py @@ -32,6 +32,22 @@ def create_locations(room, new_region: Region, world: "Lingo2World", regions: di new_location.place_locked_item(event_item) new_region.locations.append(new_location) + if world.for_tracker and world.options.shuffle_worldports: + for port_id in room.ports: + port = world.static_logic.objects.ports[port_id] + if port.no_shuffle: + continue + + new_location = Lingo2Location(world.player, f"Worldport {port.id} Entered", None, new_region) + new_location.port_id = port.id + + if port.HasField("required_door"): + new_location.access_rule = \ + make_location_lambda(world.player_logic.get_door_open_reqs(port.required_door), world, regions) + + new_region.locations.append(new_location) + + def create_regions(world: "Lingo2World"): regions = { "Menu": Region("Menu", world.player, world.multiworld) @@ -52,7 +68,6 @@ def create_regions(world: "Lingo2World"): regions["Menu"].connect(regions["The Entry - Starting Room"], "Start Game") - # TODO: The requirements of the opposite trigger also matter. for connection in world.static_logic.objects.connections: if connection.roof_access and not world.options.daedalus_roof_access: continue @@ -176,11 +191,17 @@ def connect_ports_from_ut(port_pairings: dict[int, int], world: "Lingo2World"): connection = Entrance(world.player, f"{from_region_name} - {from_port.name}", from_region) + reqs = AccessRequirements() if from_port.HasField("required_door"): - door_reqs = world.player_logic.get_door_open_reqs(from_port.required_door) - connection.access_rule = make_location_lambda(door_reqs, world, None) + reqs = world.player_logic.get_door_open_reqs(from_port.required_door).copy() - for region in door_reqs.get_referenced_rooms(): + if world.for_tracker: + reqs.items.add(f"Worldport {fpid} Entered") + + if not reqs.is_empty(): + connection.access_rule = make_location_lambda(reqs, world, None) + + for region in reqs.get_referenced_rooms(): world.multiworld.register_indirect_condition(world.multiworld.get_region(region, world.player), connection) diff --git a/apworld/tracker.py b/apworld/tracker.py index 2c3d0f3..cf2dbe1 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py @@ -21,6 +21,7 @@ class Tracker: collected_items: dict[int, int] checked_locations: set[int] accessible_locations: set[int] + accessible_worldports: set[int] state: CollectionState @@ -29,6 +30,7 @@ class Tracker: self.collected_items = {} self.checked_locations = set() self.accessible_locations = set() + self.accessible_worldports = set() def setup_slot(self, slot_data): Lingo2World.for_tracker = True @@ -84,12 +86,21 @@ class Tracker: self.state.collect(Lingo2Item(k.upper(), ItemClassification.progression, None, PLAYER_NUM), prevent_sweep=True) + for port_id in self.manager.worldports: + self.state.collect(Lingo2Item(f"Worldport {port_id} Entered", ItemClassification.progression, None, + PLAYER_NUM), prevent_sweep=True) + self.state.sweep_for_advancements() self.accessible_locations = set() + self.accessible_worldports = set() for region in self.state.reachable_regions[PLAYER_NUM]: for location in region.locations: - if location.address not in self.checked_locations and location.access_rule(self.state): + if location.access_rule(self.state): if location.address is not None: - self.accessible_locations.add(location.address) + if location.address not in self.checked_locations: + self.accessible_locations.add(location.address) + elif hasattr(location, "port_id"): + if location.port_id not in self.manager.worldports: + self.accessible_worldports.add(location.port_id) -- cgit 1.4.1