From b524e153ad71e368afbe50da78c4b73c3ac65c5f Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 27 Sep 2025 11:10:07 -0400 Subject: Added accessible locations tab to game --- apworld/client/client.gd | 25 ++++++++- apworld/client/gamedata.gd | 120 +++++++++++++++++++++++++++++++++++++++++-- apworld/client/manager.gd | 14 +++++ apworld/client/textclient.gd | 86 ++++++++++++++++++++++--------- apworld/context.py | 42 +++++++++++++-- apworld/tracker.py | 67 ++++++++++++++++++++++++ 6 files changed, 321 insertions(+), 33 deletions(-) create mode 100644 apworld/tracker.py (limited to 'apworld') diff --git a/apworld/client/client.gd b/apworld/client/client.gd index 67edf29..05b2b6c 100644 --- a/apworld/client/client.gd +++ b/apworld/client/client.gd @@ -21,6 +21,7 @@ var _checked_locations = [] var _received_indexes = [] var _received_items = {} var _slot_data = {} +var _accessible_locations = [] signal could_not_connect signal connect_status @@ -30,6 +31,8 @@ signal location_scout_received(location_id, item_name, player_name, flags, for_s signal text_message_received(message) signal item_sent_notification(message) signal hint_received(message) +signal accessible_locations_updated +signal checked_locations_updated func _init(): @@ -51,6 +54,7 @@ func _reset_state(): _should_process = false _received_items = {} _received_indexes = [] + _accessible_locations = [] func disconnect_from_ap(): @@ -92,15 +96,26 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo _gen_version = message["generator_version"] _team = message["team"] _slot = message["slot"] - _checked_locations = message["checked_locations"] _slot_data = message["slot_data"] + _checked_locations = [] + for location in message["checked_locations"]: + _checked_locations.append(int(message["checked_locations"])) + client_connected.emit(_slot_data) elif cmd == "ConnectionRefused": could_not_connect.emit(message["text"]) global._print("Connection to AP refused") + elif cmd == "UpdateLocations": + for location in message["locations"]: + var lint = int(location) + if not _checked_locations.has(lint): + _checked_locations.append(lint) + + checked_locations_updated.emit() + elif cmd == "ItemReceived": for item in message["items"]: var index = int(item["index"]) @@ -134,6 +149,14 @@ func _on_web_socket_server_message_received(_peer_id: int, packet: String) -> vo int(loc["for_self"]) ) + elif cmd == "AccessibleLocations": + _accessible_locations.clear() + + for loc in message["locations"]: + _accessible_locations.append(int(loc)) + + accessible_locations_updated.emit() + func connectToServer(server, un, pw): sendMessage([{"cmd": "Connect", "server": server, "player": un, "password": pw}]) diff --git a/apworld/client/gamedata.gd b/apworld/client/gamedata.gd index 9eeec3b..39e0583 100644 --- a/apworld/client/gamedata.gd +++ b/apworld/client/gamedata.gd @@ -13,6 +13,7 @@ var progressive_id_by_ap_id = {} var letter_id_by_ap_id = {} var symbol_item_ids = [] var anti_trap_ids = {} +var location_name_by_id = {} var kSYMBOL_ITEMS @@ -70,6 +71,7 @@ func load(data_bytes): if door.has_ap_id(): door_id_by_ap_id[door.get_ap_id()] = door.get_id() + location_name_by_id[door.get_ap_id()] = _get_door_location_name(door) for painting in objects.get_paintings(): var room = objects.get_rooms()[painting.get_room_id()] @@ -95,6 +97,17 @@ func load(data_bytes): for letter in objects.get_letters(): letter_id_by_ap_id[letter.get_ap_id()] = letter.get_id() + location_name_by_id[letter.get_ap_id()] = _get_letter_location_name(letter) + + for mastery in objects.get_masteries(): + location_name_by_id[mastery.get_ap_id()] = _get_mastery_location_name(mastery) + + for ending in objects.get_endings(): + location_name_by_id[ending.get_ap_id()] = _get_ending_location_name(ending) + + for keyholder in objects.get_keyholders(): + if keyholder.has_key(): + location_name_by_id[keyholder.get_ap_id()] = _get_keyholder_location_name(keyholder) for panel in objects.get_panels(): var room = objects.get_rooms()[panel.get_room_id()] @@ -153,7 +166,106 @@ func get_door_receivers(door_id): return door.get_receivers() -func get_door_map_name(door_id): - var door = objects.get_doors()[door_id] - var map = objects.get_maps()[door.get_map_id()] - return map.get_name() +func _get_map_object_map_name(obj): + return objects.get_maps()[obj.get_map_id()].get_display_name() + + +func _get_room_object_map_name(obj): + return _get_map_object_map_name(objects.get_rooms()[obj.get_room_id()]) + + +func _get_room_object_location_prefix(obj): + var room = objects.get_rooms()[obj.get_room_id()] + var game_map = objects.get_maps()[room.get_map_id()] + + if room.has_panel_display_name(): + return "%s (%s)" % [game_map.get_display_name(), room.get_panel_display_name()] + else: + return game_map.get_display_name() + + +func _get_door_location_name(door): + var map_part = _get_room_object_location_prefix(door) + + if door.has_location_name(): + return "%s - %s" % [map_part, door.get_location_name()] + + var generated_location_name = _get_generated_door_location_name(door) + if generated_location_name != null: + return generated_location_name + + return "%s - %s" % [map_part, door.get_name()] + + +func _get_generated_door_location_name(door): + if door.get_type() != SCRIPT_proto.DoorType.STANDARD: + return null + + if door.get_keyholders().size() > 0 or door.get_endings().size() > 0 or door.has_complete_at(): + return null + + if door.get_panels().size() > 4: + return null + + var map_areas = [] + for panel_id in door.get_panels(): + var panel = objects.get_panels()[panel_id.get_panel()] + var panel_room = objects.get_rooms()[panel.get_room_id()] + # It's okay if panel_display_name is not present because then it's coalesced with other unnamed areas. + if not map_areas.has(panel_room.get_panel_display_name()): + map_areas.append(panel_room.get_panel_display_name()) + + if map_areas.size() > 1: + return null + + var game_map = objects.get_maps()[door.get_map_id()] + var map_area = map_areas[0] + var map_part + if map_area == "": + map_part = game_map.get_display_name() + else: + map_part = "%s (%s)" % [game_map.get_display_name(), map_area] + + var panel_names = [] + for panel_id in door.get_panels(): + var panel_data = objects.get_panels()[panel_id.get_panel()] + var panel_name + if panel_data.has_display_name(): + panel_name = panel_data.get_display_name() + else: + panel_name = panel_data.get_name() + + var location_part + if panel_id.has_answer(): + location_part = "%s/%s" % [panel_name, panel_id.get_answer().to_upper()] + else: + location_part = panel_name + + panel_names.append(location_part) + + panel_names.sort() + + return map_part + " - " + ", ".join(panel_names) + + +func _get_letter_location_name(letter): + var letter_level = 2 if letter.get_level2() else 1 + var letter_name = "%s%d" % [letter.get_key().to_upper(), letter_level] + return "%s - %s" % [_get_room_object_map_name(letter), letter_name] + + +func _get_mastery_location_name(mastery): + return "%s - Mastery" % _get_room_object_map_name(mastery) + + +func _get_ending_location_name(ending): + return ( + "%s - %s Ending" % [_get_room_object_map_name(ending), ending.get_name().to_pascal_case()] + ) + + +func _get_keyholder_location_name(keyholder): + return ( + "%s - %s Keyholder" + % [_get_room_object_location_prefix(keyholder), keyholder.get_key().to_upper()] + ) diff --git a/apworld/client/manager.gd b/apworld/client/manager.gd index 955d470..7f4a8a6 100644 --- a/apworld/client/manager.gd +++ b/apworld/client/manager.gd @@ -99,6 +99,8 @@ func _ready(): client.text_message_received.connect(_process_text_message) client.item_sent_notification.connect(_process_item_sent_notification) 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.could_not_connect.connect(_client_could_not_connect) client.connect_status.connect(_client_connect_status) @@ -302,6 +304,18 @@ func _process_location_scout(location_id, item_name, player_name, flags, for_sel collectable.setScoutedText(item_name) +func _on_accessible_locations_updated(): + var textclient_node = global.get_node("Textclient") + if textclient_node != null: + textclient_node.update_locations() + + +func _on_checked_locations_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 9841063..e345489 100644 --- a/apworld/client/textclient.gd +++ b/apworld/client/textclient.gd @@ -1,8 +1,10 @@ extends CanvasLayer +var tabs var panel var label var entry +var tracker_label var is_open = false @@ -10,26 +12,32 @@ func _ready(): process_mode = ProcessMode.PROCESS_MODE_ALWAYS layer = 2 - panel = Panel.new() - panel.set_name("Panel") - panel.offset_left = 100 - panel.offset_right = 1820 - panel.offset_top = 100 - panel.offset_bottom = 980 - panel.visible = false - add_child(panel) + tabs = TabContainer.new() + tabs.name = "Tabs" + tabs.offset_left = 100 + tabs.offset_right = 1820 + tabs.offset_top = 100 + tabs.offset_bottom = 980 + tabs.visible = false + tabs.theme = preload("res://assets/themes/baseUI.tres") + tabs.add_theme_font_size_override("font_size", 36) + add_child(tabs) + + panel = MarginContainer.new() + panel.name = "Text Client" + panel.add_theme_constant_override("margin_top", 60) + panel.add_theme_constant_override("margin_left", 60) + panel.add_theme_constant_override("margin_right", 60) + panel.add_theme_constant_override("margin_bottom", 60) + tabs.add_child(panel) label = RichTextLabel.new() label.set_name("Label") - label.offset_left = 80 - label.offset_right = 1640 - label.offset_top = 80 - label.offset_bottom = 720 label.scroll_following = true label.selection_enabled = true - panel.add_child(label) - - label.push_font(load("res://assets/fonts/Lingo2.ttf")) + 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) var entry_style = StyleBoxFlat.new() @@ -37,18 +45,30 @@ func _ready(): entry = LineEdit.new() entry.set_name("Entry") - entry.offset_left = 80 - entry.offset_right = 1640 - entry.offset_top = 760 - entry.offset_bottom = 840 - entry.add_theme_font_override("font", load("res://assets/fonts/Lingo2.ttf")) + entry.add_theme_font_override("font", preload("res://assets/fonts/Lingo2.ttf")) entry.add_theme_font_size_override("font_size", 36) entry.add_theme_color_override("font_color", Color(0, 0, 0, 1)) entry.add_theme_color_override("cursor_color", Color(0, 0, 0, 1)) entry.add_theme_stylebox_override("focus", entry_style) - panel.add_child(entry) entry.text_submitted.connect(text_entered) + var tc_arranger = VBoxContainer.new() + tc_arranger.add_child(label) + tc_arranger.add_child(entry) + tc_arranger.add_theme_constant_override("separation", 40) + panel.add_child(tc_arranger) + + var tracker_margins = MarginContainer.new() + tracker_margins.name = "Locations" + tracker_margins.add_theme_constant_override("margin_top", 60) + tracker_margins.add_theme_constant_override("margin_left", 60) + tracker_margins.add_theme_constant_override("margin_right", 60) + tracker_margins.add_theme_constant_override("margin_bottom", 60) + tabs.add_child(tracker_margins) + + tracker_label = RichTextLabel.new() + tracker_margins.add_child(tracker_label) + func _input(event): if global.loaded and event is InputEventKey and event.pressed: @@ -57,7 +77,7 @@ func _input(event): is_open = true get_tree().paused = true Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) - panel.visible = true + tabs.visible = true entry.grab_focus() get_viewport().set_input_as_handled() else: @@ -72,7 +92,7 @@ func dismiss(): if is_open: get_tree().paused = false Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) - panel.visible = false + tabs.visible = false is_open = false @@ -93,3 +113,23 @@ func text_entered(text): return ap.client.say(cmd) + + +func update_locations(): + 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) + + var location_names = [] + 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) + + location_names.sort() + + for location_name in location_names: + tracker_label.append_text("[p]%s[/p]" % location_name) diff --git a/apworld/context.py b/apworld/context.py index 05f75a3..2a2149f 100644 --- a/apworld/context.py +++ b/apworld/context.py @@ -8,9 +8,12 @@ import websockets import Utils import settings +from BaseClasses import ItemClassification from CommonClient import CommonContext, server_loop, gui_enabled, logger, get_base_parser, handle_url_arg from NetUtils import Endpoint, decode, encode from Utils import async_start +from . import Lingo2World +from .tracker import Tracker PORT = 43182 MESSAGE_MAX_SIZE = 16*1024*1024 @@ -19,9 +22,11 @@ MESSAGE_MAX_SIZE = 16*1024*1024 class Lingo2GameContext: server: Endpoint | None client: "Lingo2ClientContext" + tracker: Tracker def __init__(self): self.server = None + self.tracker = Tracker() def send_connected(self): msg = { @@ -84,6 +89,22 @@ class Lingo2GameContext: async_start(self.send_msgs([msg]), name="notif") + def send_accessible_locations(self): + msg = { + "cmd": "AccessibleLocations", + "locations": list(self.tracker.accessible_locations), + } + + async_start(self.send_msgs([msg]), name="accessible locations") + + def send_update_locations(self, locations): + msg = { + "cmd": "UpdateLocations", + "locations": locations, + } + + async_start(self.send_msgs([msg]), name="update locations") + 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: @@ -119,7 +140,14 @@ class Lingo2ClientContext(CommonContext): if self.game_ctx.server is not None: self.game_ctx.send_connected() + + self.game_ctx.tracker.setup_slot(self.slot_data) + elif cmd == "RoomUpdate": + if self.game_ctx.server is not None: + self.game_ctx.send_update_locations(args["checked_locations"]) elif cmd == "ReceivedItems": + self.game_ctx.tracker.set_collected_items(self.items_received) + if self.game_ctx.server is not None: cur_index = 0 items = [] @@ -141,6 +169,9 @@ class Lingo2ClientContext(CommonContext): items.append(item_msg) self.game_ctx.send_item_received(items) + + if any(ItemClassification.progression in ItemClassification(item.flags) for item in args["items"]): + self.game_ctx.send_accessible_locations() elif cmd == "PrintJSON": if self.game_ctx.server is not None: if "receiving" in args and "item" in args and args["item"].player == self.slot: @@ -195,6 +226,9 @@ class Lingo2ClientContext(CommonContext): self.game_ctx.send_location_info(locations) + if cmd in ["Connected", "RoomUpdate"]: + self.game_ctx.tracker.set_checked_locations(self.checked_locations) + async def pipe_loop(ctx: Lingo2GameContext): while not ctx.client.exit_event.is_set(): @@ -205,6 +239,7 @@ async def pipe_loop(ctx: Lingo2GameContext): logger.info("Connected to Lingo 2!") if ctx.client.auth is not None: ctx.send_connected() + ctx.send_accessible_locations() async for data in ctx.server.socket: for msg in decode(data): await process_game_cmd(ctx, msg) @@ -237,10 +272,7 @@ async def process_game_cmd(ctx: Lingo2GameContext, args: dict): async def run_game(): exe_file = settings.get_settings().lingo2_options.exe_file - from worlds import AutoWorldRegister - world = AutoWorldRegister.world_types["Lingo 2"] - - if world.zip_path is not None: + if Lingo2World.zip_path is not None: # This is a packaged apworld. init_scene = pkgutil.get_data(__name__, "client/run_from_apworld.tscn") init_path = Utils.local_path("data", "lingo2_init.tscn") @@ -254,7 +286,7 @@ async def run_game(): "--scene", init_path, "--", - str(world.zip_path.absolute()), + str(Lingo2World.zip_path.absolute()), ], cwd=os.path.dirname(exe_file), ) diff --git a/apworld/tracker.py b/apworld/tracker.py new file mode 100644 index 0000000..721e9b3 --- /dev/null +++ b/apworld/tracker.py @@ -0,0 +1,67 @@ +from BaseClasses import MultiWorld, CollectionState, ItemClassification +from NetUtils import NetworkItem +from . import Lingo2World, Lingo2Item +from .regions import connect_ports_from_ut +from .options import Lingo2Options + +PLAYER_NUM = 1 + + +class Tracker: + multiworld: MultiWorld + + collected_items: dict[int, int] + checked_locations: set[int] + accessible_locations: set[int] + + state: CollectionState + + def __init__(self): + self.collected_items = {} + self.checked_locations = set() + self.accessible_locations = set() + + def setup_slot(self, slot_data): + self.multiworld = MultiWorld(players=PLAYER_NUM) + world = Lingo2World(self.multiworld, PLAYER_NUM) + self.multiworld.worlds[1] = world + world.options = Lingo2Options(**{k: t(slot_data.get(k, t.default)) + for k, t in Lingo2Options.type_hints.items()}) + + world.generate_early() + world.create_regions() + + if world.options.shuffle_worldports: + port_pairings = {int(fp): int(tp) for fp, tp in slot_data["port_pairings"].items()} + connect_ports_from_ut(port_pairings, world) + + self.state = CollectionState(self.multiworld) + + def set_checked_locations(self, checked_locations: set[int]): + self.checked_locations = checked_locations.copy() + + def set_collected_items(self, network_items: list[NetworkItem]): + self.collected_items = {} + + for item in network_items: + self.collected_items[item.item] = self.collected_items.get(item.item, 0) + 1 + + self.refresh_state() + + def refresh_state(self): + self.state = CollectionState(self.multiworld) + + for item_id, item_amount in self.collected_items.items(): + for i in range(item_amount): + self.state.collect(Lingo2Item(Lingo2World.static_logic.item_id_to_name.get(item_id), + ItemClassification.progression, item_id, PLAYER_NUM), prevent_sweep=True) + + self.state.sweep_for_advancements() + + self.accessible_locations = 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.address is not None: + self.accessible_locations.add(location.address) -- cgit 1.4.1