From 11e9e71990fea312fd5e01920a8dae185704166c Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 19 Jun 2024 10:23:09 -0700 Subject: [PATCH 1/8] remove assigned hint region from Thrill Digger --- data/world/Eldin.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/world/Eldin.yaml b/data/world/Eldin.yaml index afdb2fd9..57eacb0f 100644 --- a/data/world/Eldin.yaml +++ b/data/world/Eldin.yaml @@ -112,7 +112,6 @@ Eldin Volcano - Stamina Fruit on Vine Wall near Thrill Digger: Nothing - name: Thrill Digger Cave - hint_region: Eldin Volcano events: Can Play Thrill Digger Minigame: Digging_Mitts exits: From 1c1ac24a22010519ade1c304bc71f15bda52f9ff Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 19 Jun 2024 10:28:04 -0700 Subject: [PATCH 2/8] proeprly autosave marekd gossip stones --- gui/tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/tracker.py b/gui/tracker.py index 2c4f8f4d..3d50897e 100644 --- a/gui/tracker.py +++ b/gui/tracker.py @@ -1514,8 +1514,10 @@ def autosave_tracker(self) -> None: # Then read it again to input extra data autosave: dict = yaml_load(filename) + # Marked locations includes gossip stones, so read directly from + # the location table to include those autosave["marked_locations"] = [ - loc.name for loc in self.world.get_all_item_locations() if loc.marked + loc.name for loc in self.world.location_table.values() if loc.marked ] autosave["marked_items"] = [item.name for item in self.inventory.elements()] autosave["connected_entrances"] = { From ae00dabf141591dce94c7b22acd7626a22bca1de Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Sun, 23 Jun 2024 20:22:39 -0700 Subject: [PATCH 3/8] fix entrance to construction bay --- data/world/Lanayru.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/world/Lanayru.yaml b/data/world/Lanayru.yaml index aba9f796..e016f465 100644 --- a/data/world/Lanayru.yaml +++ b/data/world/Lanayru.yaml @@ -493,7 +493,7 @@ exits: Shipyard Statue: Nothing Shipyard End of Minecart Ride: Nothing - Construction Bay Sand Pit: "'Defeat_Shipyard_Moldarach'" + Construction Bay Sand Pit: Nothing Lanayru Sand Sea: Nothing locations: Shipyard - Bonk Sign before Gortram the Rickety Coaster Operator: Nothing From 15d7006b19fe44b54a7dd2bd8b81347040253c5a Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 26 Jun 2024 04:58:49 -0700 Subject: [PATCH 4/8] fix word wrapping on lead_to_label --- gui/tracker.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/tracker.py b/gui/tracker.py index 3d50897e..dbbd8cdc 100644 --- a/gui/tracker.py +++ b/gui/tracker.py @@ -1168,7 +1168,9 @@ def show_target_selection_info( lead_to_label = QLabel(f"Where did {entrance.original_name} lead to?") lead_to_label.setMargin(10) + lead_to_label.setWordWrap(True) back_button = TrackerShowEntrancesButton(parent_area_name, "Back") + back_button.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) back_button.show_area_entrances.connect(self.show_area_entrances) # Add a way to filter entrance targets From 87a57b4c85acda2029899d157833c54ba3f7526a Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 26 Jun 2024 04:59:24 -0700 Subject: [PATCH 5/8] fix no starting sword being weird --- gui/tracker.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gui/tracker.py b/gui/tracker.py index dbbd8cdc..298c98cd 100644 --- a/gui/tracker.py +++ b/gui/tracker.py @@ -701,6 +701,10 @@ def initialize_tracker_world( if starting_hearts == "random": starting_hearts.set_value("6") + # Set starting sword as No Sword if it's random + if self.world.setting("starting_sword") == "random": + self.world.setting("starting_sword").set_value("no_sword") + # Build the world (only as necessary) self.world.build() self.world.perform_pre_entrance_shuffle_tasks() From 482ab4933232dc1752b01cf9e7bd7857fdef1def Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 26 Jun 2024 05:00:17 -0700 Subject: [PATCH 6/8] remove forced volcano summit hint region from VS waterfall area --- data/world/Eldin.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/data/world/Eldin.yaml b/data/world/Eldin.yaml index 89ef872e..3870ffe6 100644 --- a/data/world/Eldin.yaml +++ b/data/world/Eldin.yaml @@ -312,7 +312,6 @@ Volcano Summit - Goddess Cube in Lava Lake: Fireshield_Earrings and (Long_Range_Skyward_Strike or ('Beaten_Boko_Base' and Goddess_Sword)) - name: Volcano Summit Waterfall - hint_region: Volcano Summit events: Can Collect Water: Bottle exits: From 1c621e3349ea2a9626de365f27a3cde33e2eef96 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Wed, 26 Jun 2024 05:01:04 -0700 Subject: [PATCH 7/8] rework how to show entrances on the entrance tracker --- gui/components/tracker_entrance_label.py | 12 ++- gui/components/tracker_target_label.py | 13 +++- gui/tracker.py | 99 +++++++++++++++++------- logic/area.py | 73 +++++++++++++++++ logic/entrance_shuffle.py | 5 ++ logic/world.py | 2 + 6 files changed, 171 insertions(+), 33 deletions(-) diff --git a/gui/components/tracker_entrance_label.py b/gui/components/tracker_entrance_label.py index 4bdb3fa9..c31d0b09 100644 --- a/gui/components/tracker_entrance_label.py +++ b/gui/components/tracker_entrance_label.py @@ -15,12 +15,17 @@ class TrackerEntranceLabel(QLabel): disconnect_entrance = Signal(Entrance, str) def __init__( - self, entrance_: Entrance, parent_area_name_: str, recent_search_: Search + self, + entrance_: Entrance, + parent_area_name_: str, + recent_search_: Search, + show_full_connection_: bool, ) -> None: super().__init__() self.entrance = entrance_ self.parent_area_name = parent_area_name_ self.recent_search = recent_search_ + self.show_full_connection = show_full_connection_ self.setCursor(QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) self.setMargin(10) self.setMinimumHeight(30) @@ -34,7 +39,10 @@ def update_text(self, recent_search_: Search | None = None) -> None: connected_area = self.entrance.connected_area original_parent, original_connected = self.entrance.original_name.split(" -> ") first_part = ( - f"{original_parent} to " if original_parent != self.parent_area_name else "" + f"{original_parent} to " + if self.entrance.parent_area.hard_assigned_region != self.parent_area_name + or self.show_full_connection + else "" ) self.setText( f"{first_part}{original_connected} -> {connected_area.name if connected_area else '?'}" diff --git a/gui/components/tracker_target_label.py b/gui/components/tracker_target_label.py index f0f8f92f..66448549 100644 --- a/gui/components/tracker_target_label.py +++ b/gui/components/tracker_target_label.py @@ -12,7 +12,11 @@ class TrackerTargetLabel(QLabel): clicked = Signal(Entrance, Entrance, str) def __init__( - self, entrance_: Entrance, target_: Entrance, parent_area_name_: str + self, + entrance_: Entrance, + target_: Entrance, + parent_area_name_: str, + show_full_connection: bool, ) -> None: super().__init__() self.entrance = entrance_ @@ -25,7 +29,12 @@ def __init__( self.setMaximumWidth(273) self.setWordWrap(True) - self.setText(self.target.replaces.original_name.split(" -> ")[1]) + if show_full_connection: + self.setText( + f"{self.target.connected_area} from {self.target.replaces.parent_area}" + ) + else: + self.setText(self.target.replaces.original_name.split(" -> ")[1]) def mouseReleaseEvent(self, ev: QMouseEvent) -> None: if ev.button() == QtCore.Qt.MouseButton.LeftButton: diff --git a/gui/tracker.py b/gui/tracker.py index 298c98cd..e3ad8b2e 100644 --- a/gui/tracker.py +++ b/gui/tracker.py @@ -765,9 +765,7 @@ def initialize_tracker_world( # Connect autosaved entrances if autosaved_entrances := autosave.get("connected_entrances", None): - for entrance in self.world.get_shuffled_entrances( - only_primary=self.world.setting("decouple_entrances") == "off" - ): + for entrance in self.world.get_shuffled_entrances(): if saved_target := autosaved_entrances.get( entrance.original_name, None ): @@ -929,6 +927,22 @@ def setup_tracker_entrances(self) -> None: entrance_pools = create_entrance_pools(self.world) self.target_entrance_pools = create_target_pools(entrance_pools) + # Create reverse pools for each pool if entrances are not already decoupled. + # This allows users to map any entrance that comes up, and allows us + # to only display the entrances that are possible to connect to + for type, pool in self.target_entrance_pools.copy().items(): + reverse_targets: list[Entrance] = [] + reverse_type = f"{type} Reverse" + for target in pool: + reverse_target = target.replaces.reverse.assumed + if not target.replaces.decoupled and reverse_target not in pool: + reverse_targets.append(reverse_target) + target.replaces.reverse.type = reverse_type + + if reverse_targets: + self.target_entrance_pools[reverse_type] = reverse_targets + print(f"Added {len(reverse_targets)} targets to {reverse_type}") + # Prevent implicit access to any target entrances for target_pool in self.target_entrance_pools.values(): for target in target_pool: @@ -945,14 +959,19 @@ def update_areas_entrances(self) -> None: area_button.main_entrance_name ) - # Then redistribute entrances to each button - for entrance in self.world.get_shuffled_entrances( - only_primary=self.world.setting("decouple_entrances") == "off" - ): - if entrance.requirement.type != RequirementType.IMPOSSIBLE: - for region in entrance.parent_area.hint_regions: - if area_button := self.areas.get(region, None): - area_button.entrances.append(entrance) + # Find all the shuffled entrances for each area + if not area_button.tracker_children: + hard_assigned_areas: list[Area] = [] + for area in self.world.areas.values(): + if area.hard_assigned_region == area_button.area: + hard_assigned_areas.append(area) + + if hard_assigned_areas: + entrance_spheres = hard_assigned_areas[0].find_shuffled_entrances( + hard_assigned_areas + ) + for sphere in entrance_spheres: + area_button.entrances.extend(sphere) def set_map_area(self, area_name: str) -> None: area = self.areas.get(area_name, None) @@ -1097,8 +1116,19 @@ def show_area_entrances(self, area_name: str) -> None: entrances.sort() for i, entrance in enumerate(entrances): + # If there are multiple entrances which lead to the same + # area, then show the full connection to differentiate them + show_full_connection = any( + [ + e + for e in entrances + if e != entrance + and e.original_connected_area + == entrance.original_connected_area + ] + ) entrance_label = TrackerEntranceLabel( - entrance, area_name, area_button.recent_search + entrance, area_name, area_button.recent_search, show_full_connection ) entrance_label.choose_target.connect(self.show_target_selection_info) entrance_label.disconnect_entrance.connect( @@ -1202,18 +1232,22 @@ def show_target_selection_info( targets.sort(key=lambda e: e.replaces.sort_priority) - areas_shown = set() for i, target in enumerate(targets): # Only show targets which haven't been connected yet - # and don't show multiple targets that lead to the same - # area - - if ( - target.connected_area is not None - and target.connected_area not in areas_shown - ): - areas_shown.add(target.connected_area) - target_label = TrackerTargetLabel(entrance, target, parent_area_name) + if target.connected_area is not None: + # If this target leads to the same area as other targets + # show the full entrance name to distinguish them from + # one another + show_full_connection = any( + [ + t + for t in targets + if t != target and target.connected_area == t.connected_area + ] + ) + target_label = TrackerTargetLabel( + entrance, target, parent_area_name, show_full_connection + ) target_label.clicked.connect(self.on_click_target_label) if i < len(targets) / 2: @@ -1236,6 +1270,16 @@ def show_target_selection_info( self.ui.tracker_locations_scroll_layout.addLayout(left_layout) self.ui.tracker_locations_scroll_layout.addLayout(right_layout) + def show_current_area(self) -> None: + if location_label := self.ui.tracker_locations_scroll_area.findChild( + TrackerLocationLabel + ): + self.show_area_locations(location_label.parent_area_button.area) + elif entrance_label := self.ui.tracker_locations_scroll_area.findChild( + TrackerEntranceLabel + ): + self.show_area_entrances(entrance_label.parent_area_name) + def on_filter_text_changed(self, filter: str) -> None: for label in self.ui.tracker_tab.findChildren(TrackerTargetLabel): label.setVisible(filter.lower() in label.text().lower()) @@ -1447,13 +1491,10 @@ def update_tracker(self) -> None: location.in_semi_logic = location in semi_logic_locations # Update any labels that are currently shown - location_label_area_name = "" for location_label in self.ui.tracker_locations_scroll_area.findChildren( TrackerLocationLabel ): location_label.update_color(search) - if not location_label_area_name: - location_label_area_name = location_label.parent_area_button.area for entrance_label in self.ui.tracker_locations_scroll_area.findChildren( TrackerEntranceLabel @@ -1463,7 +1504,7 @@ def update_tracker(self) -> None: for dungeon_label in self.ui.tracker_tab.findChildren(TrackerDungeonLabel): dungeon_label.update_style() - self.show_area_location_info(location_label_area_name) + self.show_current_area() self.autosave_tracker() self.update_statistics() if self.allow_sphere_tracking: @@ -1725,7 +1766,7 @@ def on_click_inventory_button(self, item: Item, item_image: str): self.sphere_tracked_items[self.last_checked_location] = item.name self.update_tracker() if self.last_opened_region is not None: - self.show_area_locations(self.last_opened_region.area) + self.show_current_area() def update_spheres(self): # Copy the current inventory to pass into the search @@ -1836,7 +1877,7 @@ def on_click_hint_label(self, hint: str, area: TrackerArea): else: area.hints.add(hint) - self.show_area_locations(area.area) + self.show_current_area() def toggle_sphere_tracking(self): self.allow_sphere_tracking = not self.allow_sphere_tracking @@ -1851,7 +1892,7 @@ def toggle_sphere_tracking(self): self.ui.toggle_sphere_tracking_button.setText("Enable Sphere Tracking") self.cancel_sphere_tracking() if self.last_opened_region is not None: - self.show_area_locations(self.last_opened_region.area) + self.show_current_area() self.update_tracker() diff --git a/logic/area.py b/logic/area.py index a20d513b..6bf7126e 100644 --- a/logic/area.py +++ b/logic/area.py @@ -37,6 +37,7 @@ class Area: def __init__(self) -> None: self.id: int = None self.name: str = None + self.hard_assigned_region: str = None self.hint_regions: set[str] = set() self.events: list[EventAccess] = [] self.locations: list[LocationAccess] = [] @@ -118,6 +119,78 @@ def get_provinces(self) -> set[str]: return provinces + # Performs a breadth first search to find all the shuffled entrances + # within a given area. The area must have a defined hint region. + # Returns the shuffled entrances in the order they were discovered by + # shuffled entrance spheres. + def find_shuffled_entrances(self, starting_queue: list["Area"] = []): + if not self.hard_assigned_region: + return + + shuffled_entrances: list[list[Entrance]] = [] + already_checked_areas: set["Area"] = set() + already_checked_entrances: set[Entrance] = set() + area_queue: list["Area"] = starting_queue + if not area_queue: + area_queue.append(self) + + entrances_to_try: list[Entrance] = [] + first_iteration = True + while entrances_to_try or first_iteration: + first_iteration = False + entrances_to_try.clear() + + for area in area_queue: + for entrance in area.exits: + if entrance in already_checked_entrances: + continue + + # Only add entrances which fit the following criteria + # - The entrance is shuffled and not impossible + # - The entrance is decoupled or the entrance insn't connected or the entrance's replaced reverse hasn't been added yet + if ( + entrance.shuffled + and entrance.requirement.type != RequirementType.IMPOSSIBLE + ): + if ( + entrance.decoupled + or entrance.replaces is None + or entrance.replaces.reverse + not in already_checked_entrances + ): + entrances_to_try.append(entrance) + # Else, append this entrances connected area to the area queue + else: + connected_area = entrance.connected_area + if connected_area: + if connected_area not in already_checked_areas and ( + self.hard_assigned_region in connected_area.hint_regions + or not connected_area.hint_regions + ): + area_queue.append(connected_area) + already_checked_areas.add(connected_area) + + already_checked_entrances.add(entrance) + + # Clear the area queue and append the list of entrances to try + # if there were any + area_queue.clear() + if entrances_to_try: + shuffled_entrances.append(entrances_to_try.copy()) + + # Gather all the new areas we can find to try for shuffled entrances + for entrance in entrances_to_try: + connected_area = entrance.connected_area + if connected_area: + if connected_area not in already_checked_areas and ( + self.hard_assigned_region in connected_area.hint_regions + or not connected_area.hint_regions + ): + area_queue.append(connected_area) + already_checked_areas.add(connected_area) + + return shuffled_entrances + # Will perform a search from the starting area until all # possibly connected hint regions have been found. diff --git a/logic/entrance_shuffle.py b/logic/entrance_shuffle.py index e2bb94bf..3e23aa05 100644 --- a/logic/entrance_shuffle.py +++ b/logic/entrance_shuffle.py @@ -252,6 +252,11 @@ def create_entrance_pools(world: World) -> EntrancePools: ] if world.setting("randomize_overworld_entrances") == "on": + # Normally we allow any overworld entrances to link together. + # However, if overworld entrances are mixed with other entrance types + # that expect to only match with exclusively primary or non-primary + # entrances, we have to separate overworld entrances by their primary/ + # non-primary distinction to fit with the other entrances exclude_overworld_reverse = ( any("Overworld" in pool for pool in world.setting_map.mixed_entrance_pools) and world.setting("decouple_entrances") == "off" diff --git a/logic/world.py b/logic/world.py index 1536d7bc..4f4fe59b 100644 --- a/logic/world.py +++ b/logic/world.py @@ -210,10 +210,12 @@ def load_world_graph(self) -> None: if dungeon_name := area_node.get("dungeon", False): self.add_dungeon(dungeon_name) new_area.hint_regions.add(dungeon_name) + new_area.hard_assigned_region = dungeon_name if "dungeon_starting_area" in area_node: self.get_dungeon(dungeon_name).starting_area = new_area elif hint_region := area_node.get("hint_region", False): new_area.hint_regions.add(hint_region) + new_area.hard_assigned_region = hint_region if "events" in area_node: for event_name, req_str in area_node["events"].items(): From 402bdf8d2564687747dd1ae702c92df0d551144c Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Thu, 27 Jun 2024 00:29:28 -0700 Subject: [PATCH 8/8] add an "Everything Discovered" area to the tracker --- data/tracker_areas.yaml | 5 +++++ gui/components/tracker_area.py | 11 ++++++++++- gui/tracker.py | 27 ++++++++++++++++++++++----- logic/search.py | 19 +++++++++++++++++++ 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/data/tracker_areas.yaml b/data/tracker_areas.yaml index b9bfbcc3..578330b2 100644 --- a/data/tracker_areas.yaml +++ b/data/tracker_areas.yaml @@ -7,6 +7,11 @@ - Lanayru - Sky - Inside the Thunderhead + - Everything Discovered + +- name: Everything Discovered + x: 10 + y: 263 - name: Sky alias: The Sky diff --git a/gui/components/tracker_area.py b/gui/components/tracker_area.py index 08b37ec5..3835cc18 100644 --- a/gui/components/tracker_area.py +++ b/gui/components/tracker_area.py @@ -76,6 +76,15 @@ def get_all_locations(self) -> list[Location]: if loc not in locations_set: all_locations.append(loc) locations_set.add(loc) + # If this is the "Everything Discovered" area, then get everything which has been discovered + # in the world + if self.recent_search and self.area == "Everything Discovered": + connected_areas = self.recent_search.get_all_connected_areas() + for loc in self.recent_search.worlds[0].location_table.values(): + if any( + [la for la in loc.loc_access_list if la.area in connected_areas] + ): + all_locations.append(loc) return all_locations def get_included_locations( @@ -126,7 +135,7 @@ def update(self, search: "Search | None" = None) -> None: if ( len(self.locations) + len(self.tracker_children) == 0 or self.recent_search is None - ): + ) and self.area != "Everything Discovered": return all_unmarked_locations = self.get_unmarked_locations(remove_special_types=False) diff --git a/gui/tracker.py b/gui/tracker.py index e3ad8b2e..7938388b 100644 --- a/gui/tracker.py +++ b/gui/tracker.py @@ -931,6 +931,11 @@ def setup_tracker_entrances(self) -> None: # This allows users to map any entrance that comes up, and allows us # to only display the entrances that are possible to connect to for type, pool in self.target_entrance_pools.copy().items(): + + # Reverse pools can't exist for non-assumed entrance types + if type in Entrance.NON_ASSUMED_ENTRANCE_TYPES: + continue + reverse_targets: list[Entrance] = [] reverse_type = f"{type} Reverse" for target in pool: @@ -959,7 +964,7 @@ def update_areas_entrances(self) -> None: area_button.main_entrance_name ) - # Find all the shuffled entrances for each area + # Find all the shuffled entrances for each area and add them if not area_button.tracker_children: hard_assigned_areas: list[Area] = [] for area in self.world.areas.values(): @@ -973,6 +978,14 @@ def update_areas_entrances(self) -> None: for sphere in entrance_spheres: area_button.entrances.extend(sphere) + # Add all discovered entrances to "Everything Discovered" + if area_button.area == "Everything Discovered": + search, _ = self.get_tracker_search() + connected_areas = search.get_all_connected_areas() + for entrance in self.world.get_shuffled_entrances(): + if entrance.parent_area in connected_areas: + area_button.entrances.append(entrance) + def set_map_area(self, area_name: str) -> None: area = self.areas.get(area_name, None) if area is None: @@ -1446,10 +1459,7 @@ def compute_tooltips(self) -> None: tooltips_search = TooltipsSearch(self.world) tooltips_search.do_search() - def update_tracker(self) -> None: - if not self.started: - return - + def get_tracker_search(self) -> tuple: # Make a copy of the inventory to modify inventory = self.inventory.copy() @@ -1463,6 +1473,13 @@ def update_tracker(self) -> None: # Use modified inventory for main search search = Search(SearchMode.ACCESSIBLE_LOCATIONS, [self.world], inventory) + return (search, already_added) + + def update_tracker(self) -> None: + if not self.started: + return + + search, already_added = self.get_tracker_search() search.search_worlds() for area_button in self.areas.values(): diff --git a/logic/search.py b/logic/search.py index 01380e03..ec33b3be 100644 --- a/logic/search.py +++ b/logic/search.py @@ -286,6 +286,25 @@ def remove_empty_spheres(self) -> None: self.playthrough_spheres.pop(index) self.entrance_spheres.pop(index) + # Will return all areas which have a non-impossible connection + # from the root of the world graph + def get_all_connected_areas(self) -> set[Area]: + found_areas = set() + area_queue: list[Area] = [world.root for world in self.worlds] + + while len(area_queue) > 0: + area = area_queue.pop(0) + + for entrance in area.exits: + if entrance.requirement.type == RequirementType.IMPOSSIBLE: + continue + if connected_area := entrance.connected_area: + if connected_area not in found_areas: + area_queue.append(connected_area) + found_areas.add(connected_area) + + return found_areas + # Will dump a file which can be turned into a visual graph using graphviz # https://graphviz.org/download/ # Use this command to generate the graph: dot -Tsvg -o world.svg