 cf0ae5e31b
			
		
	
	cf0ae5e31b
	
	
	
		
			
			Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
		
			
				
	
	
		
			879 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			879 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from collections import defaultdict
 | |
| from collections.abc import Generator
 | |
| from dataclasses import dataclass
 | |
| from typing import TYPE_CHECKING, ClassVar, Optional
 | |
| 
 | |
| from Fill import FillError
 | |
| from Options import OptionError
 | |
| 
 | |
| from .. import Macros
 | |
| from ..Locations import LOCATION_TABLE, TWWFlag, split_location_name_by_zone
 | |
| 
 | |
| if TYPE_CHECKING:
 | |
|     from .. import TWWWorld
 | |
| 
 | |
| 
 | |
| @dataclass(frozen=True)
 | |
| class ZoneEntrance:
 | |
|     """
 | |
|     A data class that encapsulates information about a zone entrance.
 | |
|     """
 | |
| 
 | |
|     entrance_name: str
 | |
|     island_name: Optional[str] = None
 | |
|     nested_in: Optional["ZoneExit"] = None
 | |
| 
 | |
|     @property
 | |
|     def is_nested(self) -> bool:
 | |
|         """
 | |
|         Determine if this entrance is nested within another entrance.
 | |
| 
 | |
|         :return: `True` if the entrance is nested, `False` otherwise.
 | |
|         """
 | |
|         return self.nested_in is not None
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         """
 | |
|         Provide a string representation of the zone exit.
 | |
| 
 | |
|         :return: A string representing the zone exit.
 | |
|         """
 | |
|         return f"ZoneEntrance('{self.entrance_name}')"
 | |
| 
 | |
|     all: ClassVar[dict[str, "ZoneEntrance"]] = {}
 | |
| 
 | |
|     def __post_init__(self) -> None:
 | |
|         ZoneEntrance.all[self.entrance_name] = self
 | |
| 
 | |
|         # Must be an island entrance XOR must be a nested entrance.
 | |
|         assert (self.island_name is None) ^ (self.nested_in is None)
 | |
| 
 | |
| 
 | |
| @dataclass(frozen=True)
 | |
| class ZoneExit:
 | |
|     """
 | |
|     A data class that encapsulates information about a zone exit.
 | |
|     """
 | |
| 
 | |
|     unique_name: str
 | |
|     zone_name: Optional[str] = None
 | |
| 
 | |
|     def __repr__(self) -> str:
 | |
|         """
 | |
|         Provide a string representation of the zone exit.
 | |
| 
 | |
|         :return: A string representing the zone exit.
 | |
|         """
 | |
|         return f"ZoneExit('{self.unique_name}')"
 | |
| 
 | |
|     all: ClassVar[dict[str, "ZoneExit"]] = {}
 | |
| 
 | |
|     def __post_init__(self) -> None:
 | |
|         ZoneExit.all[self.unique_name] = self
 | |
| 
 | |
| 
 | |
| DUNGEON_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Dungeon Entrance on Dragon Roost Island", "Dragon Roost Island"),
 | |
|     ZoneEntrance("Dungeon Entrance in Forest Haven Sector", "Forest Haven"),
 | |
|     ZoneEntrance("Dungeon Entrance in Tower of the Gods Sector", "Tower of the Gods Sector"),
 | |
|     ZoneEntrance("Dungeon Entrance on Headstone Island", "Headstone Island"),
 | |
|     ZoneEntrance("Dungeon Entrance on Gale Isle", "Gale Isle"),
 | |
| ]
 | |
| DUNGEON_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Dragon Roost Cavern", "Dragon Roost Cavern"),
 | |
|     ZoneExit("Forbidden Woods", "Forbidden Woods"),
 | |
|     ZoneExit("Tower of the Gods", "Tower of the Gods"),
 | |
|     ZoneExit("Earth Temple", "Earth Temple"),
 | |
|     ZoneExit("Wind Temple", "Wind Temple"),
 | |
| ]
 | |
| 
 | |
| MINIBOSS_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Miniboss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]),
 | |
|     ZoneEntrance("Miniboss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]),
 | |
|     ZoneEntrance("Miniboss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]),
 | |
|     ZoneEntrance("Miniboss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]),
 | |
|     ZoneEntrance("Miniboss Entrance in Hyrule Castle", "Tower of the Gods Sector"),
 | |
| ]
 | |
| MINIBOSS_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Forbidden Woods Miniboss Arena"),
 | |
|     ZoneExit("Tower of the Gods Miniboss Arena"),
 | |
|     ZoneExit("Earth Temple Miniboss Arena"),
 | |
|     ZoneExit("Wind Temple Miniboss Arena"),
 | |
|     ZoneExit("Master Sword Chamber"),
 | |
| ]
 | |
| 
 | |
| BOSS_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Boss Entrance in Dragon Roost Cavern", nested_in=ZoneExit.all["Dragon Roost Cavern"]),
 | |
|     ZoneEntrance("Boss Entrance in Forbidden Woods", nested_in=ZoneExit.all["Forbidden Woods"]),
 | |
|     ZoneEntrance("Boss Entrance in Tower of the Gods", nested_in=ZoneExit.all["Tower of the Gods"]),
 | |
|     ZoneEntrance("Boss Entrance in Forsaken Fortress", "Forsaken Fortress Sector"),
 | |
|     ZoneEntrance("Boss Entrance in Earth Temple", nested_in=ZoneExit.all["Earth Temple"]),
 | |
|     ZoneEntrance("Boss Entrance in Wind Temple", nested_in=ZoneExit.all["Wind Temple"]),
 | |
| ]
 | |
| BOSS_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Gohma Boss Arena"),
 | |
|     ZoneExit("Kalle Demos Boss Arena"),
 | |
|     ZoneExit("Gohdan Boss Arena"),
 | |
|     ZoneExit("Helmaroc King Boss Arena"),
 | |
|     ZoneExit("Jalhalla Boss Arena"),
 | |
|     ZoneExit("Molgera Boss Arena"),
 | |
| ]
 | |
| 
 | |
| SECRET_CAVE_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Secret Cave Entrance on Outset Island", "Outset Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Dragon Roost Island", "Dragon Roost Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Fire Mountain", "Fire Mountain"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Ice Ring Isle", "Ice Ring Isle"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Private Oasis", "Private Oasis"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Needle Rock Isle", "Needle Rock Isle"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Angular Isles", "Angular Isles"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Boating Course", "Boating Course"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Stone Watcher Island", "Stone Watcher Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Overlook Island", "Overlook Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Bird's Peak Rock", "Bird's Peak Rock"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Pawprint Isle", "Pawprint Isle"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Pawprint Isle Side Isle", "Pawprint Isle"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Diamond Steppe Island", "Diamond Steppe Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Bomb Island", "Bomb Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Rock Spire Isle", "Rock Spire Isle"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Shark Island", "Shark Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Cliff Plateau Isles", "Cliff Plateau Isles"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Horseshoe Island", "Horseshoe Island"),
 | |
|     ZoneEntrance("Secret Cave Entrance on Star Island", "Star Island"),
 | |
| ]
 | |
| SECRET_CAVE_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Savage Labyrinth", zone_name="Outset Island"),
 | |
|     ZoneExit("Dragon Roost Island Secret Cave", zone_name="Dragon Roost Island"),
 | |
|     ZoneExit("Fire Mountain Secret Cave", zone_name="Fire Mountain"),
 | |
|     ZoneExit("Ice Ring Isle Secret Cave", zone_name="Ice Ring Isle"),
 | |
|     ZoneExit("Cabana Labyrinth", zone_name="Private Oasis"),
 | |
|     ZoneExit("Needle Rock Isle Secret Cave", zone_name="Needle Rock Isle"),
 | |
|     ZoneExit("Angular Isles Secret Cave", zone_name="Angular Isles"),
 | |
|     ZoneExit("Boating Course Secret Cave", zone_name="Boating Course"),
 | |
|     ZoneExit("Stone Watcher Island Secret Cave", zone_name="Stone Watcher Island"),
 | |
|     ZoneExit("Overlook Island Secret Cave", zone_name="Overlook Island"),
 | |
|     ZoneExit("Bird's Peak Rock Secret Cave", zone_name="Bird's Peak Rock"),
 | |
|     ZoneExit("Pawprint Isle Chuchu Cave", zone_name="Pawprint Isle"),
 | |
|     ZoneExit("Pawprint Isle Wizzrobe Cave"),
 | |
|     ZoneExit("Diamond Steppe Island Warp Maze Cave", zone_name="Diamond Steppe Island"),
 | |
|     ZoneExit("Bomb Island Secret Cave", zone_name="Bomb Island"),
 | |
|     ZoneExit("Rock Spire Isle Secret Cave", zone_name="Rock Spire Isle"),
 | |
|     ZoneExit("Shark Island Secret Cave", zone_name="Shark Island"),
 | |
|     ZoneExit("Cliff Plateau Isles Secret Cave", zone_name="Cliff Plateau Isles"),
 | |
|     ZoneExit("Horseshoe Island Secret Cave", zone_name="Horseshoe Island"),
 | |
|     ZoneExit("Star Island Secret Cave", zone_name="Star Island"),
 | |
| ]
 | |
| 
 | |
| SECRET_CAVE_INNER_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Inner Entrance in Ice Ring Isle Secret Cave", nested_in=ZoneExit.all["Ice Ring Isle Secret Cave"]),
 | |
|     ZoneEntrance(
 | |
|         "Inner Entrance in Cliff Plateau Isles Secret Cave", nested_in=ZoneExit.all["Cliff Plateau Isles Secret Cave"]
 | |
|     ),
 | |
| ]
 | |
| SECRET_CAVE_INNER_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Ice Ring Isle Inner Cave"),
 | |
|     ZoneExit("Cliff Plateau Isles Inner Cave"),
 | |
| ]
 | |
| 
 | |
| FAIRY_FOUNTAIN_ENTRANCES: list[ZoneEntrance] = [
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Outset Island", "Outset Island"),
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Thorned Fairy Island", "Thorned Fairy Island"),
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Eastern Fairy Island", "Eastern Fairy Island"),
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Western Fairy Island", "Western Fairy Island"),
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Southern Fairy Island", "Southern Fairy Island"),
 | |
|     ZoneEntrance("Fairy Fountain Entrance on Northern Fairy Island", "Northern Fairy Island"),
 | |
| ]
 | |
| FAIRY_FOUNTAIN_EXITS: list[ZoneExit] = [
 | |
|     ZoneExit("Outset Fairy Fountain"),
 | |
|     ZoneExit("Thorned Fairy Fountain", zone_name="Thorned Fairy Island"),
 | |
|     ZoneExit("Eastern Fairy Fountain", zone_name="Eastern Fairy Island"),
 | |
|     ZoneExit("Western Fairy Fountain", zone_name="Western Fairy Island"),
 | |
|     ZoneExit("Southern Fairy Fountain", zone_name="Southern Fairy Island"),
 | |
|     ZoneExit("Northern Fairy Fountain", zone_name="Northern Fairy Island"),
 | |
| ]
 | |
| 
 | |
| DUNGEON_INNER_EXITS: list[ZoneExit] = (
 | |
|     MINIBOSS_EXITS
 | |
|     + BOSS_EXITS
 | |
| )
 | |
| 
 | |
| ALL_ENTRANCES: list[ZoneEntrance] = (
 | |
|     DUNGEON_ENTRANCES
 | |
|     + MINIBOSS_ENTRANCES
 | |
|     + BOSS_ENTRANCES
 | |
|     + SECRET_CAVE_ENTRANCES
 | |
|     + SECRET_CAVE_INNER_ENTRANCES
 | |
|     + FAIRY_FOUNTAIN_ENTRANCES
 | |
| )
 | |
| ALL_EXITS: list[ZoneExit] = (
 | |
|     DUNGEON_EXITS
 | |
|     + MINIBOSS_EXITS
 | |
|     + BOSS_EXITS
 | |
|     + SECRET_CAVE_EXITS
 | |
|     + SECRET_CAVE_INNER_EXITS
 | |
|     + FAIRY_FOUNTAIN_EXITS
 | |
| )
 | |
| 
 | |
| ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES: list[TWWFlag] = [
 | |
|     TWWFlag.DUNGEON,
 | |
|     TWWFlag.PZL_CVE,
 | |
|     TWWFlag.CBT_CVE,
 | |
|     TWWFlag.SAVAGE,
 | |
|     TWWFlag.GRT_FRY,
 | |
| ]
 | |
| ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES: dict[str, ZoneExit] = {
 | |
|   "Forbidden Woods - Mothula Miniboss Room":           ZoneExit.all["Forbidden Woods Miniboss Arena"],
 | |
|   "Tower of the Gods - Darknut Miniboss Room":         ZoneExit.all["Tower of the Gods Miniboss Arena"],
 | |
|   "Earth Temple - Stalfos Miniboss Room":              ZoneExit.all["Earth Temple Miniboss Arena"],
 | |
|   "Wind Temple - Wizzrobe Miniboss Room":              ZoneExit.all["Wind Temple Miniboss Arena"],
 | |
|   "Hyrule - Master Sword Chamber":                     ZoneExit.all["Master Sword Chamber"],
 | |
| 
 | |
|   "Dragon Roost Cavern - Gohma Heart Container":       ZoneExit.all["Gohma Boss Arena"],
 | |
|   "Forbidden Woods - Kalle Demos Heart Container":     ZoneExit.all["Kalle Demos Boss Arena"],
 | |
|   "Tower of the Gods - Gohdan Heart Container":        ZoneExit.all["Gohdan Boss Arena"],
 | |
|   "Forsaken Fortress - Helmaroc King Heart Container": ZoneExit.all["Helmaroc King Boss Arena"],
 | |
|   "Earth Temple - Jalhalla Heart Container":           ZoneExit.all["Jalhalla Boss Arena"],
 | |
|   "Wind Temple - Molgera Heart Container":             ZoneExit.all["Molgera Boss Arena"],
 | |
| 
 | |
|   "Pawprint Isle - Wizzrobe Cave":                     ZoneExit.all["Pawprint Isle Wizzrobe Cave"],
 | |
| 
 | |
|   "Ice Ring Isle - Inner Cave - Chest":                ZoneExit.all["Ice Ring Isle Inner Cave"],
 | |
|   "Cliff Plateau Isles - Highest Isle":                ZoneExit.all["Cliff Plateau Isles Inner Cave"],
 | |
| 
 | |
|   "Outset Island - Great Fairy":                       ZoneExit.all["Outset Fairy Fountain"],
 | |
| }
 | |
| 
 | |
| MINIBOSS_EXIT_TO_DUNGEON: dict[str, str] = {
 | |
|     "Forbidden Woods Miniboss Arena":   "Forbidden Woods",
 | |
|     "Tower of the Gods Miniboss Arena": "Tower of the Gods",
 | |
|     "Earth Temple Miniboss Arena":      "Earth Temple",
 | |
|     "Wind Temple Miniboss Arena":       "Wind Temple",
 | |
| }
 | |
| 
 | |
| BOSS_EXIT_TO_DUNGEON: dict[str, str] = {
 | |
|     "Gohma Boss Arena":         "Dragon Roost Cavern",
 | |
|     "Kalle Demos Boss Arena":   "Forbidden Woods",
 | |
|     "Gohdan Boss Arena":        "Tower of the Gods",
 | |
|     "Helmaroc King Boss Arena": "Forsaken Fortress",
 | |
|     "Jalhalla Boss Arena":      "Earth Temple",
 | |
|     "Molgera Boss Arena":       "Wind Temple",
 | |
| }
 | |
| 
 | |
| VANILLA_ENTRANCES_TO_EXITS: dict[str, str] = {
 | |
|     "Dungeon Entrance on Dragon Roost Island": "Dragon Roost Cavern",
 | |
|     "Dungeon Entrance in Forest Haven Sector": "Forbidden Woods",
 | |
|     "Dungeon Entrance in Tower of the Gods Sector": "Tower of the Gods",
 | |
|     "Dungeon Entrance on Headstone Island": "Earth Temple",
 | |
|     "Dungeon Entrance on Gale Isle": "Wind Temple",
 | |
| 
 | |
|     "Miniboss Entrance in Forbidden Woods": "Forbidden Woods Miniboss Arena",
 | |
|     "Miniboss Entrance in Tower of the Gods": "Tower of the Gods Miniboss Arena",
 | |
|     "Miniboss Entrance in Earth Temple": "Earth Temple Miniboss Arena",
 | |
|     "Miniboss Entrance in Wind Temple": "Wind Temple Miniboss Arena",
 | |
|     "Miniboss Entrance in Hyrule Castle": "Master Sword Chamber",
 | |
| 
 | |
|     "Boss Entrance in Dragon Roost Cavern": "Gohma Boss Arena",
 | |
|     "Boss Entrance in Forbidden Woods": "Kalle Demos Boss Arena",
 | |
|     "Boss Entrance in Tower of the Gods": "Gohdan Boss Arena",
 | |
|     "Boss Entrance in Forsaken Fortress": "Helmaroc King Boss Arena",
 | |
|     "Boss Entrance in Earth Temple": "Jalhalla Boss Arena",
 | |
|     "Boss Entrance in Wind Temple": "Molgera Boss Arena",
 | |
| 
 | |
|     "Secret Cave Entrance on Outset Island": "Savage Labyrinth",
 | |
|     "Secret Cave Entrance on Dragon Roost Island": "Dragon Roost Island Secret Cave",
 | |
|     "Secret Cave Entrance on Fire Mountain": "Fire Mountain Secret Cave",
 | |
|     "Secret Cave Entrance on Ice Ring Isle": "Ice Ring Isle Secret Cave",
 | |
|     "Secret Cave Entrance on Private Oasis": "Cabana Labyrinth",
 | |
|     "Secret Cave Entrance on Needle Rock Isle": "Needle Rock Isle Secret Cave",
 | |
|     "Secret Cave Entrance on Angular Isles": "Angular Isles Secret Cave",
 | |
|     "Secret Cave Entrance on Boating Course": "Boating Course Secret Cave",
 | |
|     "Secret Cave Entrance on Stone Watcher Island": "Stone Watcher Island Secret Cave",
 | |
|     "Secret Cave Entrance on Overlook Island": "Overlook Island Secret Cave",
 | |
|     "Secret Cave Entrance on Bird's Peak Rock": "Bird's Peak Rock Secret Cave",
 | |
|     "Secret Cave Entrance on Pawprint Isle": "Pawprint Isle Chuchu Cave",
 | |
|     "Secret Cave Entrance on Pawprint Isle Side Isle": "Pawprint Isle Wizzrobe Cave",
 | |
|     "Secret Cave Entrance on Diamond Steppe Island": "Diamond Steppe Island Warp Maze Cave",
 | |
|     "Secret Cave Entrance on Bomb Island": "Bomb Island Secret Cave",
 | |
|     "Secret Cave Entrance on Rock Spire Isle": "Rock Spire Isle Secret Cave",
 | |
|     "Secret Cave Entrance on Shark Island": "Shark Island Secret Cave",
 | |
|     "Secret Cave Entrance on Cliff Plateau Isles": "Cliff Plateau Isles Secret Cave",
 | |
|     "Secret Cave Entrance on Horseshoe Island": "Horseshoe Island Secret Cave",
 | |
|     "Secret Cave Entrance on Star Island": "Star Island Secret Cave",
 | |
| 
 | |
|     "Inner Entrance in Ice Ring Isle Secret Cave": "Ice Ring Isle Inner Cave",
 | |
|     "Inner Entrance in Cliff Plateau Isles Secret Cave": "Cliff Plateau Isles Inner Cave",
 | |
| 
 | |
|     "Fairy Fountain Entrance on Outset Island": "Outset Fairy Fountain",
 | |
|     "Fairy Fountain Entrance on Thorned Fairy Island": "Thorned Fairy Fountain",
 | |
|     "Fairy Fountain Entrance on Eastern Fairy Island": "Eastern Fairy Fountain",
 | |
|     "Fairy Fountain Entrance on Western Fairy Island": "Western Fairy Fountain",
 | |
|     "Fairy Fountain Entrance on Southern Fairy Island": "Southern Fairy Fountain",
 | |
|     "Fairy Fountain Entrance on Northern Fairy Island": "Northern Fairy Fountain",
 | |
| }
 | |
| 
 | |
| 
 | |
| class EntranceRandomizer:
 | |
|     """
 | |
|     This class handles the logic for The Wind Waker entrance randomizer.
 | |
| 
 | |
|     We reference the logic from the base randomizer with some modifications to suit it for Archipelago.
 | |
|     Reference: https://github.com/LagoLunatic/wwrando/blob/master/randomizers/entrances.py
 | |
| 
 | |
|     :param world: The Wind Waker game world.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, world: "TWWWorld"):
 | |
|         self.world = world
 | |
|         self.multiworld = world.multiworld
 | |
|         self.player = world.player
 | |
| 
 | |
|         self.item_location_to_containing_zone_exit: dict[str, ZoneExit] = {}
 | |
|         self.zone_exit_to_logically_dependent_item_locations: dict[ZoneExit, list[str]] = defaultdict(list)
 | |
|         self.register_mappings_between_item_locations_and_zone_exits()
 | |
| 
 | |
|         self.done_entrances_to_exits: dict[ZoneEntrance, ZoneExit] = {}
 | |
|         self.done_exits_to_entrances: dict[ZoneExit, ZoneEntrance] = {}
 | |
| 
 | |
|         for entrance_name, exit_name in VANILLA_ENTRANCES_TO_EXITS.items():
 | |
|             zone_entrance = ZoneEntrance.all[entrance_name]
 | |
|             zone_exit = ZoneExit.all[exit_name]
 | |
|             self.done_entrances_to_exits[zone_entrance] = zone_exit
 | |
|             self.done_exits_to_entrances[zone_exit] = zone_entrance
 | |
| 
 | |
|         self.banned_exits: list[ZoneExit] = []
 | |
|         self.islands_with_a_banned_dungeon: set[str] = set()
 | |
| 
 | |
|     def randomize_entrances(self) -> None:
 | |
|         """
 | |
|         Randomize entrances for The Wind Waker.
 | |
|         """
 | |
|         self.init_banned_exits()
 | |
| 
 | |
|         for relevant_entrances, relevant_exits in self.get_all_entrance_sets_to_be_randomized():
 | |
|             self.randomize_one_set_of_entrances(relevant_entrances, relevant_exits)
 | |
| 
 | |
|         self.finalize_all_randomized_sets_of_entrances()
 | |
| 
 | |
|     def init_banned_exits(self) -> None:
 | |
|         """
 | |
|         Initialize the list of banned exits for the randomizer.
 | |
| 
 | |
|         Dungeon exits in banned dungeons should be prohibited from being randomized.
 | |
|         Additionally, if dungeon entrances are not randomized, we can now note which island holds these banned dungeons.
 | |
|         """
 | |
|         options = self.world.options
 | |
| 
 | |
|         if options.required_bosses:
 | |
|             for zone_exit in BOSS_EXITS:
 | |
|                 assert zone_exit.unique_name.endswith(" Boss Arena")
 | |
|                 boss_name = zone_exit.unique_name.removesuffix(" Boss Arena")
 | |
|                 if boss_name in self.world.boss_reqs.banned_bosses:
 | |
|                     self.banned_exits.append(zone_exit)
 | |
|             for zone_exit in DUNGEON_EXITS:
 | |
|                 dungeon_name = zone_exit.unique_name
 | |
|                 if dungeon_name in self.world.boss_reqs.banned_dungeons:
 | |
|                     self.banned_exits.append(zone_exit)
 | |
|             for zone_exit in MINIBOSS_EXITS:
 | |
|                 if zone_exit == ZoneExit.all["Master Sword Chamber"]:
 | |
|                     # Hyrule cannot be chosen as a banned dungeon.
 | |
|                     continue
 | |
|                 assert zone_exit.unique_name.endswith(" Miniboss Arena")
 | |
|                 dungeon_name = zone_exit.unique_name.removesuffix(" Miniboss Arena")
 | |
|                 if dungeon_name in self.world.boss_reqs.banned_dungeons:
 | |
|                     self.banned_exits.append(zone_exit)
 | |
| 
 | |
|         if not options.randomize_dungeon_entrances:
 | |
|             # If dungeon entrances are not randomized, `islands_with_a_banned_dungeon` can be initialized early since
 | |
|             # it's preset and won't be updated later since we won't randomize the dungeon entrances.
 | |
|             for en in DUNGEON_ENTRANCES:
 | |
|                 if self.done_entrances_to_exits[en].unique_name in self.world.boss_reqs.banned_dungeons:
 | |
|                     assert en.island_name is not None
 | |
|                     self.islands_with_a_banned_dungeon.add(en.island_name)
 | |
| 
 | |
|     def randomize_one_set_of_entrances(
 | |
|         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit]
 | |
|     ) -> None:
 | |
|         """
 | |
|         Randomize a single set of entrances and their corresponding exits.
 | |
| 
 | |
|         :param relevant_entrances: A list of entrances to be randomized.
 | |
|         :param relevant_exits: A list of exits corresponding to the entrances.
 | |
|         """
 | |
|         # Keep miniboss and boss entrances vanilla in non-required bosses' dungeons.
 | |
|         for zone_entrance in relevant_entrances.copy():
 | |
|             zone_exit = self.done_entrances_to_exits[zone_entrance]
 | |
|             if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS:
 | |
|                 relevant_entrances.remove(zone_entrance)
 | |
|             else:
 | |
|                 del self.done_entrances_to_exits[zone_entrance]
 | |
|         for zone_exit in relevant_exits.copy():
 | |
|             if zone_exit in self.banned_exits and zone_exit in DUNGEON_INNER_EXITS:
 | |
|                 relevant_exits.remove(zone_exit)
 | |
|             else:
 | |
|                 del self.done_exits_to_entrances[zone_exit]
 | |
| 
 | |
|         self.multiworld.random.shuffle(relevant_entrances)
 | |
| 
 | |
|         # We calculate which exits are terminal (the end of a nested chain) per set instead of for all entrances.
 | |
|         # This is so that, for example, Ice Ring Isle counts as terminal when its inner cave is not being randomized.
 | |
|         non_terminal_exits = []
 | |
|         for en in relevant_entrances:
 | |
|             if en.nested_in is not None and en.nested_in not in non_terminal_exits:
 | |
|                 non_terminal_exits.append(en.nested_in)
 | |
|         terminal_exits = {ex for ex in relevant_exits if ex not in non_terminal_exits}
 | |
| 
 | |
|         remaining_entrances = relevant_entrances.copy()
 | |
|         remaining_exits = relevant_exits.copy()
 | |
| 
 | |
|         nonprogress_entrances, nonprogress_exits = self.split_nonprogress_entrances_and_exits(
 | |
|             remaining_entrances, remaining_exits
 | |
|         )
 | |
|         if nonprogress_entrances:
 | |
|             for en in nonprogress_entrances:
 | |
|                 remaining_entrances.remove(en)
 | |
|             for ex in nonprogress_exits:
 | |
|                 remaining_exits.remove(ex)
 | |
|             self.randomize_one_set_of_exits(nonprogress_entrances, nonprogress_exits, terminal_exits)
 | |
| 
 | |
|         self.randomize_one_set_of_exits(remaining_entrances, remaining_exits, terminal_exits)
 | |
| 
 | |
|     def check_if_one_exit_is_progress(self, zone_exit: ZoneExit) -> bool:
 | |
|         """
 | |
|         Determine if the zone exit leads to progress locations in the world.
 | |
| 
 | |
|         :param zone_exit: The zone exit to check.
 | |
|         :return: Whether the zone exit leads to progress locations.
 | |
|         """
 | |
|         locs_for_exit = self.zone_exit_to_logically_dependent_item_locations[zone_exit]
 | |
|         assert locs_for_exit, f"Could not find any item locations corresponding to zone exit: {zone_exit.unique_name}"
 | |
| 
 | |
|         # Banned required bosses mode dungeons still technically count as progress locations, so filter them out
 | |
|         # separately first.
 | |
|         nonbanned_locs = [loc for loc in locs_for_exit if loc not in self.world.boss_reqs.banned_locations]
 | |
|         progress_locs = [loc for loc in nonbanned_locs if loc not in self.world.nonprogress_locations]
 | |
|         return bool(progress_locs)
 | |
| 
 | |
|     def split_nonprogress_entrances_and_exits(
 | |
|         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit]
 | |
|     ) -> tuple[list[ZoneEntrance], list[ZoneExit]]:
 | |
|         """
 | |
|         Splits the entrance and exit lists into two pairs: ones that should be considered nonprogress on this seed (will
 | |
|         never lead to any progress items) and ones that should be regarded as potentially required.
 | |
| 
 | |
|         This is so we can effectively randomize these two pairs separately without convoluted logic to ensure they don't
 | |
|         connect.
 | |
| 
 | |
|         :param relevant_entrances: A list of entrances.
 | |
|         :param relevant_exits: A list of exits corresponding to the entrances.
 | |
|         :raises FillError: If the number of randomizable entrances does not equal the number of randomizable exits.
 | |
|         """
 | |
|         nonprogress_exits = [ex for ex in relevant_exits if not self.check_if_one_exit_is_progress(ex)]
 | |
|         nonprogress_entrances = [
 | |
|             en
 | |
|             for en in relevant_entrances
 | |
|             if en.nested_in is not None
 | |
|             and (
 | |
|                 (en.nested_in in nonprogress_exits)
 | |
|                 # The area this entrance is nested in is not randomized, but we still need to determine whether it's
 | |
|                 # progression.
 | |
|                 or (en.nested_in not in relevant_exits and not self.check_if_one_exit_is_progress(en.nested_in))
 | |
|             )
 | |
|         ]
 | |
| 
 | |
|         # At this point, `nonprogress_entrances` includes only the inner entrances nested inside the main exits, not any
 | |
|         # island entrances on the sea. So, we need to select `N` random island entrances to allow all of the nonprogress
 | |
|         # exits to be accessible, where `N` is the difference between the number of entrances and exits we currently
 | |
|         # have.
 | |
|         possible_island_entrances = [en for en in relevant_entrances if en.island_name is not None]
 | |
| 
 | |
|         # We need special logic to handle Forsaken Fortress, as it is the only island entrance inside a dungeon.
 | |
|         ff_boss_entrance = ZoneEntrance.all["Boss Entrance in Forsaken Fortress"]
 | |
|         if ff_boss_entrance in possible_island_entrances:
 | |
|             if self.world.options.progression_dungeons:
 | |
|                 if "Forsaken Fortress" in self.world.boss_reqs.banned_dungeons:
 | |
|                     ff_progress = False
 | |
|                 else:
 | |
|                     ff_progress = True
 | |
|             else:
 | |
|                 ff_progress = False
 | |
| 
 | |
|             if ff_progress:
 | |
|                 # If it's progress, don't allow it to be randomly chosen to lead to nonprogress exits.
 | |
|                 possible_island_entrances.remove(ff_boss_entrance)
 | |
|             else:
 | |
|                 # If it's not progress, manually mark it as such, and don't allow it to be chosen randomly.
 | |
|                 nonprogress_entrances.append(ff_boss_entrance)
 | |
|                 possible_island_entrances.remove(ff_boss_entrance)
 | |
| 
 | |
|         num_island_entrances_needed = len(nonprogress_exits) - len(nonprogress_entrances)
 | |
|         if num_island_entrances_needed > len(possible_island_entrances):
 | |
|             raise FillError("Not enough island entrances left to split entrances.")
 | |
| 
 | |
|         for _ in range(num_island_entrances_needed):
 | |
|             # Note: `relevant_entrances` is already shuffled, so we can just take the first result from
 | |
|             # `possible_island_entrances`—it's the same as picking one randomly.
 | |
|             nonprogress_island_entrance = possible_island_entrances.pop(0)
 | |
|             nonprogress_entrances.append(nonprogress_island_entrance)
 | |
| 
 | |
|         assert len(nonprogress_entrances) == len(nonprogress_exits)
 | |
| 
 | |
|         return nonprogress_entrances, nonprogress_exits
 | |
| 
 | |
|     def randomize_one_set_of_exits(
 | |
|         self, relevant_entrances: list[ZoneEntrance], relevant_exits: list[ZoneExit], terminal_exits: set[ZoneExit]
 | |
|     ) -> None:
 | |
|         """
 | |
|         Randomize a single set of entrances and their corresponding exits.
 | |
| 
 | |
|         :param relevant_entrances: A list of entrances to be randomized.
 | |
|         :param relevant_exits: A list of exits corresponding to the entrances.
 | |
|         :param terminal_exits: A set of exits which do not contain any entrances.
 | |
|         :raises FillError: If there are no valid exits to assign to an entrance.
 | |
|         """
 | |
|         options = self.world.options
 | |
| 
 | |
|         remaining_entrances = relevant_entrances.copy()
 | |
|         remaining_exits = relevant_exits.copy()
 | |
| 
 | |
|         doing_banned = False
 | |
|         if any(ex in self.banned_exits for ex in relevant_exits):
 | |
|             doing_banned = True
 | |
| 
 | |
|         if options.required_bosses and not doing_banned:
 | |
|             # Prioritize entrances that share an island with an entrance randomized to lead into a
 | |
|             # required-bosses-mode-banned dungeon. (e.g., DRI, Pawprint, Outset, TotG sector.)
 | |
|             # This is because we need to prevent these islands from having a required boss or anything that could lead
 | |
|             # to a required boss. If we don't do this first, we can get backed into a corner where there is no other
 | |
|             # option left.
 | |
|             entrances_not_on_unique_islands = []
 | |
|             for zone_entrance in relevant_entrances:
 | |
|                 if zone_entrance.is_nested:
 | |
|                     continue
 | |
|                 if zone_entrance.island_name in self.islands_with_a_banned_dungeon:
 | |
|                     # This island was already used on a previous call to `randomize_one_set_of_exits`.
 | |
|                     entrances_not_on_unique_islands.append(zone_entrance)
 | |
|                     continue
 | |
|             for zone_entrance in entrances_not_on_unique_islands:
 | |
|                 remaining_entrances.remove(zone_entrance)
 | |
|             remaining_entrances = entrances_not_on_unique_islands + remaining_entrances
 | |
| 
 | |
|         while remaining_entrances:
 | |
|             # Filter out boss entrances that aren't yet accessible from the sea.
 | |
|             # We don't want to connect these to anything yet or we risk creating an infinite loop.
 | |
|             possible_remaining_entrances = [
 | |
|                 en for en in remaining_entrances if self.get_outermost_entrance_for_entrance(en) is not None
 | |
|             ]
 | |
|             zone_entrance = possible_remaining_entrances.pop(0)
 | |
|             remaining_entrances.remove(zone_entrance)
 | |
| 
 | |
|             possible_remaining_exits = remaining_exits.copy()
 | |
| 
 | |
|             if len(possible_remaining_entrances) == 0 and len(remaining_entrances) > 0:
 | |
|                 # If this is the last entrance we have left to attach exits to, we can't place a terminal exit here.
 | |
|                 # Terminal exits do not create another entrance, so one would leave us with no possible way to continue
 | |
|                 # placing the remaining exits on future loops.
 | |
|                 possible_remaining_exits = [ex for ex in possible_remaining_exits if ex not in terminal_exits]
 | |
| 
 | |
|             if options.required_bosses and zone_entrance.island_name is not None and not doing_banned:
 | |
|                 # Prevent required bosses (and non-terminal exits, which could lead to required bosses) from appearing
 | |
|                 # on islands where we already placed a banned boss or dungeon.
 | |
|                 # This can happen with DRI and Pawprint, as these islands have two entrances. This would be bad because
 | |
|                 # the required bosses mode's dungeon markers only tell you what island the required dungeons are on, not
 | |
|                 # which of the two entrances to enter.
 | |
|                 # So, if a banned dungeon is placed on DRI's main entrance, we will have to fill DRI's pit entrance with
 | |
|                 # either a miniboss or one of the caves that does not have a nested entrance inside. We allow multiple
 | |
|                 # banned and required dungeons on a single island.
 | |
|                 if zone_entrance.island_name in self.islands_with_a_banned_dungeon:
 | |
|                     possible_remaining_exits = [
 | |
|                         ex
 | |
|                         for ex in possible_remaining_exits
 | |
|                         if ex in terminal_exits and ex not in (DUNGEON_EXITS + BOSS_EXITS)
 | |
|                     ]
 | |
| 
 | |
|             if not possible_remaining_exits:
 | |
|                 raise FillError(f"No valid exits to place for entrance: {zone_entrance.entrance_name}")
 | |
| 
 | |
|             zone_exit = self.multiworld.random.choice(possible_remaining_exits)
 | |
|             remaining_exits.remove(zone_exit)
 | |
| 
 | |
|             self.done_entrances_to_exits[zone_entrance] = zone_exit
 | |
|             self.done_exits_to_entrances[zone_exit] = zone_entrance
 | |
| 
 | |
|             if zone_exit in self.banned_exits:
 | |
|                 # Keep track of which islands have a required bosses mode banned dungeon to avoid marker overlap.
 | |
|                 if zone_exit in DUNGEON_EXITS + BOSS_EXITS:
 | |
|                     # We only keep track of dungeon exits and boss exits, not miniboss exits.
 | |
|                     # Banned miniboss exits can share an island with required dungeons/bosses.
 | |
|                     outer_entrance = self.get_outermost_entrance_for_entrance(zone_entrance)
 | |
| 
 | |
|                     # Because we filter above so that we always assign entrances from the sea inwards, we can assume
 | |
|                     # that when we assign an entrance, it has a path back to the sea.
 | |
|                     # If we're assigning a non-terminal entrance, any nested entrances will get assigned after this one,
 | |
|                     # and we'll run through this code again (so we can reason based on `zone_exit` only instead of
 | |
|                     # having to recurse through the nested exits to find banned dungeons/bosses).
 | |
|                     assert outer_entrance and outer_entrance.island_name is not None
 | |
|                     self.islands_with_a_banned_dungeon.add(outer_entrance.island_name)
 | |
| 
 | |
|     def finalize_all_randomized_sets_of_entrances(self) -> None:
 | |
|         """
 | |
|         Finalize all randomized entrance sets.
 | |
| 
 | |
|         For all entrance-exit pairs, this function adds a connection with the appropriate access rule to the world.
 | |
|         """
 | |
| 
 | |
|         def get_access_rule(entrance: ZoneEntrance) -> str:
 | |
|             snake_case_region = entrance.entrance_name.lower().replace("'", "").replace(" ", "_")
 | |
|             return getattr(Macros, f"can_access_{snake_case_region}")
 | |
| 
 | |
|         # Connect each entrance-exit pair in the multiworld with the access rule for the entrance.
 | |
|         # The Great Sea is the parent_region for many entrances, so get it in advance.
 | |
|         great_sea_region = self.world.get_region("The Great Sea")
 | |
|         for zone_entrance, zone_exit in self.done_entrances_to_exits.items():
 | |
|             # Get the parent region of the entrance.
 | |
|             if zone_entrance.island_name is not None:
 | |
|                 # Entrances with an `island_name` are found in The Great Sea.
 | |
|                 parent_region = great_sea_region
 | |
|             else:
 | |
|                 # All other entrances must be nested within some other region.
 | |
|                 parent_region = self.world.get_region(zone_entrance.nested_in.unique_name)
 | |
|             exit_region_name = zone_exit.unique_name
 | |
|             exit_region = self.world.get_region(exit_region_name)
 | |
|             parent_region.connect(
 | |
|                 exit_region,
 | |
|                 # The default name uses the "parent_region -> connecting_region", but the parent_region would not be
 | |
|                 # useful for spoiler paths or debugging, so use the entrance name at the start.
 | |
|                 f"{zone_entrance.entrance_name} -> {exit_region_name}",
 | |
|                 rule=lambda state, rule=get_access_rule(zone_entrance): rule(state, self.player),
 | |
|             )
 | |
| 
 | |
|         if __debug__ and self.world.options.required_bosses:
 | |
|             # Ensure we didn't accidentally place a banned boss and a required boss on the same island.
 | |
|             banned_island_names = set(
 | |
|                 self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.banned_bosses
 | |
|             )
 | |
|             required_island_names = set(
 | |
|                 self.get_entrance_zone_for_boss(boss_name) for boss_name in self.world.boss_reqs.required_bosses
 | |
|             )
 | |
|             assert not banned_island_names & required_island_names
 | |
| 
 | |
|     def register_mappings_between_item_locations_and_zone_exits(self) -> None:
 | |
|         """
 | |
|         Map item locations to their corresponding zone exits.
 | |
|         """
 | |
|         for loc_name in list(LOCATION_TABLE.keys()):
 | |
|             zone_exit = self.get_zone_exit_for_item_location(loc_name)
 | |
|             if zone_exit is not None:
 | |
|                 self.item_location_to_containing_zone_exit[loc_name] = zone_exit
 | |
|                 self.zone_exit_to_logically_dependent_item_locations[zone_exit].append(loc_name)
 | |
| 
 | |
|             if loc_name == "The Great Sea - Withered Trees":
 | |
|                 # This location isn't inside a zone exit, but it does logically require the player to be able to reach
 | |
|                 # a different item location inside one.
 | |
|                 sub_zone_exit = self.get_zone_exit_for_item_location("Cliff Plateau Isles - Highest Isle")
 | |
|                 if sub_zone_exit is not None:
 | |
|                     self.zone_exit_to_logically_dependent_item_locations[sub_zone_exit].append(loc_name)
 | |
| 
 | |
|     def get_all_entrance_sets_to_be_randomized(
 | |
|         self,
 | |
|     ) -> Generator[tuple[list[ZoneEntrance], list[ZoneExit]], None, None]:
 | |
|         """
 | |
|         Retrieve all entrance-exit pairs that need to be randomized.
 | |
| 
 | |
|         :raises OptionError: If an invalid randomization option is set in the world's options.
 | |
|         :return: A generator that yields sets of entrances and exits to be randomized.
 | |
|         """
 | |
|         options = self.world.options
 | |
| 
 | |
|         dungeons = bool(options.randomize_dungeon_entrances)
 | |
|         minibosses = bool(options.randomize_miniboss_entrances)
 | |
|         bosses = bool(options.randomize_boss_entrances)
 | |
|         secret_caves = bool(options.randomize_secret_cave_entrances)
 | |
|         inner_caves = bool(options.randomize_secret_cave_inner_entrances)
 | |
|         fountains = bool(options.randomize_fairy_fountain_entrances)
 | |
| 
 | |
|         mix_entrances = options.mix_entrances
 | |
|         if mix_entrances == "separate_pools":
 | |
|             if dungeons:
 | |
|                 yield self.get_one_entrance_set(dungeons=dungeons)
 | |
|             if minibosses:
 | |
|                 yield self.get_one_entrance_set(minibosses=minibosses)
 | |
|             if bosses:
 | |
|                 yield self.get_one_entrance_set(bosses=bosses)
 | |
|             if secret_caves:
 | |
|                 yield self.get_one_entrance_set(caves=secret_caves)
 | |
|             if inner_caves:
 | |
|                 yield self.get_one_entrance_set(inner_caves=inner_caves)
 | |
|             if fountains:
 | |
|                 yield self.get_one_entrance_set(fountains=fountains)
 | |
|         elif mix_entrances == "mix_pools":
 | |
|             yield self.get_one_entrance_set(
 | |
|                 dungeons=dungeons,
 | |
|                 minibosses=minibosses,
 | |
|                 bosses=bosses,
 | |
|                 caves=secret_caves,
 | |
|                 inner_caves=inner_caves,
 | |
|                 fountains=fountains,
 | |
|             )
 | |
|         else:
 | |
|             raise OptionError(f"Invalid entrance randomization option: {mix_entrances}")
 | |
| 
 | |
|     def get_one_entrance_set(
 | |
|         self,
 | |
|         *,
 | |
|         dungeons: bool = False,
 | |
|         caves: bool = False,
 | |
|         minibosses: bool = False,
 | |
|         bosses: bool = False,
 | |
|         inner_caves: bool = False,
 | |
|         fountains: bool = False,
 | |
|     ) -> tuple[list[ZoneEntrance], list[ZoneExit]]:
 | |
|         """
 | |
|         Retrieve a single set of entrance-exit pairs that need to be randomized.
 | |
| 
 | |
|         :param dungeons: Whether to include dungeon entrances and exits. Defaults to `False`.
 | |
|         :param caves: Whether to include secret cave entrances and exits. Defaults to `False`.
 | |
|         :param minibosses: Whether to include miniboss entrances and exits. Defaults to `False`.
 | |
|         :param bosses: Whether to include boss entrances and exits. Defaults to `False`.
 | |
|         :param inner_caves: Whether to include inner cave entrances and exits. Defaults to `False`.
 | |
|         :param fountains: Whether to include fairy fountain entrances and exits. Defaults to `False`.
 | |
|         :return: A tuple of lists of entrances and exits that should be randomized together.
 | |
|         """
 | |
|         relevant_entrances: list[ZoneEntrance] = []
 | |
|         relevant_exits: list[ZoneExit] = []
 | |
|         if dungeons:
 | |
|             relevant_entrances += DUNGEON_ENTRANCES
 | |
|             relevant_exits += DUNGEON_EXITS
 | |
|         if minibosses:
 | |
|             relevant_entrances += MINIBOSS_ENTRANCES
 | |
|             relevant_exits += MINIBOSS_EXITS
 | |
|         if bosses:
 | |
|             relevant_entrances += BOSS_ENTRANCES
 | |
|             relevant_exits += BOSS_EXITS
 | |
|         if caves:
 | |
|             relevant_entrances += SECRET_CAVE_ENTRANCES
 | |
|             relevant_exits += SECRET_CAVE_EXITS
 | |
|         if inner_caves:
 | |
|             relevant_entrances += SECRET_CAVE_INNER_ENTRANCES
 | |
|             relevant_exits += SECRET_CAVE_INNER_EXITS
 | |
|         if fountains:
 | |
|             relevant_entrances += FAIRY_FOUNTAIN_ENTRANCES
 | |
|             relevant_exits += FAIRY_FOUNTAIN_EXITS
 | |
|         return relevant_entrances, relevant_exits
 | |
| 
 | |
|     def get_outermost_entrance_for_exit(self, zone_exit: ZoneExit) -> Optional[ZoneEntrance]:
 | |
|         """
 | |
|         Unrecurses nested dungeons to determine a given exit's outermost (island) entrance.
 | |
| 
 | |
|         :param zone_exit: The given exit.
 | |
|         :return: The outermost (island) entrance for the exit, or `None` if entrances have yet to be randomized.
 | |
|         """
 | |
|         zone_entrance = self.done_exits_to_entrances[zone_exit]
 | |
|         return self.get_outermost_entrance_for_entrance(zone_entrance)
 | |
| 
 | |
|     def get_outermost_entrance_for_entrance(self, zone_entrance: ZoneEntrance) -> Optional[ZoneEntrance]:
 | |
|         """
 | |
|         Unrecurses nested dungeons to determine a given entrance's outermost (island) entrance.
 | |
| 
 | |
|         :param zone_exit: The given entrance.
 | |
|         :return: The outermost (island) entrance for the entrance, or `None` if entrances have yet to be randomized.
 | |
|         """
 | |
|         seen_entrances = self.get_all_entrances_on_path_to_entrance(zone_entrance)
 | |
|         if seen_entrances is None:
 | |
|             # Undecided.
 | |
|             return None
 | |
|         outermost_entrance = seen_entrances[-1]
 | |
|         return outermost_entrance
 | |
| 
 | |
|     def get_all_entrances_on_path_to_entrance(self, zone_entrance: ZoneEntrance) -> Optional[list[ZoneEntrance]]:
 | |
|         """
 | |
|         Unrecurses nested dungeons to build a list of all entrances leading to a given entrance.
 | |
| 
 | |
|         :param zone_exit: The given entrance.
 | |
|         :return: A list of entrances leading to the given entrance, or `None` if entrances have yet to be randomized.
 | |
|         """
 | |
|         seen_entrances: list[ZoneEntrance] = []
 | |
|         while zone_entrance.is_nested:
 | |
|             if zone_entrance in seen_entrances:
 | |
|                 path_str = ", ".join([e.entrance_name for e in seen_entrances])
 | |
|                 raise FillError(f"Entrances are in an infinite loop: {path_str}")
 | |
|             seen_entrances.append(zone_entrance)
 | |
|             if zone_entrance.nested_in not in self.done_exits_to_entrances:
 | |
|                 # Undecided.
 | |
|                 return None
 | |
|             zone_entrance = self.done_exits_to_entrances[zone_entrance.nested_in]
 | |
|         seen_entrances.append(zone_entrance)
 | |
|         return seen_entrances
 | |
| 
 | |
|     def is_item_location_behind_randomizable_entrance(self, location_name: str) -> bool:
 | |
|         """
 | |
|         Determine if the location is behind a randomizable entrance.
 | |
| 
 | |
|         :param location_name: The location to check.
 | |
|         :return: `True` if the location is behind a randomizable entrance, `False` otherwise.
 | |
|         """
 | |
|         loc_zone_name, _ = split_location_name_by_zone(location_name)
 | |
|         if loc_zone_name in ["Ganon's Tower", "Mailbox"]:
 | |
|             # Ganon's Tower and the handful of Mailbox locations that depend on beating dungeon bosses are considered
 | |
|             # "Dungeon" location types by the logic, but the entrance randomizer does not need to consider them.
 | |
|             # Although the mail locations are technically locked behind dungeons, we can still ignore them here because
 | |
|             # if all of the locations in the dungeon itself are nonprogress, then any mail depending on that dungeon
 | |
|             # should also be enforced as nonprogress by other parts of the code.
 | |
|             return False
 | |
| 
 | |
|         types = LOCATION_TABLE[location_name].flags
 | |
|         is_boss = TWWFlag.BOSS in types
 | |
|         if loc_zone_name == "Forsaken Fortress" and not is_boss:
 | |
|             # Special case. FF is a dungeon that is not randomized, except for the boss arena.
 | |
|             return False
 | |
| 
 | |
|         is_big_octo = TWWFlag.BG_OCTO in types
 | |
|         if is_big_octo:
 | |
|             # The Big Octo Great Fairy is the only Great Fairy location that is not also a Fairy Fountain.
 | |
|             return False
 | |
| 
 | |
|         # In the general case, we check if the location has a type corresponding to exits that can be randomized.
 | |
|         if any(t in types for t in ENTRANCE_RANDOMIZABLE_ITEM_LOCATION_TYPES):
 | |
|             return True
 | |
| 
 | |
|         return False
 | |
| 
 | |
|     def get_zone_exit_for_item_location(self, location_name: str) -> Optional[ZoneExit]:
 | |
|         """
 | |
|         Retrieve the zone exit for a given location.
 | |
| 
 | |
|         :param location_name: The name of the location.
 | |
|         :raises Exception: If a location exit override should be used instead.
 | |
|         :return: The zone exit for the location or `None` if the location is not behind a randomizable entrance.
 | |
|         """
 | |
|         if not self.is_item_location_behind_randomizable_entrance(location_name):
 | |
|             return None
 | |
| 
 | |
|         zone_exit = ITEM_LOCATION_NAME_TO_EXIT_OVERRIDES.get(location_name, None)
 | |
|         if zone_exit is not None:
 | |
|             return zone_exit
 | |
| 
 | |
|         loc_zone_name, _ = split_location_name_by_zone(location_name)
 | |
|         possible_exits = [ex for ex in ZoneExit.all.values() if ex.zone_name == loc_zone_name]
 | |
|         if len(possible_exits) == 0:
 | |
|             return None
 | |
|         elif len(possible_exits) == 1:
 | |
|             return possible_exits[0]
 | |
|         else:
 | |
|             raise Exception(
 | |
|                 f"Multiple zone exits share the same zone name: {loc_zone_name!r}. "
 | |
|                 "Use a location exit override instead."
 | |
|             )
 | |
| 
 | |
|     def get_entrance_zone_for_boss(self, boss_name: str) -> str:
 | |
|         """
 | |
|         Retrieve the entrance zone for a given boss.
 | |
| 
 | |
|         :param boss_name: The name of the boss.
 | |
|         :return: The name of the island on which the boss is located.
 | |
|         """
 | |
|         boss_arena_name = f"{boss_name} Boss Arena"
 | |
|         zone_exit = ZoneExit.all[boss_arena_name]
 | |
|         outermost_entrance = self.get_outermost_entrance_for_exit(zone_exit)
 | |
|         assert outermost_entrance is not None and outermost_entrance.island_name is not None
 | |
|         return outermost_entrance.island_name
 |