From b53d1f94582ebc62eb0520f041f83baecb747c0a Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 4 Oct 2025 16:06:32 -0400 Subject: Added button to get logical path --- apworld/client/client.gd | 14 ++++ apworld/client/manager.gd | 2 +- apworld/client/textclient.gd | 186 ++++++++++++++++++++++++++++++++++++------- apworld/context.py | 26 ++++++ apworld/player_logic.py | 2 + apworld/tracker.py | 35 +++++++- 6 files changed, 233 insertions(+), 32 deletions(-) diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 62d7fd8..e25ad4b 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd @@ -187,6 +187,12 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo keyboard_update_received.emit(updates) + elif cmd == "PathReply": + var textclient = global.get_node("Textclient") + textclient.display_logical_path( + message["type"], int(message.get("id", null)), message["path"] + ) + func connectToServer(server, un, pw): sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) @@ -253,6 +259,14 @@ func checkWorldport(port_id): sendMessage([{"cmd": "CheckWorldport", "port_id": port_id}]) +func getLogicalPath(object_type, object_id): + var msg = {"cmd": "GetPath", "type": object_type} + if object_id != null: + msg["id"] = object_id + + sendMessage([msg]) + + func sendQuit(): sendMessage([{"cmd": "Quit"}]) diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 9212233..0d5a5aa 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -347,7 +347,7 @@ func _on_accessible_locations_updated(): func _on_checked_locations_updated(): var textclient_node = global.get_node("Textclient") if textclient_node != null: - textclient_node.update_locations() + textclient_node.update_locations(false) func _on_checked_worldports_updated(): diff --git a/apworld/client/textclient.gd b/apworld/client/textclient.gd index 530eddb..f785a03 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd @@ -4,7 +4,6 @@ var tabs var panel var label var entry -var tracker_label var is_open = false var locations_overlay @@ -12,11 +11,21 @@ var location_texture var worldport_texture var goal_texture +var tracker_tree +var tracker_loc_tree_item_by_id = {} +var tracker_port_tree_item_by_id = {} +var tracker_goal_tree_item = null +var tracker_object_by_index = {} + var worldports_tab var worldports_tree var port_tree_item_by_map = {} var port_tree_item_by_map_port = {} +const kLocation = 0 +const kWorldport = 1 +const kGoal = 2 + func _ready(): process_mode = ProcessMode.PROCESS_MODE_ALWAYS @@ -61,7 +70,7 @@ func _ready(): label.size_flags_horizontal = Control.SIZE_EXPAND_FILL label.size_flags_vertical = Control.SIZE_EXPAND_FILL label.push_font(preload("res://assets/fonts/Lingo2.ttf")) - label.push_font_size(36) + label.push_font_size(30) var entry_style = StyleBoxFlat.new() entry_style.bg_color = Color(0.9, 0.9, 0.9, 1) @@ -89,8 +98,18 @@ func _ready(): tracker_margins.add_theme_constant_override("margin_bottom", 60) tabs.add_child(tracker_margins) - tracker_label = RichTextLabel.new() - tracker_margins.add_child(tracker_label) + tracker_tree = Tree.new() + tracker_tree.columns = 3 + tracker_tree.hide_root = true + tracker_tree.add_theme_font_size_override("font_size", 24) + tracker_tree.add_theme_color_override("font_color", Color(0.8, 0.8, 0.8, 1)) + tracker_tree.add_theme_constant_override("v_separation", 1) + tracker_tree.item_edited.connect(_on_tracker_button_clicked) + tracker_tree.set_column_expand(0, false) + tracker_tree.set_column_expand(1, true) + tracker_tree.set_column_expand(2, false) + tracker_tree.set_column_custom_minimum_width(2, 200) + tracker_margins.add_child(tracker_tree) worldports_tab = MarginContainer.new() worldports_tab.name = "Worldports" @@ -167,14 +186,10 @@ func text_entered(text): ap.client.say(cmd) -func update_locations(): +func update_locations(reset_locations = true): var ap = global.get_node("Archipelago") var gamedata = global.get_node("Gamedata") - tracker_label.clear() - tracker_label.push_font(preload("res://assets/fonts/Lingo2.ttf")) - tracker_label.push_font_size(24) - locations_overlay.clear() locations_overlay.push_font(preload("res://assets/fonts/Lingo2.ttf")) locations_overlay.push_font_size(24) @@ -182,45 +197,62 @@ func update_locations(): locations_overlay.push_outline_color(Color(0, 0, 0, 1)) locations_overlay.push_outline_size(2) - const kLocation = 0 - const kWorldport = 1 - const kGoal = 2 - - var location_names = [] - var type_by_name = {} + var locations = [] for location_id in ap.client._accessible_locations: if not ap.client._checked_locations.has(location_id): var location_name = gamedata.location_name_by_id.get(location_id, "(Unknown)") - location_names.append(location_name) - type_by_name[location_name] = kLocation + ( + locations + . append( + { + "name": location_name, + "type": kLocation, + "id": location_id, + } + ) + ) 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) - type_by_name[port_name] = kWorldport - - location_names.sort() + ( + locations + . append( + { + "name": port_name, + "type": kWorldport, + "id": port_id, + } + ) + ) + + locations.sort_custom(func(a, b): return a["name"] < b["name"]) if ap.client._goal_accessible: var location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[ ap.victory_condition ]] - location_names.push_front(location_name) - type_by_name[location_name] = kGoal + ( + locations + . push_front( + { + "name": location_name, + "type": kGoal, + } + ) + ) var count = 0 - for location_name in location_names: - tracker_label.append_text("[p]%s[/p]" % location_name) + for location in locations: if count < 18: locations_overlay.push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT) - locations_overlay.append_text(location_name) + locations_overlay.append_text(location["name"]) locations_overlay.append_text(" ") - if type_by_name[location_name] == kLocation: + if location["type"] == kLocation: locations_overlay.add_image(location_texture) - elif type_by_name[location_name] == kWorldport: + elif location["type"] == kWorldport: locations_overlay.add_image(worldport_texture) - elif type_by_name[location_name] == kGoal: + elif location["type"] == kGoal: locations_overlay.add_image(goal_texture) locations_overlay.pop() count += 1 @@ -228,12 +260,99 @@ func update_locations(): if count > 18: locations_overlay.append_text("[p align=right][lb]...[rb][/p]") + if reset_locations: + reset_tracker_tab() + + var root_ti = tracker_tree.create_item(null) + + for location in locations: + var loc_row = root_ti.create_child() + loc_row.set_cell_mode(0, TreeItem.CELL_MODE_ICON) + loc_row.set_selectable(0, false) + loc_row.set_text(1, location["name"]) + loc_row.set_selectable(1, false) + loc_row.set_cell_mode(2, TreeItem.CELL_MODE_CUSTOM) + loc_row.set_text(2, "Show Path") + loc_row.set_custom_as_button(2, true) + loc_row.set_editable(2, true) + loc_row.set_selectable(2, false) + loc_row.set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER) + + if location["type"] == kLocation: + loc_row.set_icon(0, location_texture) + tracker_loc_tree_item_by_id[location["id"]] = loc_row + elif location["type"] == kWorldport: + loc_row.set_icon(0, worldport_texture) + tracker_port_tree_item_by_id[location["id"]] = loc_row + elif location["type"] == kGoal: + loc_row.set_icon(0, goal_texture) + tracker_goal_tree_item = loc_row + + tracker_object_by_index[loc_row.get_index()] = location + else: + for loc_row in tracker_tree.get_root().get_children(): + loc_row.visible = false + + for location_id in tracker_loc_tree_item_by_id.keys(): + if ( + ap.client._accessible_locations.has(location_id) + and not ap.client._checked_locations.has(location_id) + ): + tracker_loc_tree_item_by_id[location_id].visible = true + + for port_id in tracker_port_tree_item_by_id.keys(): + if ( + ap.client._accessible_worldports.has(port_id) + and not ap.client._checked_worldports.has(port_id) + ): + tracker_port_tree_item_by_id[port_id].visible = true + + if tracker_goal_tree_item != null and ap.client._goal_accessible: + tracker_goal_tree_item.visible = true + func update_locations_visibility(): var ap = global.get_node("Archipelago") locations_overlay.visible = ap.show_locations +func _on_tracker_button_clicked(): + var edited_item = tracker_tree.get_edited() + var edited_index = edited_item.get_index() + + if tracker_object_by_index.has(edited_index): + var tracker_object = tracker_object_by_index[edited_index] + var ap = global.get_node("Archipelago") + var type_str = "" + if tracker_object["type"] == kLocation: + type_str = "location" + elif tracker_object["type"] == kWorldport: + type_str = "worldport" + elif tracker_object["type"] == kGoal: + type_str = "goal" + ap.client.getLogicalPath(type_str, tracker_object.get("id", null)) + + +func display_logical_path(object_type, object_id, paths): + var ap = global.get_node("Archipelago") + var gamedata = global.get_node("Gamedata") + + var location_name = "(Unknown)" + if object_type == "location" and object_id != null: + location_name = gamedata.location_name_by_id.get(object_id, "(Unknown)") + elif object_type == "worldport" and object_id != null: + location_name = gamedata.get_worldport_display_name(object_id) + elif object_type == "goal": + location_name = gamedata.ending_display_name_by_name[ap.kEndingNameByVictoryValue[ + ap.victory_condition + ]] + + label.append_text("[p]Path to %s:[/p]" % location_name) + label.append_text("[ol]" + "\n".join(paths) + "[/ol]") + + panel.visible = true + + func setup_worldports(): tabs.set_tab_hidden(2, false) @@ -308,3 +427,12 @@ func reset(): port_tree_item_by_map.clear() port_tree_item_by_map_port.clear() worldports_tree.clear() + reset_tracker_tab() + + +func reset_tracker_tab(): + tracker_loc_tree_item_by_id.clear() + tracker_port_tree_item_by_id.clear() + tracker_goal_tree_item = null + tracker_object_by_index.clear() + tracker_tree.clear() diff --git a/apworld/context.py b/apworld/context.py index e78ce35..7b5f0bc 100644 --- a/apworld/context.py +++ b/apworld/context.py @@ -229,6 +229,21 @@ class Lingo2GameContext: async_start(self.send_msgs([msg]), name="update worldports") + def send_path_reply(self, object_type: str, object_id: int | None, path: list[str]): + if self.server is None: + return + + msg = { + "cmd": "PathReply", + "type": object_type, + "path": path, + } + + if object_id is not None: + msg["id"] = object_id + + async_start(self.send_msgs([msg]), name="path reply") + 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: @@ -542,6 +557,17 @@ async def process_game_cmd(manager: Lingo2Manager, args: dict): if len(updates) > 0: async_start(manager.client_ctx.update_worldports(updates), name="client update worldports") manager.game_ctx.send_update_worldports(updates) + elif cmd == "GetPath": + path = None + + if args["type"] == "location": + path = manager.tracker.get_path_to_location(args["id"]) + elif args["type"] == "worldport": + path = manager.tracker.get_path_to_port(args["id"]) + elif args["type"] == "goal": + path = manager.tracker.get_path_to_goal() + + manager.game_ctx.send_path_reply(args["type"], args.get("id", None), path) elif cmd == "Quit": manager.client_ctx.exit_event.set() diff --git a/apworld/player_logic.py b/apworld/player_logic.py index 3ed1bb1..84c93c8 100644 --- a/apworld/player_logic.py +++ b/apworld/player_logic.py @@ -214,6 +214,7 @@ class Lingo2PlayerLogic: real_items: list[str] double_letter_amount: dict[str, int] + goal_room_id: int def __init__(self, world: "Lingo2World"): self.world = world @@ -327,6 +328,7 @@ class Lingo2PlayerLogic: if world.options.victory_condition.current_key.removesuffix("_ending").upper() == ending.name: item_name = "Victory" + self.goal_room_id = ending.room_id self.event_loc_item_by_room.setdefault(ending.room_id, {})[event_name] = item_name diff --git a/apworld/tracker.py b/apworld/tracker.py index 7239b65..c65317c 100644 --- a/apworld/tracker.py +++ b/apworld/tracker.py @@ -1,6 +1,6 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Iterator -from BaseClasses import MultiWorld, CollectionState, ItemClassification +from BaseClasses import MultiWorld, CollectionState, ItemClassification, Region, Entrance from NetUtils import NetworkItem from . import Lingo2World, Lingo2Item from .regions import connect_ports_from_ut @@ -110,3 +110,34 @@ class Tracker: elif hasattr(location, "goal") and location.goal: if not self.manager.goaled: self.goal_accessible = True + + def get_path_to_location(self, location_id: int) -> list[str] | None: + location_name = self.world.location_id_to_name.get(location_id) + location = self.multiworld.get_location(location_name, PLAYER_NUM) + return self.get_logical_path(location.parent_region) + + def get_path_to_port(self, port_id: int) -> list[str] | None: + port = self.world.static_logic.objects.ports[port_id] + region_name = self.world.static_logic.get_room_region_name(port.room_id) + region = self.multiworld.get_region(region_name, PLAYER_NUM) + return self.get_logical_path(region) + + def get_path_to_goal(self): + room_id = self.world.player_logic.goal_room_id + region_name = self.world.static_logic.get_room_region_name(room_id) + region = self.multiworld.get_region(region_name, PLAYER_NUM) + return self.get_logical_path(region) + + def get_logical_path(self, region: Region) -> list[str] | None: + if region not in self.state.path: + return None + + def flist_to_iter(path_value) -> Iterator[str]: + while path_value: + region_or_entrance, path_value = path_value + yield region_or_entrance + + reversed_path = self.state.path.get(region) + flat_path = reversed(list(map(str, flist_to_iter(reversed_path)))) + + return list(flat_path)[1::2] -- cgit 1.4.1