diff --git a/.gitignore b/.gitignore
index adf8cf53..829090ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,9 +5,12 @@
*.bmbp
*.apbp
*.apmc
+*.apz5
*.pyc
*.pyd
*.sfc
+*.z64
+*.n64
*.wixobj
*.lck
*.db3
diff --git a/BaseClasses.py b/BaseClasses.py
index 55f88efe..ba14dcda 100644
--- a/BaseClasses.py
+++ b/BaseClasses.py
@@ -888,6 +888,8 @@ class Location():
locked: bool = False
spot_type = 'Location'
game: str = "Generic"
+ show_in_spoiler: bool = True
+ excluded: bool = False
crystal: bool = False
always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True)
@@ -1031,24 +1033,24 @@ class Spoiler():
self.locations = OrderedDict()
listed_locations = set()
- lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld]
+ lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
listed_locations.update(lw_locations)
- dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld]
+ dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
listed_locations.update(dw_locations)
- cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave]
+ cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
listed_locations.update(cave_locations)
for dungeon in self.world.dungeons.values():
- dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon]
+ dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
listed_locations.update(dungeon_locations)
- other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations]
+ other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.show_in_spoiler]
if other_locations:
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
listed_locations.update(other_locations)
diff --git a/Generate.py b/Generate.py
index 927c9e0e..211d6f0a 100644
--- a/Generate.py
+++ b/Generate.py
@@ -501,7 +501,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
if option_name in game_weights:
try:
- if issubclass(option, Options.OptionDict):
+ if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList):
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
diff --git a/Options.py b/Options.py
index fbb19d88..27f97b80 100644
--- a/Options.py
+++ b/Options.py
@@ -245,6 +245,27 @@ class OptionDict(Option):
return str(value)
+class OptionList(Option):
+ default = []
+
+ def __init__(self, value: typing.List[str, typing.Any]):
+ self.value = value
+
+ @classmethod
+ def from_text(cls, text: str):
+ return cls([option.strip() for option in text.split(",")])
+
+ @classmethod
+ def from_any(cls, data: typing.Any):
+ if type(data) == list:
+ return cls(data)
+ return cls.from_text(str(data))
+
+ def get_option_name(self, value):
+ return str(value)
+
+
+
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
diff --git a/Utils.py b/Utils.py
index b655e1d4..5f3cc0cb 100644
--- a/Utils.py
+++ b/Utils.py
@@ -201,6 +201,9 @@ def get_default_options() -> dict:
"forge_directory": "Minecraft Forge server",
"max_heap_size": "2G"
},
+ "oot_options": {
+ "rom_file": "The Legend of Zelda - Ocarina of Time.z64",
+ }
}
return options
diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py
index 77dc179e..e7856a12 100644
--- a/WebHostLib/downloads.py
+++ b/WebHostLib/downloads.py
@@ -66,6 +66,8 @@ def download_slot_file(room_id, player_id: int):
for name in zf.namelist():
if name.endswith("info.json"):
fname = name.rsplit("/", 1)[0]+".zip"
+ elif slot_data.game == "Ocarina of Time":
+ fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html
index 3e32b796..7c7161a7 100644
--- a/WebHostLib/templates/macros.html
+++ b/WebHostLib/templates/macros.html
@@ -16,6 +16,9 @@
{% elif patch.game == "Factorio" %}
Mod for player {{ patch.player_id }} - {{ patch.player_name }}
+ {% elif patch.game == "Ocarina of Time" %}
+
+ APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}
{% else %}
Patch for player {{ patch.player_id }} - {{ patch.player_name }}
diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py
index 97087629..8a010e3d 100644
--- a/WebHostLib/upload.py
+++ b/WebHostLib/upload.py
@@ -63,6 +63,12 @@ def uploads():
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Factorio"))
+ elif file.filename.endswith(".apz5"):
+ # .apz5 must be named specifically since they don't contain any metadata
+ _, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
+ slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
+ player_id=int(slot_id[1:]), game="Ocarina of Time"))
+
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith(".archipelago"):
diff --git a/host.yaml b/host.yaml
index 8fe346b0..ed65d37b 100644
--- a/host.yaml
+++ b/host.yaml
@@ -85,4 +85,7 @@ factorio_options:
executable: "factorio\\bin\\x64\\factorio"
minecraft_options:
forge_directory: "Minecraft Forge server"
- max_heap_size: "2G"
\ No newline at end of file
+ max_heap_size: "2G"
+oot_options:
+ # File name of the OoT v1.0 ROM
+ rom_file: "The Legend of Zelda - Ocarina of Time.z64"
\ No newline at end of file
diff --git a/playerSettings.yaml b/playerSettings.yaml
index d503e922..bfec9c15 100644
--- a/playerSettings.yaml
+++ b/playerSettings.yaml
@@ -674,6 +674,618 @@ A Link to the Past:
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
+
+Ocarina of Time:
+ logic_rules: # Set the logic used for the generator.
+ glitchless: 50
+ glitched: 0
+ no_logic: 0
+ logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
+ false: 50
+ true: 0
+ open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
+ open: 50
+ closed_deku: 0
+ closed: 0
+ open_kakariko: # Set the state of the Kakariko Village gate.
+ open: 50
+ zelda: 0
+ closed: 0
+ open_door_of_time: # Open the Door of Time by default, without the Song of Time.
+ false: 0
+ true: 50
+ zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
+ open: 0
+ adult: 0
+ closed: 50
+ gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
+ normal: 0
+ fast: 50
+ open: 0
+ bridge: # Set the requirements for the Rainbow Bridge.
+ open: 0
+ vanilla: 0
+ stones: 0
+ medallions: 50
+ dungeons: 0
+ tokens: 0
+ trials: # Set the number of required trials in Ganon's Castle.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 6: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ starting_age: # Choose which age Link will start as.
+ child: 50
+ adult: 0
+ triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
+ false: 50
+ true: 0
+ triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
+ # you can add additional values between minimum and maximum
+ 1: 0 # minimum value
+ 50: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
+ false: 50
+ true: 0
+ bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 3: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ bridge_medallions: # Set the number of medallions required for the rainbow bridge.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 6: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 9: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 100: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
+ remove: 0
+ startwith: 50
+ vanilla: 0
+ dungeon: 0
+ overworld: 0
+ any_dungeon: 0
+ keysanity: 0
+ shuffle_smallkeys: # Control where to shuffle dungeon small keys.
+ remove: 0
+ vanilla: 0
+ dungeon: 50
+ overworld: 0
+ any_dungeon: 0
+ keysanity: 0
+ shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
+ vanilla: 50
+ overworld: 0
+ any_dungeon: 0
+ keysanity: 0
+ shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
+ remove: 0
+ vanilla: 0
+ dungeon: 50
+ overworld: 0
+ any_dungeon: 0
+ keysanity: 0
+ shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
+ remove: 50
+ vanilla: 0
+ dungeon: 0
+ overworld: 0
+ any_dungeon: 0
+ keysanity: 0
+ on_lacs: 0
+ enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
+ false: 50
+ true: 0
+ lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
+ vanilla: 50
+ stones: 0
+ medallions: 0
+ dungeons: 0
+ tokens: 0
+ lacs_stones: # Set the number of Spiritual Stones required for LACS.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 3: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ lacs_medallions: # Set the number of medallions required for LACS.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 6: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ lacs_rewards: # Set the number of dungeon rewards required for LACS.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 9: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 100: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ shuffle_song_items: # Set where songs can appear.
+ song: 50
+ dungeon: 0
+ any: 0
+ shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
+ off: 50
+ "0": 0
+ "1": 0
+ "2": 0
+ "3": 0
+ "4": 0
+ random_value: 0
+ tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
+ off: 50
+ dungeons: 0
+ overworld: 0
+ all: 0
+ shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
+ off: 50
+ low: 0
+ affordable: 0
+ expensive: 0
+ shuffle_cows: # Cows give items when Epona's Song is played.
+ false: 50
+ true: 0
+ shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
+ false: 50
+ true: 0
+ shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
+ false: 50
+ true: 0
+ shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
+ false: 50
+ true: 0
+ shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
+ false: 50
+ true: 0
+ shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
+ false: 50
+ true: 0
+ shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
+ false: 50
+ true: 0
+ skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
+ false: 50
+ true: 0
+ no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
+ false: 0
+ true: 50
+ no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
+ false: 0
+ true: 50
+ no_epona_race: # Epona can always be summoned with Epona's Song.
+ false: 0
+ true: 50
+ skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
+ false: 0
+ true: 50
+ complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
+ false: 50
+ true: 0
+ useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
+ false: 50
+ true: 0
+ fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
+ false: 0
+ true: 50
+ free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
+ false: 50
+ true: 0
+ fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
+ false: 50
+ true: 0
+ chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
+ # you can add additional values between minimum and maximum
+ 0: 0 # minimum value
+ 7: 0 # maximum value
+ random: 50
+ random-low: 0
+ random-high: 0
+ hints: # Gossip Stones can give hints about item locations.
+ none: 0
+ mask: 0
+ agony: 0
+ always: 50
+ false: 0
+ hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
+ balanced: 50
+ ddr: 0
+ league: 0
+ mw2: 0
+ scrubs: 0
+ strong: 0
+ tournament: 0
+ useless: 0
+ very_strong: 0
+ text_shuffle: # Randomizes text in the game for comedic effect.
+ none: 50
+ except_hints: 0
+ complete: 0
+ damage_multiplier: # Controls the amount of damage Link takes.
+ half: 0
+ normal: 50
+ double: 0
+ quadruple: 0
+ ohko: 0
+ no_collectible_hearts: # Hearts will not drop from enemies or objects.
+ false: 50
+ true: 0
+ starting_tod: # Change the starting time of day.
+ default: 50
+ sunrise: 0
+ morning: 0
+ noon: 0
+ afternoon: 0
+ sunset: 0
+ evening: 0
+ midnight: 0
+ witching_hour: 0
+ start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
+ false: 50
+ true: 0
+ start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
+ false: 50
+ true: 0
+ item_pool_value: # Changes the number of items available in the game.
+ plentiful: 0
+ balanced: 50
+ scarce: 0
+ minimal: 0
+ junk_ice_traps: # Adds ice traps to the item pool.
+ off: 0
+ normal: 50
+ extra: 0
+ mayhem: 0
+ onslaught: 0
+ ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
+ major_only: 50
+ junk_only: 0
+ anything: 0
+ logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
+ pocket_egg: 0
+ pocket_cucco: 0
+ cojiro: 0
+ odd_mushroom: 0
+ poachers_saw: 0
+ broken_sword: 0
+ prescription: 50
+ eyeball_frog: 0
+ eyedrops: 0
+ claim_check: 0
+ logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
+ pocket_egg: 0
+ pocket_cucco: 0
+ cojiro: 0
+ odd_mushroom: 0
+ poachers_saw: 0
+ broken_sword: 0
+ prescription: 0
+ eyeball_frog: 0
+ eyedrops: 0
+ claim_check: 50
+ default_targeting: # Default targeting option.
+ hold: 50
+ switch: 0
+ display_dpad: # Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).
+ false: 0
+ true: 50
+ correct_model_colors: # Makes in-game models match their HUD element colors.
+ false: 0
+ true: 50
+ background_music: # Randomize or disable background music.
+ normal: 50
+ off: 0
+ randomized: 0
+ fanfares: # Randomize or disable item fanfares.
+ normal: 50
+ off: 0
+ randomized: 0
+ ocarina_fanfares: # Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized.
+ false: 50
+ true: 0
+ kokiri_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ kokiri_green: 50
+ goron_red: 0
+ zora_blue: 0
+ black: 0
+ white: 0
+ azure_blue: 0
+ vivid_cyan: 0
+ light_red: 0
+ fuchsia: 0
+ purple: 0
+ majora_purple: 0
+ twitch_purple: 0
+ purple_heart: 0
+ persian_rose: 0
+ dirty_yellow: 0
+ blush_pink: 0
+ hot_pink: 0
+ rose_pink: 0
+ orange: 0
+ gray: 0
+ gold: 0
+ silver: 0
+ beige: 0
+ teal: 0
+ blood_red: 0
+ blood_orange: 0
+ royal_blue: 0
+ sonic_blue: 0
+ nes_green: 0
+ dark_green: 0
+ lumen: 0
+ goron_color: # Choose a color. Uses the same options as "kokiri_color".
+ random_choice: 0
+ completely_random: 0
+ goron_red: 50
+ zora_color: # Choose a color. Uses the same options as "kokiri_color".
+ random_choice: 0
+ completely_random: 0
+ zora_blue: 50
+ silver_gauntlets_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ silver: 50
+ gold: 0
+ black: 0
+ green: 0
+ blue: 0
+ bronze: 0
+ red: 0
+ sky_blue: 0
+ pink: 0
+ magenta: 0
+ orange: 0
+ lime: 0
+ purple: 0
+ golden_gauntlets_color: # Choose a color. Uses the same options as "silver_gauntlets_color".
+ random_choice: 0
+ completely_random: 0
+ gold: 50
+ mirror_shield_frame_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ red: 50
+ green: 0
+ blue: 0
+ yellow: 0
+ cyan: 0
+ magenta: 0
+ orange: 0
+ gold: 0
+ purple: 0
+ pink: 0
+ navi_color_default_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ rainbow: 0
+ gold: 0
+ white: 50
+ green: 0
+ light_blue: 0
+ yellow: 0
+ red: 0
+ magenta: 0
+ black: 0
+ tatl: 0
+ tael: 0
+ fi: 0
+ ciela: 0
+ epona: 0
+ ezlo: 0
+ king_of_red_lions: 0
+ linebeck: 0
+ loftwing: 0
+ midna: 0
+ phantom_zelda: 0
+ navi_color_default_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ navi_color_enemy_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
+ random_choice: 0
+ completely_random: 0
+ yellow: 50
+ navi_color_enemy_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ navi_color_npc_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
+ random_choice: 0
+ completely_random: 0
+ light_blue: 50
+ navi_color_npc_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ navi_color_prop_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
+ random_choice: 0
+ completely_random: 0
+ green: 50
+ navi_color_prop_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ sword_trail_duration: # Set the duration for sword trails.
+ # you can add additional values between minimum and maximum
+ 4: 50 # minimum value
+ 20: 0 # maximum value
+ random: 0
+ random-low: 0
+ random-high: 0
+ sword_trail_color_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ rainbow: 0
+ white: 50
+ red: 0
+ green: 0
+ blue: 0
+ cyan: 0
+ magenta: 0
+ orange: 0
+ gold: 0
+ purple: 0
+ pink: 0
+ sword_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ bombchu_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
+ random_choice: 0
+ completely_random: 0
+ red: 50
+ bombchu_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ boomerang_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
+ random_choice: 0
+ completely_random: 0
+ yellow: 50
+ boomerang_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
+ random_choice: 0
+ completely_random: 0
+ match_inner: 50
+ heart_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ red: 50
+ green: 0
+ blue: 0
+ yellow: 0
+ magic_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ green: 50
+ red: 0
+ blue: 0
+ purple: 0
+ pink: 0
+ yellow: 0
+ white: 0
+ a_button_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
+ random_choice: 0
+ completely_random: 0
+ n64_blue: 50
+ n64_green: 0
+ n64_red: 0
+ gamecube_green: 0
+ gamecube_red: 0
+ gamecube_grey: 0
+ yellow: 0
+ black: 0
+ white: 0
+ magenta: 0
+ ruby: 0
+ sapphire: 0
+ lime: 0
+ cyan: 0
+ purple: 0
+ orange: 0
+ b_button_color: # Choose a color. Uses the same options as "a_button_color".
+ random_choice: 0
+ completely_random: 0
+ n64_green: 50
+ c_button_color: # Choose a color. Uses the same options as "a_button_color".
+ random_choice: 0
+ completely_random: 0
+ yellow: 50
+ start_button_color: # Choose a color. Uses the same options as "a_button_color".
+ random_choice: 0
+ completely_random: 0
+ n64_red: 50
+ sfx_navi_overworld: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_navi_enemy: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_low_hp: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_menu_cursor: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_menu_select: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_nightfall: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_horse_neigh: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ none: 0
+ sfx_hover_boots: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
+ default: 50
+ completely_random: 0
+ random_ear_safe: 0
+ random_choice: 0
+ sfx_ocarina: # Change the sound of the ocarina.
+ ocarina: 50
+ malon: 0
+ whistle: 0
+ harp: 0
+ grind_organ: 0
+ flute: 0
+ logic_tricks:
+ []
+
# meta_ignore, linked_options and triggers work for any game
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode:
diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py
index eb769bd0..06177c48 100644
--- a/worlds/generic/Rules.py
+++ b/worlds/generic/Rules.py
@@ -13,6 +13,7 @@ def exclusion_rules(world, player: int, excluded_locations: set):
for loc_name in excluded_locations:
location = world.get_location(loc_name, player)
add_item_rule(location, lambda i: not (i.advancement or i.never_exclude))
+ location.excluded = True
def set_rule(spot, rule):
diff --git a/worlds/oot/Colors.py b/worlds/oot/Colors.py
new file mode 100644
index 00000000..996c68bf
--- /dev/null
+++ b/worlds/oot/Colors.py
@@ -0,0 +1,403 @@
+from collections import namedtuple
+import random
+import re
+
+Color = namedtuple('Color', ' R G B')
+
+tunic_colors = {
+ "Kokiri Green": Color(0x1E, 0x69, 0x1B),
+ "Goron Red": Color(0x64, 0x14, 0x00),
+ "Zora Blue": Color(0x00, 0x3C, 0x64),
+ "Black": Color(0x30, 0x30, 0x30),
+ "White": Color(0xF0, 0xF0, 0xFF),
+ "Azure Blue": Color(0x13, 0x9E, 0xD8),
+ "Vivid Cyan": Color(0x13, 0xE9, 0xD8),
+ "Light Red": Color(0xF8, 0x7C, 0x6D),
+ "Fuchsia": Color(0xFF, 0x00, 0xFF),
+ "Purple": Color(0x95, 0x30, 0x80),
+ "Majora Purple": Color(0x40, 0x00, 0x40),
+ "Twitch Purple": Color(0x64, 0x41, 0xA5),
+ "Purple Heart": Color(0x8A, 0x2B, 0xE2),
+ "Persian Rose": Color(0xFF, 0x14, 0x93),
+ "Dirty Yellow": Color(0xE0, 0xD8, 0x60),
+ "Blush Pink": Color(0xF8, 0x6C, 0xF8),
+ "Hot Pink": Color(0xFF, 0x69, 0xB4),
+ "Rose Pink": Color(0xFF, 0x90, 0xB3),
+ "Orange": Color(0xE0, 0x79, 0x40),
+ "Gray": Color(0xA0, 0xA0, 0xB0),
+ "Gold": Color(0xD8, 0xB0, 0x60),
+ "Silver": Color(0xD0, 0xF0, 0xFF),
+ "Beige": Color(0xC0, 0xA0, 0xA0),
+ "Teal": Color(0x30, 0xD0, 0xB0),
+ "Blood Red": Color(0x83, 0x03, 0x03),
+ "Blood Orange": Color(0xFE, 0x4B, 0x03),
+ "Royal Blue": Color(0x40, 0x00, 0x90),
+ "Sonic Blue": Color(0x50, 0x90, 0xE0),
+ "NES Green": Color(0x00, 0xD0, 0x00),
+ "Dark Green": Color(0x00, 0x25, 0x18),
+ "Lumen": Color(0x50, 0x8C, 0xF0),
+}
+
+NaviColors = { # Inner Core Color Outer Glow Color
+ "Rainbow": (Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00)),
+ "Gold": (Color(0xFE, 0xCC, 0x3C), Color(0xFE, 0xC0, 0x07)),
+ "White": (Color(0xFF, 0xFF, 0xFF), Color(0x00, 0x00, 0xFF)),
+ "Green": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
+ "Light Blue": (Color(0x96, 0x96, 0xFF), Color(0x96, 0x96, 0xFF)),
+ "Yellow": (Color(0xFF, 0xFF, 0x00), Color(0xC8, 0x9B, 0x00)),
+ "Red": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
+ "Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xC8, 0x00, 0x9B)),
+ "Black": (Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00)),
+ "Tatl": (Color(0xFF, 0xFF, 0xFF), Color(0xC8, 0x98, 0x00)),
+ "Tael": (Color(0x49, 0x14, 0x6C), Color(0xFF, 0x00, 0x00)),
+ "Fi": (Color(0x2C, 0x9E, 0xC4), Color(0x2C, 0x19, 0x83)),
+ "Ciela": (Color(0xE6, 0xDE, 0x83), Color(0xC6, 0xBE, 0x5B)),
+ "Epona": (Color(0xD1, 0x49, 0x02), Color(0x55, 0x1F, 0x08)),
+ "Ezlo": (Color(0x62, 0x9C, 0x5F), Color(0x3F, 0x5D, 0x37)),
+ "King of Red Lions": (Color(0xA8, 0x33, 0x17), Color(0xDE, 0xD7, 0xC5)),
+ "Linebeck": (Color(0x03, 0x26, 0x60), Color(0xEF, 0xFF, 0xFF)),
+ "Loftwing": (Color(0xD6, 0x2E, 0x31), Color(0xFD, 0xE6, 0xCC)),
+ "Midna": (Color(0x19, 0x24, 0x26), Color(0xD2, 0x83, 0x30)),
+ "Phantom Zelda": (Color(0x97, 0x7A, 0x6C), Color(0x6F, 0x46, 0x67)),
+}
+
+sword_trail_colors = {
+ "Rainbow": Color(0x00, 0x00, 0x00),
+ "White": Color(0xFF, 0xFF, 0xFF),
+ "Red": Color(0xFF, 0x00, 0x00),
+ "Green": Color(0x00, 0xFF, 0x00),
+ "Blue": Color(0x00, 0x00, 0xFF),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Orange": Color(0xFF, 0xA5, 0x00),
+ "Gold": Color(0xFF, 0xD7, 0x00),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Pink": Color(0xFF, 0x69, 0xB4),
+}
+
+bombchu_trail_colors = {
+ "Rainbow": Color(0x00, 0x00, 0x00),
+ "Red": Color(0xFA, 0x00, 0x00),
+ "Green": Color(0x00, 0xFF, 0x00),
+ "Blue": Color(0x00, 0x00, 0xFF),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Orange": Color(0xFF, 0xA5, 0x00),
+ "Gold": Color(0xFF, 0xD7, 0x00),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Pink": Color(0xFF, 0x69, 0xB4),
+}
+
+boomerang_trail_colors = {
+ "Rainbow": Color(0x00, 0x00, 0x00),
+ "Yellow": Color(0xFF, 0xFF, 0x64),
+ "Red": Color(0xFF, 0x00, 0x00),
+ "Green": Color(0x00, 0xFF, 0x00),
+ "Blue": Color(0x00, 0x00, 0xFF),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Orange": Color(0xFF, 0xA5, 0x00),
+ "Gold": Color(0xFF, 0xD7, 0x00),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Pink": Color(0xFF, 0x69, 0xB4),
+}
+
+gauntlet_colors = {
+ "Silver": Color(0xFF, 0xFF, 0xFF),
+ "Gold": Color(0xFE, 0xCF, 0x0F),
+ "Black": Color(0x00, 0x00, 0x06),
+ "Green": Color(0x02, 0x59, 0x18),
+ "Blue": Color(0x06, 0x02, 0x5A),
+ "Bronze": Color(0x60, 0x06, 0x02),
+ "Red": Color(0xFF, 0x00, 0x00),
+ "Sky Blue": Color(0x02, 0x5D, 0xB0),
+ "Pink": Color(0xFA, 0x6A, 0x90),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Orange": Color(0xDA, 0x38, 0x00),
+ "Lime": Color(0x5B, 0xA8, 0x06),
+ "Purple": Color(0x80, 0x00, 0x80),
+}
+
+shield_frame_colors = {
+ "Red": Color(0xD7, 0x00, 0x00),
+ "Green": Color(0x00, 0xFF, 0x00),
+ "Blue": Color(0x00, 0x40, 0xD8),
+ "Yellow": Color(0xFF, 0xFF, 0x64),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Orange": Color(0xFF, 0xA5, 0x00),
+ "Gold": Color(0xFF, 0xD7, 0x00),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Pink": Color(0xFF, 0x69, 0xB4),
+}
+
+heart_colors = {
+ "Red": Color(0xFF, 0x46, 0x32),
+ "Green": Color(0x46, 0xC8, 0x32),
+ "Blue": Color(0x32, 0x46, 0xFF),
+ "Yellow": Color(0xFF, 0xE0, 0x00),
+}
+
+magic_colors = {
+ "Green": Color(0x00, 0xC8, 0x00),
+ "Red": Color(0xC8, 0x00, 0x00),
+ "Blue": Color(0x00, 0x30, 0xFF),
+ "Purple": Color(0xB0, 0x00, 0xFF),
+ "Pink": Color(0xFF, 0x00, 0xC8),
+ "Yellow": Color(0xFF, 0xFF, 0x00),
+ "White": Color(0xFF, 0xFF, 0xFF),
+}
+
+# A Button Text Cursor Shop Cursor Save/Death Cursor
+# Pause Menu A Cursor Pause Menu A Icon A Note
+a_button_colors = {
+ "N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x50, 0xC8), Color(0x00, 0x50, 0xFF), Color(0x64, 0x64, 0xFF),
+ Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)),
+ "N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x64, 0x96, 0x64),
+ Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00)),
+ "N64 Red": (Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x64, 0x64),
+ Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00)),
+ "GameCube Green": (Color(0x00, 0xC8, 0x32), Color(0x00, 0xC8, 0x50), Color(0x00, 0xFF, 0x50), Color(0x64, 0xFF, 0x64),
+ Color(0x00, 0xFF, 0x32), Color(0x00, 0xFF, 0x64), Color(0x50, 0xFF, 0x96)),
+ "GameCube Red": (Color(0xFF, 0x1E, 0x1E), Color(0xC8, 0x00, 0x00), Color(0xFF, 0x00, 0x50), Color(0xFF, 0x64, 0x64),
+ Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E)),
+ "GameCube Grey": (Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78),
+ Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78)),
+ "Yellow": (Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xA0, 0x00),
+ Color(0xFF, 0xFF, 0x00), Color(0xFF, 0x96, 0x00), Color(0xFF, 0xFF, 0x32)),
+ "Black": (Color(0x10, 0x10, 0x10), Color(0x00, 0x00, 0x00), Color(0x00, 0x00, 0x00), Color(0x10, 0x10, 0x10),
+ Color(0x00, 0x00, 0x00), Color(0x18, 0x18, 0x18), Color(0x18, 0x18, 0x18)),
+ "White": (Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF),
+ Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF)),
+ "Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF),
+ Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF)),
+ "Ruby": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00),
+ Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
+ "Sapphire": (Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF),
+ Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF)),
+ "Lime": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00),
+ Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
+ "Cyan": (Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF),
+ Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF)),
+ "Purple": (Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80),
+ Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80)),
+ "Orange": (Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00),
+ Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00)),
+}
+
+# B Button
+b_button_colors = {
+ "N64 Blue": Color(0x5A, 0x5A, 0xFF),
+ "N64 Green": Color(0x00, 0x96, 0x00),
+ "N64 Red": Color(0xC8, 0x00, 0x00),
+ "GameCube Green": Color(0x00, 0xC8, 0x32),
+ "GameCube Red": Color(0xFF, 0x1E, 0x1E),
+ "GameCube Grey": Color(0x78, 0x78, 0x78),
+ "Yellow": Color(0xFF, 0xA0, 0x00),
+ "Black": Color(0x10, 0x10, 0x10),
+ "White": Color(0xFF, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Ruby": Color(0xFF, 0x00, 0x00),
+ "Sapphire": Color(0x00, 0x00, 0xFF),
+ "Lime": Color(0x00, 0xFF, 0x00),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Orange": Color(0xFF, 0x80, 0x00),
+}
+
+# C Button Pause Menu C Cursor Pause Menu C Icon C Note
+c_button_colors = {
+ "N64 Blue": (Color(0x5A, 0x5A, 0xFF), Color(0x00, 0x32, 0xFF), Color(0x00, 0x64, 0xFF), Color(0x50, 0x96, 0xFF)),
+ "N64 Green": (Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00), Color(0x00, 0x96, 0x00)),
+ "N64 Red": (Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00), Color(0xC8, 0x00, 0x00)),
+ "GameCube Green": (Color(0x00, 0xC8, 0x32), Color(0x00, 0xFF, 0x32), Color(0x00, 0xFF, 0x64), Color(0x50, 0xFF, 0x96)),
+ "GameCube Red": (Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E), Color(0xFF, 0x1E, 0x1E)),
+ "GameCube Grey": (Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78), Color(0x78, 0x78, 0x78)),
+ "Yellow": (Color(0xFF, 0xA0, 0x00), Color(0xFF, 0xFF, 0x00), Color(0xFF, 0x96, 0x00), Color(0xFF, 0xFF, 0x32)),
+ "Black": (Color(0x10, 0x10, 0x10), Color(0x00, 0x00, 0x00), Color(0x18, 0x18, 0x18), Color(0x18, 0x18, 0x18)),
+ "White": (Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF), Color(0xFF, 0xFF, 0xFF)),
+ "Magenta": (Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF), Color(0xFF, 0x00, 0xFF)),
+ "Ruby": (Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00), Color(0xFF, 0x00, 0x00)),
+ "Sapphire": (Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF), Color(0x00, 0x00, 0xFF)),
+ "Lime": (Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00), Color(0x00, 0xFF, 0x00)),
+ "Cyan": (Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF), Color(0x00, 0xFF, 0xFF)),
+ "Purple": (Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80), Color(0x80, 0x00, 0x80)),
+ "Orange": (Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00), Color(0xFF, 0x80, 0x00)),
+}
+
+# Start Button
+start_button_colors = {
+ "N64 Blue": Color(0x5A, 0x5A, 0xFF),
+ "N64 Green": Color(0x00, 0x96, 0x00),
+ "N64 Red": Color(0xC8, 0x00, 0x00),
+ "GameCube Green": Color(0x00, 0xC8, 0x32),
+ "GameCube Red": Color(0xFF, 0x1E, 0x1E),
+ "GameCube Grey": Color(0x78, 0x78, 0x78),
+ "Yellow": Color(0xFF, 0xA0, 0x00),
+ "Black": Color(0x10, 0x10, 0x10),
+ "White": Color(0xFF, 0xFF, 0xFF),
+ "Magenta": Color(0xFF, 0x00, 0xFF),
+ "Ruby": Color(0xFF, 0x00, 0x00),
+ "Sapphire": Color(0x00, 0x00, 0xFF),
+ "Lime": Color(0x00, 0xFF, 0x00),
+ "Cyan": Color(0x00, 0xFF, 0xFF),
+ "Purple": Color(0x80, 0x00, 0x80),
+ "Orange": Color(0xFF, 0x80, 0x00),
+}
+
+meta_color_choices = ["Random Choice", "Completely Random"] #, "Custom Color"]
+
+
+def get_tunic_colors():
+ return list(tunic_colors.keys())
+
+
+def get_tunic_color_options():
+ return meta_color_choices + get_tunic_colors()
+
+
+def get_navi_colors():
+ return list(NaviColors.keys())
+
+
+def get_navi_color_options(outer=False):
+ if outer:
+ return ["[Same as Inner]"] + meta_color_choices + get_navi_colors()
+ else:
+ return meta_color_choices + get_navi_colors()
+
+
+def get_sword_trail_colors():
+ return list(sword_trail_colors.keys())
+
+
+def get_sword_trail_color_options(outer=False):
+ if outer:
+ return ["[Same as Inner]"] + meta_color_choices + get_sword_trail_colors()
+ else:
+ return meta_color_choices + get_sword_trail_colors()
+
+
+def get_bombchu_trail_colors():
+ return list(bombchu_trail_colors.keys())
+
+
+def get_bombchu_trail_color_options(outer=False):
+ if outer:
+ return ["[Same as Inner]"] + meta_color_choices + get_bombchu_trail_colors()
+ else:
+ return meta_color_choices + get_bombchu_trail_colors()
+
+
+def get_boomerang_trail_colors():
+ return list(boomerang_trail_colors.keys())
+
+
+def get_boomerang_trail_color_options(outer=False):
+ if outer:
+ return ["[Same as Inner]"] + meta_color_choices + get_boomerang_trail_colors()
+ else:
+ return meta_color_choices + get_boomerang_trail_colors()
+
+
+def get_gauntlet_colors():
+ return list(gauntlet_colors.keys())
+
+
+def get_gauntlet_color_options():
+ return meta_color_choices + get_gauntlet_colors()
+
+
+def get_shield_frame_colors():
+ return list(shield_frame_colors.keys())
+
+
+def get_shield_frame_color_options():
+ return meta_color_choices + get_shield_frame_colors()
+
+
+def get_heart_colors():
+ return list(heart_colors.keys())
+
+
+def get_heart_color_options():
+ return meta_color_choices + get_heart_colors()
+
+
+def get_magic_colors():
+ return list(magic_colors.keys())
+
+
+def get_magic_color_options():
+ return meta_color_choices + get_magic_colors()
+
+
+def get_a_button_colors():
+ return list(a_button_colors.keys())
+
+
+def get_a_button_color_options():
+ return meta_color_choices + get_a_button_colors()
+
+
+def get_b_button_colors():
+ return list(b_button_colors.keys())
+
+
+def get_b_button_color_options():
+ return meta_color_choices + get_b_button_colors()
+
+
+def get_c_button_colors():
+ return list(c_button_colors.keys())
+
+
+def get_c_button_color_options():
+ return meta_color_choices + get_c_button_colors()
+
+
+def get_start_button_colors():
+ return list(start_button_colors.keys())
+
+
+def get_start_button_color_options():
+ return meta_color_choices + get_start_button_colors()
+
+
+def contrast_ratio(color1, color2):
+ # Based on accessibility standards (WCAG 2.0)
+ lum1 = relative_luminance(color1)
+ lum2 = relative_luminance(color2)
+ return (max(lum1, lum2) + 0.05) / (min(lum1, lum2) + 0.05)
+
+
+def relative_luminance(color):
+ color_ratios = list(map(lum_color_ratio, color))
+ return color_ratios[0] * 0.299 + color_ratios[1] * 0.587 + color_ratios[2] * 0.114
+
+
+def lum_color_ratio(val):
+ val /= 255
+ if val <= 0.03928:
+ return val / 12.92
+ else:
+ return pow((val + 0.055) / 1.055, 2.4)
+
+
+def generate_random_color():
+ return [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
+
+
+def hex_to_color(option):
+ # build color from hex code
+ option = option[1:] if option[0] == "#" else option
+ if not re.search(r'^(?:[0-9a-fA-F]{3}){1,2}$', option):
+ raise Exception(f"Invalid color value provided: {option}")
+ if len(option) > 3:
+ return list(int(option[i:i + 2], 16) for i in (0, 2, 4))
+ else:
+ return list(int(f'{option[i]}{option[i]}', 16) for i in (0, 1, 2))
+
+
+def color_to_hex(color):
+ return '#' + ''.join(['{:02X}'.format(c) for c in color])
diff --git a/worlds/oot/Cosmetics.py b/worlds/oot/Cosmetics.py
new file mode 100644
index 00000000..b91b4606
--- /dev/null
+++ b/worlds/oot/Cosmetics.py
@@ -0,0 +1,814 @@
+from .Utils import data_path, __version__
+from .Colors import *
+import logging
+import worlds.oot.Music as music
+import worlds.oot.Sounds as sfx
+import worlds.oot.IconManip as icon
+from .JSONDump import dump_obj, CollapseList, CollapseDict, AlignedDict, SortedDict
+import json
+
+logger = logging.getLogger('')
+
+# Options are all lowercase and have underscores instead of spaces
+# this needs to be undone for the oot generator
+def format_cosmetic_option_result(option_result):
+ def format_word(word):
+ special_words = {
+ 'nes': 'NES',
+ 'gamecube': 'GameCube',
+ 'of': 'of'
+ }
+ return special_words.get(word, word.capitalize())
+ words = option_result.split('_')
+ return ' '.join([format_word(word) for word in words])
+
+
+def patch_targeting(rom, ootworld, symbols):
+ # Set default targeting option to Hold
+ if ootworld.default_targeting == 'hold':
+ rom.write_byte(0xB71E6D, 0x01)
+ else:
+ rom.write_byte(0xB71E6D, 0x00)
+
+
+def patch_dpad(rom, ootworld, symbols):
+ # Display D-Pad HUD
+ if ootworld.display_dpad:
+ rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x01)
+ else:
+ rom.write_byte(symbols['CFG_DISPLAY_DPAD'], 0x00)
+
+
+
+def patch_music(rom, ootworld, symbols):
+ # patch music
+ if ootworld.background_music != 'normal' or ootworld.fanfares != 'normal':
+ music.restore_music(rom)
+ log, errors = music.randomize_music(rom, ootworld, {})
+ if errors:
+ logger.error(errors)
+ else:
+ music.restore_music(rom)
+
+
+def patch_model_colors(rom, color, model_addresses):
+ main_addresses, dark_addresses = model_addresses
+
+ if color is None:
+ for address in main_addresses + dark_addresses:
+ original = rom.original.read_bytes(address, 3)
+ rom.write_bytes(address, original)
+ return
+
+ for address in main_addresses:
+ rom.write_bytes(address, color)
+
+ darkened_color = list(map(lambda light: int(max((light - 0x32) * 0.6, 0)), color))
+ for address in dark_addresses:
+ rom.write_bytes(address, darkened_color)
+
+
+def patch_tunic_icon(rom, tunic, color):
+ # patch tunic icon colors
+ icon_locations = {
+ 'Kokiri Tunic': 0x007FE000,
+ 'Goron Tunic': 0x007FF000,
+ 'Zora Tunic': 0x00800000,
+ }
+
+ if color is not None:
+ tunic_icon = icon.generate_tunic_icon(color)
+ else:
+ tunic_icon = rom.original.read_bytes(icon_locations[tunic], 0x1000)
+
+ rom.write_bytes(icon_locations[tunic], tunic_icon)
+
+
+def patch_tunic_colors(rom, ootworld, symbols):
+ # patch tunic colors
+ tunics = [
+ ('Kokiri Tunic', 'kokiri_color', 0x00B6DA38),
+ ('Goron Tunic', 'goron_color', 0x00B6DA3B),
+ ('Zora Tunic', 'zora_color', 0x00B6DA3E),
+ ]
+ tunic_color_list = get_tunic_colors()
+
+ for tunic, tunic_setting, address in tunics:
+ tunic_option = format_cosmetic_option_result(ootworld.__dict__[tunic_setting])
+
+ # handle random
+ if tunic_option == 'Random Choice':
+ tunic_option = random.choice(tunic_color_list)
+ # handle completely random
+ if tunic_option == 'Completely Random':
+ color = generate_random_color()
+ # grab the color from the list
+ elif tunic_option in tunic_colors:
+ color = list(tunic_colors[tunic_option])
+ # build color from hex code
+ else:
+ color = hex_to_color(tunic_option)
+ tunic_option = 'Custom'
+ # "Weird" weirdshots will crash if the Kokiri Tunic Green value is > 0x99. Brickwall it.
+ if ootworld.logic_rules != 'glitchless' and tunic == 'Kokiri Tunic':
+ color[1] = min(color[1],0x98)
+ rom.write_bytes(address, color)
+
+ # patch the tunic icon
+ if [tunic, tunic_option] not in [['Kokiri Tunic', 'Kokiri Green'], ['Goron Tunic', 'Goron Red'], ['Zora Tunic', 'Zora Blue']]:
+ patch_tunic_icon(rom, tunic, color)
+ else:
+ patch_tunic_icon(rom, tunic, None)
+
+
+def patch_navi_colors(rom, ootworld, symbols):
+ # patch navi colors
+ navi = [
+ # colors for Navi
+ ('Navi Idle', 'navi_color_default',
+ [0x00B5E184], # Default (Player)
+ symbols.get('CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED', None)),
+ ('Navi Targeting Enemy', 'navi_color_enemy',
+ [0x00B5E19C, 0x00B5E1BC], # Enemy, Boss
+ symbols.get('CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED', None)),
+ ('Navi Targeting NPC', 'navi_color_npc',
+ [0x00B5E194], # NPC
+ symbols.get('CFG_RAINBOW_NAVI_NPC_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED', None)),
+ ('Navi Targeting Prop', 'navi_color_prop',
+ [0x00B5E174, 0x00B5E17C, 0x00B5E18C, 0x00B5E1A4, 0x00B5E1AC,
+ 0x00B5E1B4, 0x00B5E1C4, 0x00B5E1CC, 0x00B5E1D4], # Everything else
+ symbols.get('CFG_RAINBOW_NAVI_PROP_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED', None)),
+ ]
+
+ navi_color_list = get_navi_colors()
+ rainbow_error = None
+
+ for navi_action, navi_setting, navi_addresses, rainbow_inner_symbol, rainbow_outer_symbol in navi:
+ navi_option_inner = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_inner'])
+ navi_option_outer = format_cosmetic_option_result(ootworld.__dict__[navi_setting+'_outer'])
+
+ # choose a random choice for the whole group
+ if navi_option_inner == 'Random Choice':
+ navi_option_inner = random.choice(navi_color_list)
+ if navi_option_outer == 'Random Choice':
+ navi_option_outer = random.choice(navi_color_list)
+
+ if navi_option_outer == 'Match Inner':
+ navi_option_outer = navi_option_inner
+
+ colors = []
+ option_dict = {}
+ for address_index, address in enumerate(navi_addresses):
+ address_colors = {}
+ colors.append(address_colors)
+ for index, (navi_part, option, rainbow_symbol) in enumerate([
+ ('inner', navi_option_inner, rainbow_inner_symbol),
+ ('outer', navi_option_outer, rainbow_outer_symbol),
+ ]):
+ color = None
+
+ # set rainbow option
+ if rainbow_symbol is not None and option == 'Rainbow':
+ rom.write_byte(rainbow_symbol, 0x01)
+ color = [0x00, 0x00, 0x00]
+ elif rainbow_symbol is not None:
+ rom.write_byte(rainbow_symbol, 0x00)
+ elif option == 'Rainbow':
+ rainbow_error = "Rainbow Navi is not supported by this patch version. Using 'Completely Random' as a substitute."
+ option = 'Completely Random'
+
+ # completely random is random for every subgroup
+ if color is None and option == 'Completely Random':
+ color = generate_random_color()
+
+ # grab the color from the list
+ if color is None and option in NaviColors:
+ color = list(NaviColors[option][index])
+
+ # build color from hex code
+ if color is None:
+ color = hex_to_color(option)
+ option = 'Custom'
+
+ # Check color validity
+ if color is None:
+ raise Exception(f'Invalid {navi_part} color {option} for {navi_action}')
+
+ address_colors[navi_part] = color
+ option_dict[navi_part] = option
+
+ # write color
+ color = address_colors['inner'] + [0xFF] + address_colors['outer'] + [0xFF]
+ rom.write_bytes(address, color)
+
+
+ if rainbow_error:
+ logger.error(rainbow_error)
+
+
+def patch_sword_trails(rom, ootworld, symbols):
+ # patch sword trail duration
+ rom.write_byte(0x00BEFF8C, ootworld.sword_trail_duration)
+
+ # patch sword trail colors
+ sword_trails = [
+ ('Sword Trail', 'sword_trail_color',
+ [(0x00BEFF7C, 0xB0, 0x40, 0xB0, 0xFF), (0x00BEFF84, 0x20, 0x00, 0x10, 0x00)],
+ symbols.get('CFG_RAINBOW_SWORD_INNER_ENABLED', None), symbols.get('CFG_RAINBOW_SWORD_OUTER_ENABLED', None)),
+ ]
+
+ sword_trail_color_list = get_sword_trail_colors()
+ rainbow_error = None
+
+ for trail_name, trail_setting, trail_addresses, rainbow_inner_symbol, rainbow_outer_symbol in sword_trails:
+ option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner'])
+ option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer'])
+
+ # handle random choice
+ if option_inner == 'Random Choice':
+ option_inner = random.choice(sword_trail_color_list)
+ if option_outer == 'Random Choice':
+ option_outer = random.choice(sword_trail_color_list)
+
+ if option_outer == 'Match Inner':
+ option_outer = option_inner
+
+ colors = []
+ option_dict = {}
+ for address_index, (address, inner_transparency, inner_white_transparency, outer_transparency, outer_white_transparency) in enumerate(trail_addresses):
+ address_colors = {}
+ colors.append(address_colors)
+ transparency_dict = {}
+ for index, (trail_part, option, rainbow_symbol, white_transparency, transparency) in enumerate([
+ ('inner', option_inner, rainbow_inner_symbol, inner_white_transparency, inner_transparency),
+ ('outer', option_outer, rainbow_outer_symbol, outer_white_transparency, outer_transparency),
+ ]):
+ color = None
+
+ # set rainbow option
+ if rainbow_symbol is not None and option == 'Rainbow':
+ rom.write_byte(rainbow_symbol, 0x01)
+ color = [0x00, 0x00, 0x00]
+ elif rainbow_symbol is not None:
+ rom.write_byte(rainbow_symbol, 0x00)
+ elif option == 'Rainbow':
+ rainbow_error = "Rainbow Sword Trail is not supported by this patch version. Using 'Completely Random' as a substitute."
+ option = 'Completely Random'
+
+ # completely random is random for every subgroup
+ if color is None and option == 'Completely Random':
+ color = generate_random_color()
+
+ # grab the color from the list
+ if color is None and option in sword_trail_colors:
+ color = list(sword_trail_colors[option])
+
+ # build color from hex code
+ if color is None:
+ color = hex_to_color(option)
+ option = 'Custom'
+
+ # Check color validity
+ if color is None:
+ raise Exception(f'Invalid {trail_part} color {option} for {trail_name}')
+
+ # handle white transparency
+ if option == 'White':
+ transparency_dict[trail_part] = white_transparency
+ else:
+ transparency_dict[trail_part] = transparency
+
+ address_colors[trail_part] = color
+ option_dict[trail_part] = option
+
+ # write color
+ color = address_colors['outer'] + [transparency_dict['outer']] + address_colors['inner'] + [transparency_dict['inner']]
+ rom.write_bytes(address, color)
+
+ if rainbow_error:
+ logger.error(rainbow_error)
+
+
+def patch_bombchu_trails(rom, ootworld, symbols):
+ # patch bombchu trail colors
+ bombchu_trails = [
+ ('Bombchu Trail', 'bombchu_trail_color', get_bombchu_trail_colors(), bombchu_trail_colors,
+ (symbols['CFG_BOMBCHU_TRAIL_INNER_COLOR'], symbols['CFG_BOMBCHU_TRAIL_OUTER_COLOR'],
+ symbols['CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED'])),
+ ]
+
+ patch_trails(rom, ootworld, bombchu_trails)
+
+
+def patch_boomerang_trails(rom, ootworld, symbols):
+ # patch boomerang trail colors
+ boomerang_trails = [
+ ('Boomerang Trail', 'boomerang_trail_color', get_boomerang_trail_colors(), boomerang_trail_colors,
+ (symbols['CFG_BOOM_TRAIL_INNER_COLOR'], symbols['CFG_BOOM_TRAIL_OUTER_COLOR'],
+ symbols['CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED'], symbols['CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED'])),
+ ]
+
+ patch_trails(rom, ootworld, boomerang_trails)
+
+
+def patch_trails(rom, ootworld, trails):
+ for trail_name, trail_setting, trail_color_list, trail_color_dict, trail_symbols in trails:
+ color_inner_symbol, color_outer_symbol, rainbow_inner_symbol, rainbow_outer_symbol = trail_symbols
+ option_inner = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_inner'])
+ option_outer = format_cosmetic_option_result(ootworld.__dict__[trail_setting+'_outer'])
+
+ # handle random choice
+ if option_inner == 'Random Choice':
+ option_inner = random.choice(trail_color_list)
+ if option_outer == 'Random Choice':
+ option_outer = random.choice(trail_color_list)
+
+ if option_outer == 'Match Inner':
+ option_outer = option_inner
+
+ option_dict = {}
+ colors = {}
+
+ for index, (trail_part, option, rainbow_symbol, color_symbol) in enumerate([
+ ('inner', option_inner, rainbow_inner_symbol, color_inner_symbol),
+ ('outer', option_outer, rainbow_outer_symbol, color_outer_symbol),
+ ]):
+ color = None
+
+ # set rainbow option
+ if option == 'Rainbow':
+ rom.write_byte(rainbow_symbol, 0x01)
+ color = [0x00, 0x00, 0x00]
+ else:
+ rom.write_byte(rainbow_symbol, 0x00)
+
+ # handle completely random
+ if color is None and option == 'Completely Random':
+ # Specific handling for inner bombchu trails for contrast purposes.
+ if trail_name == 'Bombchu Trail' and trail_part == 'inner':
+ fixed_dark_color = [0, 0, 0]
+ color = [0, 0, 0]
+ # Avoid colors which have a low contrast so the bombchu ticking is still visible
+ while contrast_ratio(color, fixed_dark_color) <= 4:
+ color = generate_random_color()
+ else:
+ color = generate_random_color()
+
+ # grab the color from the list
+ if color is None and option in trail_color_dict:
+ color = list(trail_color_dict[option])
+
+ # build color from hex code
+ if color is None:
+ color = hex_to_color(option)
+ option = 'Custom'
+
+ option_dict[trail_part] = option
+ colors[trail_part] = color
+
+ # write color
+ rom.write_bytes(color_symbol, color)
+
+
+
+def patch_gauntlet_colors(rom, ootworld, symbols):
+ # patch gauntlet colors
+ gauntlets = [
+ ('Silver Gauntlets', 'silver_gauntlets_color', 0x00B6DA44,
+ ([0x173B4CC], [0x173B4D4, 0x173B50C, 0x173B514])), # GI Model DList colors
+ ('Gold Gauntlets', 'golden_gauntlets_color', 0x00B6DA47,
+ ([0x173B4EC], [0x173B4F4, 0x173B52C, 0x173B534])), # GI Model DList colors
+ ]
+ gauntlet_color_list = get_gauntlet_colors()
+
+ for gauntlet, gauntlet_setting, address, model_addresses in gauntlets:
+ gauntlet_option = format_cosmetic_option_result(ootworld.__dict__[gauntlet_setting])
+
+ # handle random
+ if gauntlet_option == 'Random Choice':
+ gauntlet_option = random.choice(gauntlet_color_list)
+ # handle completely random
+ if gauntlet_option == 'Completely Random':
+ color = generate_random_color()
+ # grab the color from the list
+ elif gauntlet_option in gauntlet_colors:
+ color = list(gauntlet_colors[gauntlet_option])
+ # build color from hex code
+ else:
+ color = hex_to_color(gauntlet_option)
+ gauntlet_option = 'Custom'
+ rom.write_bytes(address, color)
+ if ootworld.correct_model_colors:
+ patch_model_colors(rom, color, model_addresses)
+ else:
+ patch_model_colors(rom, None, model_addresses)
+
+def patch_shield_frame_colors(rom, ootworld, symbols):
+ # patch shield frame colors
+ shield_frames = [
+ ('Mirror Shield Frame', 'mirror_shield_frame_color',
+ [0xFA7274, 0xFA776C, 0xFAA27C, 0xFAC564, 0xFAC984, 0xFAEDD4],
+ ([0x1616FCC], [0x1616FD4])),
+ ]
+ shield_frame_color_list = get_shield_frame_colors()
+
+ for shield_frame, shield_frame_setting, addresses, model_addresses in shield_frames:
+ shield_frame_option = format_cosmetic_option_result(ootworld.__dict__[shield_frame_setting])
+
+ # handle random
+ if shield_frame_option == 'Random Choice':
+ shield_frame_option = random.choice(shield_frame_color_list)
+ # handle completely random
+ if shield_frame_option == 'Completely Random':
+ color = [random.getrandbits(8), random.getrandbits(8), random.getrandbits(8)]
+ # grab the color from the list
+ elif shield_frame_option in shield_frame_colors:
+ color = list(shield_frame_colors[shield_frame_option])
+ # build color from hex code
+ else:
+ color = hex_to_color(shield_frame_option)
+ shield_frame_option = 'Custom'
+ for address in addresses:
+ rom.write_bytes(address, color)
+ if ootworld.correct_model_colors and shield_frame_option != 'Red':
+ patch_model_colors(rom, color, model_addresses)
+ else:
+ patch_model_colors(rom, None, model_addresses)
+
+
+def patch_heart_colors(rom, ootworld, symbols):
+ # patch heart colors
+ hearts = [
+ ('Heart Color', 'heart_color', symbols['CFG_HEART_COLOR'], 0xBB0994,
+ ([0x14DA474, 0x14DA594, 0x14B701C, 0x14B70DC],
+ [0x14B70FC, 0x14DA494, 0x14DA5B4, 0x14B700C, 0x14B702C, 0x14B703C, 0x14B704C, 0x14B705C,
+ 0x14B706C, 0x14B707C, 0x14B708C, 0x14B709C, 0x14B70AC, 0x14B70BC, 0x14B70CC])), # GI Model DList colors
+ ]
+ heart_color_list = get_heart_colors()
+
+ for heart, heart_setting, symbol, file_select_address, model_addresses in hearts:
+ heart_option = format_cosmetic_option_result(ootworld.__dict__[heart_setting])
+
+ # handle random
+ if heart_option == 'Random Choice':
+ heart_option = random.choice(heart_color_list)
+ # handle completely random
+ if heart_option == 'Completely Random':
+ color = generate_random_color()
+ # grab the color from the list
+ elif heart_option in heart_colors:
+ color = list(heart_colors[heart_option])
+ # build color from hex code
+ else:
+ color = hex_to_color(heart_option)
+ heart_option = 'Custom'
+ rom.write_int16s(symbol, color) # symbol for ingame HUD
+ rom.write_int16s(file_select_address, color) # file select normal hearts
+ if heart_option != 'Red':
+ rom.write_int16s(file_select_address + 6, color) # file select DD hearts
+ else:
+ original_dd_color = rom.original.read_bytes(file_select_address + 6, 6)
+ rom.write_bytes(file_select_address + 6, original_dd_color)
+ if ootworld.correct_model_colors and heart_option != 'Red':
+ patch_model_colors(rom, color, model_addresses) # heart model colors
+ icon.patch_overworld_icon(rom, color, 0xF43D80) # Overworld Heart Icon
+ else:
+ patch_model_colors(rom, None, model_addresses)
+ icon.patch_overworld_icon(rom, None, 0xF43D80)
+
+def patch_magic_colors(rom, ootworld, symbols):
+ # patch magic colors
+ magic = [
+ ('Magic Meter Color', 'magic_color', symbols["CFG_MAGIC_COLOR"],
+ ([0x154C654, 0x154CFB4], [0x154C65C, 0x154CFBC])), # GI Model DList colors
+ ]
+ magic_color_list = get_magic_colors()
+
+ for magic_color, magic_setting, symbol, model_addresses in magic:
+ magic_option = format_cosmetic_option_result(ootworld.__dict__[magic_setting])
+
+ if magic_option == 'Random Choice':
+ magic_option = random.choice(magic_color_list)
+
+ if magic_option == 'Completely Random':
+ color = generate_random_color()
+ elif magic_option in magic_colors:
+ color = list(magic_colors[magic_option])
+ else:
+ color = hex_to_color(magic_option)
+ magic_option = 'Custom'
+ rom.write_int16s(symbol, color)
+ if magic_option != 'Green' and ootworld.correct_model_colors:
+ patch_model_colors(rom, color, model_addresses)
+ icon.patch_overworld_icon(rom, color, 0xF45650, data_path('icons/magicSmallExtras.raw')) # Overworld Small Pot
+ icon.patch_overworld_icon(rom, color, 0xF47650, data_path('icons/magicLargeExtras.raw')) # Overworld Big Pot
+ else:
+ patch_model_colors(rom, None, model_addresses)
+ icon.patch_overworld_icon(rom, None, 0xF45650)
+ icon.patch_overworld_icon(rom, None, 0xF47650)
+
+def patch_button_colors(rom, ootworld, symbols):
+ buttons = [
+ ('A Button Color', 'a_button_color', a_button_colors,
+ [('A Button Color', symbols['CFG_A_BUTTON_COLOR'],
+ None),
+ ('Text Cursor Color', symbols['CFG_TEXT_CURSOR_COLOR'],
+ [(0xB88E81, 0xB88E85, 0xB88E9)]), # Initial Inner Color
+ ('Shop Cursor Color', symbols['CFG_SHOP_CURSOR_COLOR'],
+ None),
+ ('Save/Death Cursor Color', None,
+ [(0xBBEBC2, 0xBBEBC3, 0xBBEBD6), (0xBBEDDA, 0xBBEDDB, 0xBBEDDE)]), # Save Cursor / Death Cursor
+ ('Pause Menu A Cursor Color', None,
+ [(0xBC7849, 0xBC784B, 0xBC784D), (0xBC78A9, 0xBC78AB, 0xBC78AD), (0xBC78BB, 0xBC78BD, 0xBC78BF)]), # Inner / Pulse 1 / Pulse 2
+ ('Pause Menu A Icon Color', None,
+ [(0x845754, 0x845755, 0x845756)]),
+ ('A Note Color', symbols['CFG_A_NOTE_COLOR'], # For Textbox Song Display
+ [(0xBB299A, 0xBB299B, 0xBB299E), (0xBB2C8E, 0xBB2C8F, 0xBB2C92), (0xBB2F8A, 0xBB2F8B, 0xBB2F96)]), # Pause Menu Song Display
+ ]),
+ ('B Button Color', 'b_button_color', b_button_colors,
+ [('B Button Color', symbols['CFG_B_BUTTON_COLOR'],
+ None),
+ ]),
+ ('C Button Color', 'c_button_color', c_button_colors,
+ [('C Button Color', symbols['CFG_C_BUTTON_COLOR'],
+ None),
+ ('Pause Menu C Cursor Color', None,
+ [(0xBC7843, 0xBC7845, 0xBC7847), (0xBC7891, 0xBC7893, 0xBC7895), (0xBC78A3, 0xBC78A5, 0xBC78A7)]), # Inner / Pulse 1 / Pulse 2
+ ('Pause Menu C Icon Color', None,
+ [(0x8456FC, 0x8456FD, 0x8456FE)]),
+ ('C Note Color', symbols['CFG_C_NOTE_COLOR'], # For Textbox Song Display
+ [(0xBB2996, 0xBB2997, 0xBB29A2), (0xBB2C8A, 0xBB2C8B, 0xBB2C96), (0xBB2F86, 0xBB2F87, 0xBB2F9A)]), # Pause Menu Song Display
+ ]),
+ ('Start Button Color', 'start_button_color', start_button_colors,
+ [('Start Button Color', None,
+ [(0xAE9EC6, 0xAE9EC7, 0xAE9EDA)]),
+ ]),
+ ]
+
+ for button, button_setting, button_colors, patches in buttons:
+ button_option = format_cosmetic_option_result(ootworld.__dict__[button_setting])
+ color_set = None
+ colors = {}
+
+ # handle random
+ if button_option == 'Random Choice':
+ button_option = random.choice(list(button_colors.keys()))
+ # handle completely random
+ if button_option == 'Completely Random':
+ fixed_font_color = [10, 10, 10]
+ color = [0, 0, 0]
+ # Avoid colors which have a low contrast with the font inside buttons (eg. the A letter)
+ while contrast_ratio(color, fixed_font_color) <= 3:
+ color = generate_random_color()
+ # grab the color from the list
+ elif button_option in button_colors:
+ color_set = [button_colors[button_option]] if isinstance(button_colors[button_option][0], int) else list(button_colors[button_option])
+ color = color_set[0]
+ # build color from hex code
+ else:
+ color = hex_to_color(button_option)
+ button_option = 'Custom'
+
+ # apply all button color patches
+ for i, (patch, symbol, byte_addresses) in enumerate(patches):
+ if color_set is not None and len(color_set) > i and color_set[i]:
+ colors[patch] = color_set[i]
+ else:
+ colors[patch] = color
+
+ if symbol:
+ rom.write_int16s(symbol, colors[patch])
+
+ if byte_addresses:
+ for r_addr, g_addr, b_addr in byte_addresses:
+ rom.write_byte(r_addr, colors[patch][0])
+ rom.write_byte(g_addr, colors[patch][1])
+ rom.write_byte(b_addr, colors[patch][2])
+
+
+def patch_sfx(rom, ootworld, symbols):
+ # Configurable Sound Effects
+ sfx_config = [
+ ('sfx_navi_overworld', sfx.SoundHooks.NAVI_OVERWORLD),
+ ('sfx_navi_enemy', sfx.SoundHooks.NAVI_ENEMY),
+ ('sfx_low_hp', sfx.SoundHooks.HP_LOW),
+ ('sfx_menu_cursor', sfx.SoundHooks.MENU_CURSOR),
+ ('sfx_menu_select', sfx.SoundHooks.MENU_SELECT),
+ ('sfx_nightfall', sfx.SoundHooks.NIGHTFALL),
+ ('sfx_horse_neigh', sfx.SoundHooks.HORSE_NEIGH),
+ ('sfx_hover_boots', sfx.SoundHooks.BOOTS_HOVER),
+ ]
+ sound_dict = sfx.get_patch_dict()
+ sounds_keyword_label = {sound.value.keyword: sound.value.label for sound in sfx.Sounds}
+ sounds_label_keyword = {sound.value.label: sound.value.keyword for sound in sfx.Sounds}
+
+ for setting, hook in sfx_config:
+ selection = ootworld.__dict__[setting].replace('_', '-')
+
+ if selection == 'default':
+ for loc in hook.value.locations:
+ sound_id = rom.original.read_int16(loc)
+ rom.write_int16(loc, sound_id)
+ else:
+ if selection == 'random-choice':
+ selection = random.choice(sfx.get_hook_pool(hook)).value.keyword
+ elif selection == 'random-ear-safe':
+ selection = random.choice(sfx.get_hook_pool(hook, "TRUE")).value.keyword
+ elif selection == 'completely-random':
+ selection = random.choice(sfx.standard).value.keyword
+ sound_id = sound_dict[selection]
+ for loc in hook.value.locations:
+ rom.write_int16(loc, sound_id)
+
+
+
+def patch_instrument(rom, ootworld, symbols):
+ # Player Instrument
+ instruments = {
+ #'none': 0x00,
+ 'ocarina': 0x01,
+ 'malon': 0x02,
+ 'whistle': 0x03,
+ 'harp': 0x04,
+ 'grind_organ': 0x05,
+ 'flute': 0x06,
+ #'another_ocarina': 0x07,
+ }
+
+ choice = ootworld.sfx_ocarina
+ if choice == 'random-choice':
+ choice = random.choice(list(instruments.keys()))
+
+ rom.write_byte(0x00B53C7B, instruments[choice])
+ rom.write_byte(0x00B4BF6F, instruments[choice]) # For Lost Woods Skull Kids' minigame in Lost Woods
+
+
+legacy_cosmetic_data_headers = [
+ 0x03481000,
+ 0x03480810,
+]
+
+global_patch_sets = [
+ patch_targeting,
+ patch_music,
+ patch_tunic_colors,
+ patch_navi_colors,
+ patch_sword_trails,
+ patch_gauntlet_colors,
+ patch_shield_frame_colors,
+ patch_sfx,
+ patch_instrument,
+]
+
+patch_sets = {
+ 0x1F04FA62: {
+ "patches": [
+ patch_dpad,
+ patch_sword_trails,
+ ],
+ "symbols": {
+ "CFG_DISPLAY_DPAD": 0x0004,
+ "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005,
+ "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006,
+ },
+ },
+ 0x1F05D3F9: {
+ "patches": [
+ patch_dpad,
+ patch_sword_trails,
+ ],
+ "symbols": {
+ "CFG_DISPLAY_DPAD": 0x0004,
+ "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0005,
+ "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0006,
+ },
+ },
+ 0x1F0693FB: {
+ "patches": [
+ patch_dpad,
+ patch_sword_trails,
+ patch_heart_colors,
+ patch_magic_colors,
+ ],
+ "symbols": {
+ "CFG_MAGIC_COLOR": 0x0004,
+ "CFG_HEART_COLOR": 0x000A,
+ "CFG_DISPLAY_DPAD": 0x0010,
+ "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0011,
+ "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0012,
+ }
+ },
+ 0x1F073FC9: {
+ "patches": [
+ patch_dpad,
+ patch_sword_trails,
+ patch_heart_colors,
+ patch_magic_colors,
+ patch_button_colors,
+ ],
+ "symbols": {
+ "CFG_MAGIC_COLOR": 0x0004,
+ "CFG_HEART_COLOR": 0x000A,
+ "CFG_A_BUTTON_COLOR": 0x0010,
+ "CFG_B_BUTTON_COLOR": 0x0016,
+ "CFG_C_BUTTON_COLOR": 0x001C,
+ "CFG_TEXT_CURSOR_COLOR": 0x0022,
+ "CFG_SHOP_CURSOR_COLOR": 0x0028,
+ "CFG_A_NOTE_COLOR": 0x002E,
+ "CFG_C_NOTE_COLOR": 0x0034,
+ "CFG_DISPLAY_DPAD": 0x003A,
+ "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x003B,
+ "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x003C,
+ }
+ },
+ 0x1F073FD8: {
+ "patches": [
+ patch_dpad,
+ patch_navi_colors,
+ patch_sword_trails,
+ patch_heart_colors,
+ patch_magic_colors,
+ patch_button_colors,
+ patch_boomerang_trails,
+ patch_bombchu_trails,
+ ],
+ "symbols": {
+ "CFG_MAGIC_COLOR": 0x0004,
+ "CFG_HEART_COLOR": 0x000A,
+ "CFG_A_BUTTON_COLOR": 0x0010,
+ "CFG_B_BUTTON_COLOR": 0x0016,
+ "CFG_C_BUTTON_COLOR": 0x001C,
+ "CFG_TEXT_CURSOR_COLOR": 0x0022,
+ "CFG_SHOP_CURSOR_COLOR": 0x0028,
+ "CFG_A_NOTE_COLOR": 0x002E,
+ "CFG_C_NOTE_COLOR": 0x0034,
+ "CFG_BOOM_TRAIL_INNER_COLOR": 0x003A,
+ "CFG_BOOM_TRAIL_OUTER_COLOR": 0x003D,
+ "CFG_BOMBCHU_TRAIL_INNER_COLOR": 0x0040,
+ "CFG_BOMBCHU_TRAIL_OUTER_COLOR": 0x0043,
+ "CFG_DISPLAY_DPAD": 0x0046,
+ "CFG_RAINBOW_SWORD_INNER_ENABLED": 0x0047,
+ "CFG_RAINBOW_SWORD_OUTER_ENABLED": 0x0048,
+ "CFG_RAINBOW_BOOM_TRAIL_INNER_ENABLED": 0x0049,
+ "CFG_RAINBOW_BOOM_TRAIL_OUTER_ENABLED": 0x004A,
+ "CFG_RAINBOW_BOMBCHU_TRAIL_INNER_ENABLED": 0x004B,
+ "CFG_RAINBOW_BOMBCHU_TRAIL_OUTER_ENABLED": 0x004C,
+ "CFG_RAINBOW_NAVI_IDLE_INNER_ENABLED": 0x004D,
+ "CFG_RAINBOW_NAVI_IDLE_OUTER_ENABLED": 0x004E,
+ "CFG_RAINBOW_NAVI_ENEMY_INNER_ENABLED": 0x004F,
+ "CFG_RAINBOW_NAVI_ENEMY_OUTER_ENABLED": 0x0050,
+ "CFG_RAINBOW_NAVI_NPC_INNER_ENABLED": 0x0051,
+ "CFG_RAINBOW_NAVI_NPC_OUTER_ENABLED": 0x0052,
+ "CFG_RAINBOW_NAVI_PROP_INNER_ENABLED": 0x0053,
+ "CFG_RAINBOW_NAVI_PROP_OUTER_ENABLED": 0x0054,
+ }
+ },
+}
+
+
+def patch_cosmetics(ootworld, rom):
+ # Use the world's slot seed for cosmetics
+ random.seed(ootworld.world.slot_seeds[ootworld.player])
+
+ # try to detect the cosmetic patch data format
+ versioned_patch_set = None
+ cosmetic_context = rom.read_int32(rom.sym('RANDO_CONTEXT') + 4)
+ if cosmetic_context >= 0x80000000 and cosmetic_context <= 0x80F7FFFC:
+ cosmetic_context = (cosmetic_context - 0x80400000) + 0x3480000 # convert from RAM to ROM address
+ cosmetic_version = rom.read_int32(cosmetic_context)
+ versioned_patch_set = patch_sets.get(cosmetic_version)
+ else:
+ # If cosmetic_context is not a valid pointer, then try to
+ # search over all possible legacy header locations.
+ for header in legacy_cosmetic_data_headers:
+ cosmetic_context = header
+ cosmetic_version = rom.read_int32(cosmetic_context)
+ if cosmetic_version in patch_sets:
+ versioned_patch_set = patch_sets[cosmetic_version]
+ break
+
+ # patch version specific patches
+ if versioned_patch_set:
+ # offset the cosmetic_context struct for absolute addressing
+ cosmetic_context_symbols = {
+ sym: address + cosmetic_context
+ for sym, address in versioned_patch_set['symbols'].items()
+ }
+
+ # warn if patching a legacy format
+ if cosmetic_version != rom.read_int32(rom.sym('COSMETIC_FORMAT_VERSION')):
+ logger.error("ROM uses old cosmetic patch format.")
+
+ # patch cosmetics that use vanilla oot data, and always compatible
+ for patch_func in [patch for patch in global_patch_sets if patch not in versioned_patch_set['patches']]:
+ patch_func(rom, ootworld, {})
+
+ for patch_func in versioned_patch_set['patches']:
+ patch_func(rom, ootworld, cosmetic_context_symbols)
+ else:
+ # patch cosmetics that use vanilla oot data, and always compatible
+ for patch_func in global_patch_sets:
+ patch_func(rom, ootworld, {})
+
+ # Unknown patch format
+ logger.error("Unable to patch some cosmetics. ROM uses unknown cosmetic patch format.")
diff --git a/worlds/oot/Dungeon.py b/worlds/oot/Dungeon.py
new file mode 100644
index 00000000..6e227baa
--- /dev/null
+++ b/worlds/oot/Dungeon.py
@@ -0,0 +1,56 @@
+class Dungeon(object):
+
+ def __init__(self, world, name, hint, boss_key, small_keys, dungeon_items):
+ def to_array(obj):
+ if obj == None:
+ return []
+ if isinstance(obj, list):
+ return obj
+ else:
+ return [obj]
+
+ self.world = world
+ self.name = name
+ self.hint_text = hint
+ self.regions = []
+ self.boss_key = to_array(boss_key)
+ self.small_keys = to_array(small_keys)
+ self.dungeon_items = to_array(dungeon_items)
+
+ for region in world.world.regions:
+ if region.player == world.player and region.dungeon == self.name:
+ region.dungeon = self
+ self.regions.append(region)
+
+
+ def copy(self, new_world):
+ new_boss_key = [item.copy(new_world) for item in self.boss_key]
+ new_small_keys = [item.copy(new_world) for item in self.small_keys]
+ new_dungeon_items = [item.copy(new_world) for item in self.dungeon_items]
+
+ new_dungeon = Dungeon(new_world, self.name, self.hint, new_boss_key, new_small_keys, new_dungeon_items)
+
+ return new_dungeon
+
+
+ @property
+ def keys(self):
+ return self.small_keys + self.boss_key
+
+
+ @property
+ def all_items(self):
+ return self.dungeon_items + self.keys
+
+
+ def is_dungeon_item(self, item):
+ return item.name in [dungeon_item.name for dungeon_item in self.all_items]
+
+
+ def __str__(self):
+ return str(self.__unicode__())
+
+
+ def __unicode__(self):
+ return '%s' % self.name
+
diff --git a/worlds/oot/DungeonList.py b/worlds/oot/DungeonList.py
new file mode 100644
index 00000000..45ac4a72
--- /dev/null
+++ b/worlds/oot/DungeonList.py
@@ -0,0 +1,129 @@
+import os
+
+from .Dungeon import Dungeon
+from .Utils import data_path
+
+
+dungeon_table = [
+ {
+ 'name': 'Deku Tree',
+ 'boss_key': 0,
+ 'small_key': 0,
+ 'small_key_mq': 0,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Dodongos Cavern',
+ 'hint': 'Dodongo\'s Cavern',
+ 'boss_key': 0,
+ 'small_key': 0,
+ 'small_key_mq': 0,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Jabu Jabus Belly',
+ 'hint': 'Jabu Jabu\'s Belly',
+ 'boss_key': 0,
+ 'small_key': 0,
+ 'small_key_mq': 0,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Forest Temple',
+ 'boss_key': 1,
+ 'small_key': 5,
+ 'small_key_mq': 6,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Bottom of the Well',
+ 'boss_key': 0,
+ 'small_key': 3,
+ 'small_key_mq': 2,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Fire Temple',
+ 'boss_key': 1,
+ 'small_key': 8,
+ 'small_key_mq': 5,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Ice Cavern',
+ 'boss_key': 0,
+ 'small_key': 0,
+ 'small_key_mq': 0,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Water Temple',
+ 'boss_key': 1,
+ 'small_key': 6,
+ 'small_key_mq': 2,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Shadow Temple',
+ 'boss_key': 1,
+ 'small_key': 5,
+ 'small_key_mq': 6,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Gerudo Training Grounds',
+ 'boss_key': 0,
+ 'small_key': 9,
+ 'small_key_mq': 3,
+ 'dungeon_item': 0,
+ },
+ {
+ 'name': 'Spirit Temple',
+ 'boss_key': 1,
+ 'small_key': 5,
+ 'small_key_mq': 7,
+ 'dungeon_item': 1,
+ },
+ {
+ 'name': 'Ganons Castle',
+ 'hint': 'Ganon\'s Castle',
+ 'boss_key': 1,
+ 'small_key': 2,
+ 'small_key_mq': 3,
+ 'dungeon_item': 0,
+ },
+]
+
+
+def create_dungeons(ootworld):
+ ootworld.dungeons = []
+ for dungeon_info in dungeon_table:
+ name = dungeon_info['name']
+ hint = dungeon_info['hint'] if 'hint' in dungeon_info else name
+
+ if ootworld.logic_rules == 'glitchless':
+ if not ootworld.dungeon_mq[name]:
+ dungeon_json = os.path.join(data_path('World'), name + '.json')
+ else:
+ dungeon_json = os.path.join(data_path('World'), name + ' MQ.json')
+ else:
+ if not ootworld.dungeon_mq[name]:
+ dungeon_json = os.path.join(data_path('Glitched World'), name + '.json')
+ else:
+ dungeon_json = os.path.join(data_path('Glitched World'), name + ' MQ.json')
+
+
+ ootworld.load_regions_from_json(dungeon_json)
+
+ boss_keys = [ootworld.create_item(f'Boss Key ({name})') for i in range(dungeon_info['boss_key'])]
+ if not ootworld.dungeon_mq[dungeon_info['name']]:
+ small_keys = [ootworld.create_item(f'Small Key ({name})') for i in range(dungeon_info['small_key'])]
+ else:
+ small_keys = [ootworld.create_item(f'Small Key ({name})') for i in range(dungeon_info['small_key_mq'])]
+ dungeon_items = [ootworld.create_item(f'Map ({name})'), ootworld.create_item(f'Compass ({name})')] * dungeon_info['dungeon_item']
+ if ootworld.shuffle_mapcompass in ['any_dungeon', 'overworld']:
+ for item in dungeon_items:
+ item.priority = True
+
+ ootworld.dungeons.append(Dungeon(ootworld, name, hint, boss_keys, small_keys, dungeon_items))
+
diff --git a/worlds/oot/Entrance.py b/worlds/oot/Entrance.py
new file mode 100644
index 00000000..310fcee4
--- /dev/null
+++ b/worlds/oot/Entrance.py
@@ -0,0 +1,19 @@
+
+from BaseClasses import Entrance
+from .Regions import TimeOfDay
+
+class OOTEntrance(Entrance):
+ game: str = 'Ocarina of Time'
+
+ def __init__(self, player, name='', parent=None):
+ super(OOTEntrance, self).__init__(player, name, parent)
+ self.access_rules = []
+ self.reverse = None
+ self.replaces = None
+ self.assumed = None
+ self.type = None
+ self.shuffled = False
+ self.data = None
+ self.primary = False
+ self.always = False
+ self.never = False
diff --git a/worlds/oot/EntranceShuffle.py b/worlds/oot/EntranceShuffle.py
new file mode 100644
index 00000000..f1e91773
--- /dev/null
+++ b/worlds/oot/EntranceShuffle.py
@@ -0,0 +1,25 @@
+
+def shuffle_random_entrances(ootworld):
+ world = ootworld.world
+ player = ootworld.player
+
+ # Gather locations to keep reachable for validation
+
+ # Set entrance data for all entrances
+
+ # Determine entrance pools based on settings
+
+ # Mark shuffled entrances
+
+ # Build target entrance pools
+
+ # Place priority entrances
+
+ # Delete priority targets from one-way pools
+
+ # Shuffle all entrance pools, in order
+
+ # Verification steps:
+ # All entrances are properly connected to a region
+ # Game is beatable
+ # Validate world
diff --git a/worlds/oot/HintList.py b/worlds/oot/HintList.py
new file mode 100644
index 00000000..a53f9db3
--- /dev/null
+++ b/worlds/oot/HintList.py
@@ -0,0 +1,1292 @@
+import random
+
+# Abbreviations
+# DMC Death Mountain Crater
+# DMT Death Mountain Trail
+# GC Goron City
+# GF Gerudo Fortress
+# GS Gold Skulltula
+# GV Gerudo Valley
+# HC Hyrule Castle
+# HF Hyrule Field
+# KF Kokiri Forest
+# LH Lake Hylia
+# LLR Lon Lon Ranch
+# LW Lost Woods
+# OGC Outside Ganon's Castle
+# SFM Sacred Forest Meadow
+# ZD Zora's Domain
+# ZF Zora's Fountain
+# ZR Zora's River
+
+class Hint(object):
+ name = ""
+ text = ""
+ type = []
+
+ def __init__(self, name, text, type, choice=None):
+ self.name = name
+ self.type = [type] if not isinstance(type, list) else type
+
+ if isinstance(text, str):
+ self.text = text
+ else:
+ if choice == None:
+ self.text = random.choice(text)
+ else:
+ self.text = text[choice]
+
+
+def getHint(item, clearer_hint=False):
+ if item in hintTable:
+ textOptions, clearText, hintType = hintTable[item]
+ if clearer_hint:
+ if clearText == None:
+ return Hint(item, textOptions, hintType, 0)
+ return Hint(item, clearText, hintType)
+ else:
+ return Hint(item, textOptions, hintType)
+ elif type(item) is str:
+ return Hint(item, item, 'generic')
+ else: # is an Item
+ return Hint(item.name, item.hint_text, 'item')
+
+
+def getHintGroup(group, world):
+ ret = []
+ for name in hintTable:
+
+ hint = getHint(name, world.clearer_hints)
+
+ if hint.name in world.always_hints and group == 'always':
+ hint.type = 'always'
+
+ # Hint inclusion override from distribution
+ if group in world.added_hint_types or group in world.item_added_hint_types:
+ if hint.name in world.added_hint_types[group]:
+ hint.type = group
+ if nameIsLocation(name, hint.type, world):
+ location = world.get_location(name)
+ for i in world.item_added_hint_types[group]:
+ if i == location.item.name:
+ hint.type = group
+ for i in world.item_hint_type_overrides[group]:
+ if i == location.item.name:
+ hint.type = []
+ type_override = False
+ if group in world.hint_type_overrides:
+ if name in world.hint_type_overrides[group]:
+ type_override = True
+ if group in world.item_hint_type_overrides:
+ if nameIsLocation(name, hint.type, world):
+ location = world.get_location(name)
+ if location.item.name in world.item_hint_type_overrides[group]:
+ type_override = True
+
+ if group in hint.type and (name not in hintExclusions(world)) and not type_override:
+ ret.append(hint)
+ return ret
+
+
+def getRequiredHints(world):
+ ret = []
+ for name in hintTable:
+ hint = getHint(name)
+ if 'always' in hint.type or hint.name in conditional_always and conditional_always[hint.name](world):
+ ret.append(hint)
+ return ret
+
+
+# Helpers for conditional always hints
+def stones_required_by_settings(world):
+ stones = 0
+ if world.bridge == 'stones':
+ stones = max(stones, world.bridge_stones)
+ if world.shuffle_ganon_bosskey == 'on_lacs' and world.lacs_condition == 'stones':
+ stones = max(stones, world.lacs_stones)
+ if world.bridge == 'dungeons':
+ stones = max(stones, world.bridge_rewards - 6)
+ if world.shuffle_ganon_bosskey == 'on_lacs' and world.lacs_condition == 'dungeons':
+ stones = max(stones, world.lacs_rewards - 6)
+
+ return stones
+
+
+def medallions_required_by_settings(world):
+ medallions = 0
+ if world.bridge == 'medallions':
+ medallions = max(medallions, world.bridge_medallions)
+ if world.shuffle_ganon_bosskey == 'on_lacs' and world.lacs_condition == 'medallions':
+ medallions = max(medallions, world.lacs_medallions)
+ if world.bridge == 'dungeons':
+ medallions = max(medallions, max(world.bridge_rewards - 3, 0))
+ if world.shuffle_ganon_bosskey == 'on_lacs' and world.lacs_condition == 'dungeons':
+ medallions = max(medallions, max(world.lacs_rewards - 3, 0))
+
+ return medallions
+
+
+def tokens_required_by_settings(world):
+ tokens = 0
+ if world.bridge == 'tokens':
+ tokens = max(tokens, world.bridge_tokens)
+ if world.shuffle_ganon_bosskey == 'on_lacs' and world.lacs_condition == 'tokens':
+ tokens = max(tokens, world.lacs_tokens)
+
+ return tokens
+
+
+# Hints required under certain settings
+conditional_always = {
+ 'Market 10 Big Poes': lambda world: world.big_poe_count > 3,
+ 'Deku Theater Mask of Truth': lambda world: not world.complete_mask_quest,
+ 'Song from Ocarina of Time': lambda world: stones_required_by_settings(world) < 2,
+ 'HF Ocarina of Time Item': lambda world: stones_required_by_settings(world) < 2,
+ 'Sheik in Kakariko': lambda world: medallions_required_by_settings(world) < 5,
+ 'DMT Biggoron': lambda world: world.logic_earliest_adult_trade != 'claim_check' or world.logic_latest_adult_trade != 'claim_check',
+ 'Kak 30 Gold Skulltula Reward': lambda world: tokens_required_by_settings(world) < 30,
+ 'Kak 40 Gold Skulltula Reward': lambda world: tokens_required_by_settings(world) < 40,
+ 'Kak 50 Gold Skulltula Reward': lambda world: tokens_required_by_settings(world) < 50,
+}
+
+
+# table of hints, format is (name, hint text, clear hint text, type of hint) there are special characters that are read for certain in game commands:
+# ^ is a box break
+# & is a new line
+# @ will print the player name
+# # sets color to white (currently only used for dungeon reward hints).
+hintTable = {
+ 'Triforce Piece': (["a triumph fork", "cheese", "a gold fragment"], "a Piece of the Triforce", "item"),
+ 'Magic Meter': (["mystic training", "pixie dust", "a green rectangle"], "a Magic Meter", 'item'),
+ 'Double Defense': (["a white outline", "damage decrease", "strengthened love"], "Double Defense", 'item'),
+ 'Slingshot': (["a seed shooter", "a rubberband", "a child's catapult"], "a Slingshot", 'item'),
+ 'Boomerang': (["a banana", "a stun stick"], "the Boomerang", 'item'),
+ 'Bow': (["an archery enabler", "a danger dart launcher"], "a Bow", 'item'),
+ 'Bomb Bag': (["an explosive container", "a blast bag"], "a Bomb Bag", 'item'),
+ 'Progressive Hookshot': (["Dampé's keepsake", "the Grapple Beam", "the BOING! chain"], "a Hookshot", 'item'),
+ 'Progressive Strength Upgrade': (["power gloves", "metal mittens", "the heavy lifty"], "a Strength Upgrade", 'item'),
+ 'Progressive Scale': (["a deeper dive", "a piece of Zora"], "a Zora Scale", 'item'),
+ 'Megaton Hammer': (["the dragon smasher", "the metal mallet", "the heavy hitter"], "the Megaton Hammer", 'item'),
+ 'Iron Boots': (["sink shoes", "clank cleats"], "the Iron Boots", 'item'),
+ 'Hover Boots': (["butter boots", "sacred slippers", "spacewalkers"], "the Hover Boots", 'item'),
+ 'Kokiri Sword': (["a butter knife", "a starter slasher", "a switchblade"], "the Kokiri Sword", 'item'),
+ 'Giants Knife': (["a fragile blade", "a breakable cleaver"], "the Giant's Knife", 'item'),
+ 'Biggoron Sword': (["the biggest blade", "a colossal cleaver"], "the Biggoron Sword", 'item'),
+ 'Master Sword': (["evil's bane"], "the Master Sword", 'item'),
+ 'Deku Shield': (["a wooden ward", "a burnable barrier"], "a Deku Shield", 'item'),
+ 'Hylian Shield': (["a steel safeguard", "Like Like's metal meal"], "a Hylian Shield", 'item'),
+ 'Mirror Shield': (["the reflective rampart", "Medusa's weakness", "a silvered surface"], "the Mirror Shield", 'item'),
+ 'Farores Wind': (["teleportation", "a relocation rune", "a green ball", "a green gust"], "Farore's Wind", 'item'),
+ 'Nayrus Love': (["a safe space", "an impregnable aura", "a blue barrier", "a blue crystal"], "Nayru's Love", 'item'),
+ 'Dins Fire': (["an inferno", "a heat wave", "a red ball"], "Din's Fire", 'item'),
+ 'Fire Arrows': (["the furnace firearm", "the burning bolts", "a magma missile"], "the Fire Arrows", 'item'),
+ 'Ice Arrows': (["the refrigerator rocket", "the frostbite bolts", "an iceberg maker"], "the Ice Arrows", 'item'),
+ 'Light Arrows': (["the shining shot", "the luminous launcher", "Ganondorf's bane", "the lighting bolts"], "the Light Arrows", 'item'),
+ 'Lens of Truth': (["a lie detector", "a ghost tracker", "true sight", "a detective's tool"], "the Lens of Truth", 'item'),
+ 'Ocarina': (["a flute", "a music maker"], "an Ocarina", 'item'),
+ 'Goron Tunic': (["ruby robes", "fireproof fabric", "cooking clothes"], "a Goron Tunic", 'item'),
+ 'Zora Tunic': (["a sapphire suit", "scuba gear", "a swimsuit"], "a Zora Tunic", 'item'),
+ 'Epona': (["a horse", "a four legged friend"], "Epona", 'item'),
+ 'Zeldas Lullaby': (["a song of royal slumber", "a triforce tune"], "Zelda's Lullaby", 'item'),
+ 'Eponas Song': (["an equestrian etude", "Malon's melody", "a ranch song"], "Epona's Song", 'item'),
+ 'Sarias Song': (["a song of dancing Gorons", "Saria's phone number"], "Saria's Song", 'item'),
+ 'Suns Song': (["Sunny Day", "the ReDead's bane", "the Gibdo's bane"], "the Sun's Song", 'item'),
+ 'Song of Time': (["a song 7 years long", "the tune of ages"], "the Song of Time", 'item'),
+ 'Song of Storms': (["Rain Dance", "a thunderstorm tune", "windmill acceleration"], "the Song of Storms", 'item'),
+ 'Minuet of Forest': (["the song of tall trees", "an arboreal anthem", "a green spark trail"], "the Minuet of Forest", 'item'),
+ 'Bolero of Fire': (["a song of lethal lava", "a red spark trail", "a volcanic verse"], "the Bolero of Fire", 'item'),
+ 'Serenade of Water': (["a song of a damp ditch", "a blue spark trail", "the lake's lyric"], "the Serenade of Water", 'item'),
+ 'Requiem of Spirit': (["a song of sandy statues", "an orange spark trail", "the desert ditty"], "the Requiem of Spirit", 'item'),
+ 'Nocturne of Shadow': (["a song of spooky spirits", "a graveyard boogie", "a haunted hymn", "a purple spark trail"], "the Nocturne of Shadow", 'item'),
+ 'Prelude of Light': (["a luminous prologue melody", "a yellow spark trail", "the temple traveler"], "the Prelude of Light", 'item'),
+ 'Bottle': (["a glass container", "an empty jar", "encased air"], "a Bottle", 'item'),
+ 'Rutos Letter': (["a call for help", "the note that Mweeps", "an SOS call", "a fishy stationery"], "Ruto's Letter", 'item'),
+ 'Bottle with Milk': (["cow juice", "a white liquid", "a baby's breakfast"], "a Milk Bottle", 'item'),
+ 'Bottle with Red Potion': (["a vitality vial", "a red liquid"], "a Red Potion Bottle", 'item'),
+ 'Bottle with Green Potion': (["a magic mixture", "a green liquid"], "a Green Potion Bottle", 'item'),
+ 'Bottle with Blue Potion': (["an ailment antidote", "a blue liquid"], "a Blue Potion Bottle", 'item'),
+ 'Bottle with Fairy': (["an imprisoned fairy", "an extra life", "Navi's cousin"], "a Fairy Bottle", 'item'),
+ 'Bottle with Fish': (["an aquarium", "a deity's snack"], "a Fish Bottle", 'item'),
+ 'Bottle with Blue Fire': (["a conflagration canteen", "an icemelt jar"], "a Blue Fire Bottle", 'item'),
+ 'Bottle with Bugs': (["an insectarium", "Skulltula finders"], "a Bug Bottle", 'item'),
+ 'Bottle with Poe': (["a spooky ghost", "a face in the jar"], "a Poe Bottle", 'item'),
+ 'Bottle with Big Poe': (["the spookiest ghost", "a sidequest spirit"], "a Big Poe Bottle", 'item'),
+ 'Stone of Agony': (["the shake stone", "the Rumble Pak (TM)"], "the Stone of Agony", 'item'),
+ 'Gerudo Membership Card': (["a girl club membership", "a desert tribe's pass"], "the Gerudo Card", 'item'),
+ 'Progressive Wallet': (["a mo' money holder", "a gem purse", "a portable bank"], "a Wallet", 'item'),
+ 'Deku Stick Capacity': (["a lumber rack", "more flammable twigs"], "Deku Stick Capacity", 'item'),
+ 'Deku Nut Capacity': (["more nuts", "flashbang storage"], "Deku Nut Capacity", 'item'),
+ 'Heart Container': (["a lot of love", "a Valentine's gift", "a boss's organ"], "a Heart Container", 'item'),
+ 'Piece of Heart': (["a little love", "a broken heart"], "a Piece of Heart", 'item'),
+ 'Piece of Heart (Treasure Chest Game)': ("a victory valentine", "a Piece of Heart", 'item'),
+ 'Recovery Heart': (["a free heal", "a hearty meal", "a Band-Aid"], "a Recovery Heart", 'item'),
+ 'Rupee (Treasure Chest Game)': ("the dollar of defeat", 'a Green Rupee', 'item'),
+ 'Deku Stick (1)': ("a breakable branch", 'a Deku Stick', 'item'),
+ 'Rupee (1)': (["a unique coin", "a penny", "a green gem"], "a Green Rupee", 'item'),
+ 'Rupees (5)': (["a common coin", "a blue gem"], "a Blue Rupee", 'item'),
+ 'Rupees (20)': (["couch cash", "a red gem"], "a Red Rupee", 'item'),
+ 'Rupees (50)': (["big bucks", "a purple gem", "wealth"], "a Purple Rupee", 'item'),
+ 'Rupees (200)': (["a juicy jackpot", "a yellow gem", "a giant gem", "great wealth"], "a Huge Rupee", 'item'),
+ 'Weird Egg': (["a chicken dilemma"], "the Weird Egg", 'item'),
+ 'Zeldas Letter': (["an autograph", "royal stationery", "royal snail mail"], "Zelda's Letter", 'item'),
+ 'Pocket Egg': (["a Cucco container", "a Cucco, eventually", "a fowl youth"], "the Pocket Egg", 'item'),
+ 'Pocket Cucco': (["a little clucker"], "the Pocket Cucco", 'item'),
+ 'Cojiro': (["a cerulean capon"], "Cojiro", 'item'),
+ 'Odd Mushroom': (["a powder ingredient"], "an Odd Mushroom", 'item'),
+ 'Odd Potion': (["Granny's goodies"], "an Odd Potion", 'item'),
+ 'Poachers Saw': (["a tree killer"], "the Poacher's Saw", 'item'),
+ 'Broken Sword': (["a shattered slicer"], "the Broken Sword", 'item'),
+ 'Prescription': (["a pill pamphlet", "a doctor's note"], "the Prescription", 'item'),
+ 'Eyeball Frog': (["a perceiving polliwog"], "the Eyeball Frog", 'item'),
+ 'Eyedrops': (["a vision vial"], "the Eyedrops", 'item'),
+ 'Claim Check': (["a three day wait"], "the Claim Check", 'item'),
+ 'Map': (["a dungeon atlas", "blueprints"], "a Map", 'item'),
+ 'Compass': (["a treasure tracker", "a magnetic needle"], "a Compass", 'item'),
+ 'BossKey': (["a master of unlocking", "a dungeon's master pass"], "a Boss Key", 'item'),
+ 'GanonBossKey': (["a master of unlocking", "a dungeon's master pass"], "a Boss Key", 'item'),
+ 'SmallKey': (["a tool for unlocking", "a dungeon pass", "a lock remover", "a lockpick"], "a Small Key", 'item'),
+ 'FortressSmallKey': (["a get out of jail free card"], "a Jail Key", 'item'),
+ 'KeyError': (["something mysterious", "an unknown treasure"], "An Error (Please Report This)", 'item'),
+ 'Arrows (5)': (["a few danger darts", "a few sharp shafts"], "Arrows (5 pieces)", 'item'),
+ 'Arrows (10)': (["some danger darts", "some sharp shafts"], "Arrows (10 pieces)", 'item'),
+ 'Arrows (30)': (["plenty of danger darts", "plenty of sharp shafts"], "Arrows (30 pieces)", 'item'),
+ 'Bombs (5)': (["a few explosives", "a few blast balls"], "Bombs (5 pieces)", 'item'),
+ 'Bombs (10)': (["some explosives", "some blast balls"], "Bombs (10 pieces)", 'item'),
+ 'Bombs (20)': (["lots-o-explosives", "plenty of blast balls"], "Bombs (20 pieces)", 'item'),
+ 'Ice Trap': (["a gift from Ganon", "a chilling discovery", "frosty fun"], "an Ice Trap", 'item'),
+ 'Magic Bean': (["a wizardly legume"], "a Magic Bean", 'item'),
+ 'Magic Bean Pack': (["wizardly legumes"], "Magic Beans", 'item'),
+ 'Bombchus': (["mice bombs", "proximity mice", "wall crawlers", "trail blazers"], "Bombchus", 'item'),
+ 'Bombchus (5)': (["a few mice bombs", "a few proximity mice", "a few wall crawlers", "a few trail blazers"], "Bombchus (5 pieces)", 'item'),
+ 'Bombchus (10)': (["some mice bombs", "some proximity mice", "some wall crawlers", "some trail blazers"], "Bombchus (10 pieces)", 'item'),
+ 'Bombchus (20)': (["plenty of mice bombs", "plenty of proximity mice", "plenty of wall crawlers", "plenty of trail blazers"], "Bombchus (20 pieces)", 'item'),
+ 'Deku Nuts (5)': (["some nuts", "some flashbangs", "some scrub spit"], "Deku Nuts (5 pieces)", 'item'),
+ 'Deku Nuts (10)': (["lots-o-nuts", "plenty of flashbangs", "plenty of scrub spit"], "Deku Nuts (10 pieces)", 'item'),
+ 'Deku Seeds (30)': (["catapult ammo", "lots-o-seeds"], "Deku Seeds (30 pieces)", 'item'),
+ 'Gold Skulltula Token': (["proof of destruction", "an arachnid chip", "spider remains", "one percent of a curse"], "a Gold Skulltula Token", 'item'),
+
+ 'ZR Frogs Ocarina Game': (["an #amphibian feast# yields", "the #croaking choir's magnum opus# awards", "the #froggy finale# yields"], "the final reward from the #Frogs of Zora's River# is", 'always'),
+ 'KF Links House Cow': ("the #bovine bounty of a horseback hustle# gifts", "#Malon's obstacle course# leads to", 'always'),
+
+ 'Song from Ocarina of Time': ("the #Ocarina of Time# teaches", None, ['song', 'sometimes']),
+ 'Song from Composers Grave': (["#ReDead in the Composers' Grave# guard", "the #Composer Brothers wrote#"], None, ['song', 'sometimes']),
+ 'Sheik in Forest': ("#in a meadow# Sheik teaches", None, ['song', 'sometimes']),
+ 'Sheik at Temple': ("Sheik waits at a #monument to time# to teach", None, ['song', 'sometimes']),
+ 'Sheik in Crater': ("the #crater's melody# is", None, ['song', 'sometimes']),
+ 'Sheik in Ice Cavern': ("the #frozen cavern# echoes with", None, ['song', 'sometimes']),
+ 'Sheik in Kakariko': ("a #ravaged village# mourns with", None, ['song', 'sometimes']),
+ 'Sheik at Colossus': ("a hero ventures #beyond the wasteland# to learn", None, ['song', 'sometimes']),
+
+ 'Market 10 Big Poes': ("#ghost hunters# will be rewarded with", "catching #Big Poes# leads to", ['overworld', 'sometimes']),
+ 'Deku Theater Skull Mask': ("the #Skull Mask# yields", None, ['overworld', 'sometimes']),
+ 'Deku Theater Mask of Truth': ("showing a #truthful eye to the crowd# rewards", "the #Mask of Truth# yields", ['overworld', 'sometimes']),
+ 'HF Ocarina of Time Item': ("the #treasure thrown by Princess Zelda# is", None, ['overworld', 'sometimes']),
+ 'DMT Biggoron': ("#Biggoron# crafts", None, ['overworld', 'sometimes']),
+ 'Kak 50 Gold Skulltula Reward': (["#50 bug badges# rewards", "#50 spider souls# yields", "#50 auriferous arachnids# lead to"], "slaying #50 Gold Skulltulas# reveals", ['overworld', 'sometimes']),
+ 'Kak 40 Gold Skulltula Reward': (["#40 bug badges# rewards", "#40 spider souls# yields", "#40 auriferous arachnids# lead to"], "slaying #40 Gold Skulltulas# reveals", ['overworld', 'sometimes']),
+ 'Kak 30 Gold Skulltula Reward': (["#30 bug badges# rewards", "#30 spider souls# yields", "#30 auriferous arachnids# lead to"], "slaying #30 Gold Skulltulas# reveals", ['overworld', 'sometimes']),
+ 'Kak 20 Gold Skulltula Reward': (["#20 bug badges# rewards", "#20 spider souls# yields", "#20 auriferous arachnids# lead to"], "slaying #20 Gold Skulltulas# reveals", ['overworld', 'sometimes']),
+ 'Kak Anju as Child': (["#wrangling roosters# rewards", "#chucking chickens# gifts"], "#collecting cuccos# rewards", ['overworld', 'sometimes']),
+ 'GC Darunias Joy': ("a #groovin' goron# gifts", "#Darunia's dance# leads to", ['overworld', 'sometimes']),
+ 'LW Skull Kid': ("the #Skull Kid# grants", None, ['overworld', 'sometimes']),
+ 'LH Sun': ("staring into #the sun# grants", "shooting #the sun# grants", ['overworld', 'sometimes']),
+ 'Market Treasure Chest Game Reward': (["#gambling# grants", "there is a #1/32 chance# to win"], "the #treasure chest game# grants", ['overworld', 'sometimes']),
+ 'GF HBA 1500 Points': ("mastery of #horseback archery# grants", "scoring 1500 in #horseback archery# grants", ['overworld', 'sometimes']),
+ 'Graveyard Heart Piece Grave Chest': ("playing #Sun's Song# in a grave spawns", None, ['overworld', 'sometimes']),
+ 'GC Maze Left Chest': ("in #Goron City# the hammer unlocks", None, ['overworld', 'sometimes']),
+ 'GV Chest': ("in #Gerudo Valley# the hammer unlocks", None, ['overworld', 'sometimes']),
+ 'GV Cow': ("a #cow in Gerudo Valley# gifts", None, ['overworld', 'sometimes']),
+ 'HC GS Storms Grotto': ("a #spider behind a muddy wall# in a grotto holds", None, ['overworld', 'sometimes']),
+ 'HF GS Cow Grotto': ("a #spider behind webs# in a grotto holds", None, ['overworld', 'sometimes']),
+ 'HF Cow Grotto Cow': ("the #cobwebbed cow# gifts", "a #cow behind webs# in a grotto gifts", ['overworld', 'sometimes']),
+ 'ZF GS Hidden Cave': ("a spider high #above the icy waters# holds", None, ['overworld', 'sometimes']),
+ 'Wasteland Chest': (["#deep in the wasteland# is", "beneath #the sands#, flames reveal"], None, ['overworld', 'sometimes']),
+ 'Wasteland GS': ("a #spider in the wasteland# holds", None, ['overworld', 'sometimes']),
+ 'Graveyard Composers Grave Chest': (["#flames in the Composers' Grave# reveal", "the #Composer Brothers hid#"], None, ['overworld', 'sometimes']),
+ 'ZF Bottom Freestanding PoH': ("#under the icy waters# lies", None, ['overworld', 'sometimes']),
+ 'GC Pot Freestanding PoH': ("spinning #Goron pottery# contains", None, ['overworld', 'sometimes']),
+ 'ZD King Zora Thawed': ("a #defrosted dignitary# gifts", "unfreezing #King Zora# grants", ['overworld', 'sometimes']),
+ 'DMC Deku Scrub': ("a single #scrub in the crater# sells", None, ['overworld', 'sometimes']),
+ 'DMC GS Crate': ("a spider under a #crate in the crater# holds", None, ['overworld', 'sometimes']),
+
+ 'Deku Tree MQ After Spinning Log Chest': ("a #temporal stone within a tree# contains", "a #temporal stone within the Deku Tree# contains", ['dungeon', 'sometimes']),
+ 'Deku Tree MQ GS Basement Graves Room': ("a #spider on a ceiling in a tree# holds", "a #spider on a ceiling in the Deku Tree# holds", ['dungeon', 'sometimes']),
+ 'Dodongos Cavern MQ GS Song of Time Block Room': ("a spider under #temporal stones in a cavern# holds", "a spider under #temporal stones in Dodongo's Cavern# holds", ['dungeon', 'sometimes']),
+ 'Jabu Jabus Belly Boomerang Chest': ("a school of #stingers swallowed by a deity# guard", "a school of #stingers swallowed by Jabu Jabu# guard", ['dungeon', 'sometimes']),
+ 'Jabu Jabus Belly MQ GS Invisible Enemies Room': ("a spider surrounded by #shadows in the belly of a deity# holds", "a spider surrounded by #shadows in Jabu Jabu's Belly# holds", ['dungeon', 'sometimes']),
+ 'Jabu Jabus Belly MQ Cow': ("a #cow swallowed by a deity# gifts", "a #cow swallowed by Jabu Jabu# gifts", ['dungeon', 'sometimes']),
+ 'Fire Temple Scarecrow Chest': ("a #scarecrow atop the volcano# hides", "#Pierre atop the Fire Temple# hides", ['dungeon', 'sometimes']),
+ 'Fire Temple Megaton Hammer Chest': ("the #Flare Dancer atop the volcano# guards a chest containing", "the #Flare Dancer atop the Fire Temple# guards a chest containing", ['dungeon', 'sometimes']),
+ 'Fire Temple MQ Chest On Fire': ("the #Flare Dancer atop the volcano# guards a chest containing", "the #Flare Dancer atop the Fire Temple# guards a chest containing", ['dungeon', 'sometimes']),
+ 'Fire Temple MQ GS Skull On Fire': ("a #spider under a block in the volcano# holds", "a #spider under a block in the Fire Temple# holds", ['dungeon', 'sometimes']),
+ 'Water Temple River Chest': ("beyond the #river under the lake# waits", "beyond the #river in the Water Temple# waits", ['dungeon', 'sometimes']),
+ 'Water Temple Boss Key Chest': ("dodging #rolling boulders under the lake# leads to", "dodging #rolling boulders in the Water Temple# leads to", ['dungeon', 'sometimes']),
+ 'Water Temple GS Behind Gate': ("a spider behind a #gate under the lake# holds", "a spider behind a #gate in the Water Temple# holds", ['dungeon', 'sometimes']),
+ 'Water Temple MQ Freestanding Key': ("hidden in a #box under the lake# lies", "hidden in a #box in the Water Temple# lies", ['dungeon', 'sometimes']),
+ 'Water Temple MQ GS Freestanding Key Area': ("the #locked spider under the lake# holds", "the #locked spider in the Water Temple# holds", ['dungeon', 'sometimes']),
+ 'Water Temple MQ GS Triple Wall Torch': ("a spider behind a #gate under the lake# holds", "a spider behind a #gate in the Water Temple# holds", ['dungeon', 'sometimes']),
+ 'Gerudo Training Grounds Underwater Silver Rupee Chest': (["those who seek #sunken silver rupees# will find", "the #thieves' underwater training# rewards"], None, ['dungeon', 'sometimes']),
+ 'Gerudo Training Grounds MQ Underwater Silver Rupee Chest': (["those who seek #sunken silver rupees# will find", "the #thieves' underwater training# rewards"], None, ['dungeon', 'sometimes']),
+ 'Gerudo Training Grounds Maze Path Final Chest': ("the final prize of #the thieves' training# is", None, ['dungeon', 'sometimes']),
+ 'Gerudo Training Grounds MQ Ice Arrows Chest': ("the final prize of #the thieves' training# is", None, ['dungeon', 'sometimes']),
+ 'Bottom of the Well Lens of Truth Chest': (["the well's #grasping ghoul# hides", "a #nether dweller in the well# holds"], "#Dead Hand in the well# holds", ['dungeon', 'sometimes']),
+ 'Bottom of the Well MQ Compass Chest': (["the well's #grasping ghoul# hides", "a #nether dweller in the well# holds"], "#Dead Hand in the well# holds", ['dungeon', 'sometimes']),
+ 'Spirit Temple Silver Gauntlets Chest': ("the treasure #sought by Nabooru# is", "upon the #Colossus's right hand# is", ['dungeon', 'sometimes']),
+ 'Spirit Temple Mirror Shield Chest': ("upon the #Colossus's left hand# is", None, ['dungeon', 'sometimes']),
+ 'Spirit Temple MQ Child Hammer Switch Chest': ("a #temporal paradox in the Colossus# yields", "a #temporal paradox in the Spirit Temple# yields", ['dungeon', 'sometimes']),
+ 'Spirit Temple MQ Symphony Room Chest': ("a #symphony in the Colossus# yields", "a #symphony in the Spirit Temple# yields", ['dungeon', 'sometimes']),
+ 'Spirit Temple MQ GS Symphony Room': ("a #spider's symphony in the Colossus# yields", "a #spider's symphony in the Spirit Temple# yields", ['dungeon', 'sometimes']),
+ 'Shadow Temple Invisible Floormaster Chest': ("shadows in an #invisible maze# guard", None, ['dungeon', 'sometimes']),
+ 'Shadow Temple MQ Bomb Flower Chest': ("shadows in an #invisible maze# guard", None, ['dungeon', 'sometimes']),
+
+ 'KF Kokiri Sword Chest': ("the #hidden treasure of the Kokiri# is", None, 'exclude'),
+ 'KF Midos Top Left Chest': ("the #leader of the Kokiri# hides", "#inside Mido's house# is", 'exclude'),
+ 'KF Midos Top Right Chest': ("the #leader of the Kokiri# hides", "#inside Mido's house# is", 'exclude'),
+ 'KF Midos Bottom Left Chest': ("the #leader of the Kokiri# hides", "#inside Mido's house# is", 'exclude'),
+ 'KF Midos Bottom Right Chest': ("the #leader of the Kokiri# hides", "#inside Mido's house# is", 'exclude'),
+ 'Graveyard Shield Grave Chest': ("the #treasure of a fallen soldier# is", None, 'exclude'),
+ 'DMT Chest': ("hidden behind a wall on a #mountain trail# is", None, 'exclude'),
+ 'GC Maze Right Chest': ("in #Goron City# explosives unlock", None, 'exclude'),
+ 'GC Maze Center Chest': ("in #Goron City# explosives unlock", None, 'exclude'),
+ 'ZD Chest': ("fire #beyond a waterfall# reveals", None, 'exclude'),
+ 'Graveyard Hookshot Chest': ("a chest hidden by a #speedy spectre# holds", "#dead Dampé's first prize# is", 'exclude'),
+ 'GF Chest': ("on a #rooftop in the desert# lies", None, 'exclude'),
+ 'Kak Redead Grotto Chest': ("#zombies beneath the earth# guard", None, 'exclude'),
+ 'SFM Wolfos Grotto Chest': ("#wolves beneath the earth# guard", None, 'exclude'),
+ 'HF Near Market Grotto Chest': ("a #hole in a field near a drawbridge# holds", None, 'exclude'),
+ 'HF Southeast Grotto Chest': ("a #hole amongst trees in a field# holds", None, 'exclude'),
+ 'HF Open Grotto Chest': ("an #open hole in a field# holds", None, 'exclude'),
+ 'Kak Open Grotto Chest': ("an #open hole in a town# holds", None, 'exclude'),
+ 'ZR Open Grotto Chest': ("a #hole along a river# holds", None, 'exclude'),
+ 'KF Storms Grotto Chest': ("a #hole in a forest village# holds", None, 'exclude'),
+ 'LW Near Shortcuts Grotto Chest': ("a #hole in a wooded maze# holds", None, 'exclude'),
+ 'DMT Storms Grotto Chest': ("#hole flooded with rain on a mountain# holds", None, 'exclude'),
+ 'DMC Upper Grotto Chest': ("a #hole in a volcano# holds", None, 'exclude'),
+
+ 'ToT Light Arrows Cutscene': ("the #final gift of a princess# is", None, 'exclude'),
+ 'LW Gift from Saria': (["a #potato hoarder# holds", "a rooty tooty #flutey cutey# gifts"], "#Saria's Gift# is", 'exclude'),
+ 'ZF Great Fairy Reward': ("the #fairy of winds# holds", None, 'exclude'),
+ 'HC Great Fairy Reward': ("the #fairy of fire# holds", None, 'exclude'),
+ 'Colossus Great Fairy Reward': ("the #fairy of love# holds", None, 'exclude'),
+ 'DMT Great Fairy Reward': ("a #magical fairy# gifts", None, 'exclude'),
+ 'DMC Great Fairy Reward': ("a #magical fairy# gifts", None, 'exclude'),
+ 'OGC Great Fairy Reward': ("the #fairy of strength# holds", None, 'exclude'),
+
+ 'Song from Impa': ("#deep in a castle#, Impa teaches", None, 'exclude'),
+ 'Song from Malon': ("#a farm girl# sings", None, 'exclude'),
+ 'Song from Saria': ("#deep in the forest#, Saria teaches", None, 'exclude'),
+ 'Song from Windmill': ("a man #in a windmill# is obsessed with", None, 'exclude'),
+
+ 'HC Malon Egg': ("a #girl looking for her father# gives", None, 'exclude'),
+ 'HC Zeldas Letter': ("a #princess in a castle# gifts", None, 'exclude'),
+ 'ZD Diving Minigame': ("an #unsustainable business model# gifts", "those who #dive for Zora rupees# will find", 'exclude'),
+ 'LH Child Fishing': ("#fishing in youth# bestows", None, 'exclude'),
+ 'LH Adult Fishing': ("#fishing in maturity# bestows", None, 'exclude'),
+ 'LH Lab Dive': ("a #diving experiment# is rewarded with", None, 'exclude'),
+ 'GC Rolling Goron as Adult': ("#comforting yourself# provides", "#reassuring a young Goron# is rewarded with", 'exclude'),
+ 'Market Bombchu Bowling First Prize': ("the #first explosive prize# is", None, 'exclude'),
+ 'Market Bombchu Bowling Second Prize': ("the #second explosive prize# is", None, 'exclude'),
+ 'Market Lost Dog': ("#puppy lovers# will find", "#rescuing Richard the Dog# is rewarded with", 'exclude'),
+ 'LW Ocarina Memory Game': (["the prize for a #game of Simon Says# is", "a #child sing-a-long# holds"], "#playing an Ocarina in Lost Woods# is rewarded with", 'exclude'),
+ 'Kak 10 Gold Skulltula Reward': (["#10 bug badges# rewards", "#10 spider souls# yields", "#10 auriferous arachnids# lead to"], "slaying #10 Gold Skulltulas# reveals", 'exclude'),
+ 'Kak Man on Roof': ("a #rooftop wanderer# holds", None, 'exclude'),
+ 'ZR Magic Bean Salesman': ("a seller of #colorful crops# has", "a #bean seller# offers", 'exclude'),
+ 'ZR Frogs in the Rain': ("#frogs in a storm# gift", None, 'exclude'),
+ 'GF HBA 1000 Points': ("scoring 1000 in #horseback archery# grants", None, 'exclude'),
+ 'Market Shooting Gallery Reward': ("#shooting in youth# grants", None, 'exclude'),
+ 'Kak Shooting Gallery Reward': ("#shooting in maturity# grants", None, 'exclude'),
+ 'LW Target in Woods': ("shooting a #target in the woods# grants", None, 'exclude'),
+ 'Kak Anju as Adult': ("a #chicken caretaker# offers adults", None, 'exclude'),
+ 'LLR Talons Chickens': ("#finding Super Cuccos# is rewarded with", None, 'exclude'),
+ 'GC Rolling Goron as Child': ("the prize offered by a #large rolling Goron# is", None, 'exclude'),
+ 'LH Underwater Item': ("the #sunken treasure in a lake# is", None, 'exclude'),
+ 'GF Gerudo Membership Card': ("#rescuing captured carpenters# is rewarded with", None, 'exclude'),
+ 'Wasteland Bombchu Salesman': ("a #carpet guru# sells", None, 'exclude'),
+
+ 'Kak Impas House Freestanding PoH': ("#imprisoned in a house# lies", None, 'exclude'),
+ 'HF Tektite Grotto Freestanding PoH': ("#deep underwater in a hole# is", None, 'exclude'),
+ 'Kak Windmill Freestanding PoH': ("on a #windmill ledge# lies", None, 'exclude'),
+ 'Graveyard Dampe Race Freestanding PoH': ("#racing a ghost# leads to", "#dead Dampé's second# prize is", 'exclude'),
+ 'LLR Freestanding PoH': ("in a #ranch silo# lies", None, 'exclude'),
+ 'Graveyard Freestanding PoH': ("a #crate in a graveyard# hides", None, 'exclude'),
+ 'Graveyard Dampe Gravedigging Tour': ("a #gravekeeper digs up#", None, 'exclude'),
+ 'ZR Near Open Grotto Freestanding PoH': ("on top of a #pillar in a river# lies", None, 'exclude'),
+ 'ZR Near Domain Freestanding PoH': ("on a #river ledge by a waterfall# lies", None, 'exclude'),
+ 'LH Freestanding PoH': ("high on a #lab rooftop# one can find", None, 'exclude'),
+ 'ZF Iceberg Freestanding PoH': ("#floating on ice# is", None, 'exclude'),
+ 'GV Waterfall Freestanding PoH': ("behind a #desert waterfall# is", None, 'exclude'),
+ 'GV Crate Freestanding PoH': ("a #crate in a valley# hides", None, 'exclude'),
+ 'Colossus Freestanding PoH': ("on top of an #arch of stone# lies", None, 'exclude'),
+ 'DMT Freestanding PoH': ("above a #mountain cavern entrance# is", None, 'exclude'),
+ 'DMC Wall Freestanding PoH': ("nestled in a #volcanic wall# is", None, 'exclude'),
+ 'DMC Volcano Freestanding PoH': ("obscured by #volcanic ash# is", None, 'exclude'),
+ 'GF North F1 Carpenter': ("#defeating Gerudo guards# reveals", None, 'exclude'),
+ 'GF North F2 Carpenter': ("#defeating Gerudo guards# reveals", None, 'exclude'),
+ 'GF South F1 Carpenter': ("#defeating Gerudo guards# reveals", None, 'exclude'),
+ 'GF South F2 Carpenter': ("#defeating Gerudo guards# reveals", None, 'exclude'),
+
+ 'Deku Tree Map Chest': ("in the #center of the Deku Tree# lies", None, 'exclude'),
+ 'Deku Tree Slingshot Chest': ("the #treasure guarded by a scrub# in the Deku Tree is", None, 'exclude'),
+ 'Deku Tree Slingshot Room Side Chest': ("the #treasure guarded by a scrub# in the Deku Tree is", None, 'exclude'),
+ 'Deku Tree Compass Chest': ("#pillars of wood# in the Deku Tree lead to", None, 'exclude'),
+ 'Deku Tree Compass Room Side Chest': ("#pillars of wood# in the Deku Tree lead to", None, 'exclude'),
+ 'Deku Tree Basement Chest': ("#webs in the Deku Tree# hide", None, 'exclude'),
+
+ 'Deku Tree MQ Map Chest': ("in the #center of the Deku Tree# lies", None, 'exclude'),
+ 'Deku Tree MQ Compass Chest': ("a #treasure guarded by a large spider# in the Deku Tree is", None, 'exclude'),
+ 'Deku Tree MQ Slingshot Chest': ("#pillars of wood# in the Deku Tree lead to", None, 'exclude'),
+ 'Deku Tree MQ Slingshot Room Back Chest': ("#pillars of wood# in the Deku Tree lead to", None, 'exclude'),
+ 'Deku Tree MQ Basement Chest': ("#webs in the Deku Tree# hide", None, 'exclude'),
+ 'Deku Tree MQ Before Spinning Log Chest': ("#magical fire in the Deku Tree# leads to", None, 'exclude'),
+
+ 'Dodongos Cavern Boss Room Chest': ("#above King Dodongo# lies", None, 'exclude'),
+
+ 'Dodongos Cavern Map Chest': ("a #muddy wall in Dodongo's Cavern# hides", None, 'exclude'),
+ 'Dodongos Cavern Compass Chest': ("a #statue in Dodongo's Cavern# guards", None, 'exclude'),
+ 'Dodongos Cavern Bomb Flower Platform Chest': ("above a #maze of stone# in Dodongo's Cavern lies", None, 'exclude'),
+ 'Dodongos Cavern Bomb Bag Chest': ("the #second lizard cavern battle# yields", None, 'exclude'),
+ 'Dodongos Cavern End of Bridge Chest': ("a #chest at the end of a bridge# yields", None, 'exclude'),
+
+ 'Dodongos Cavern MQ Map Chest': ("a #muddy wall in Dodongo's Cavern# hides", None, 'exclude'),
+ 'Dodongos Cavern MQ Bomb Bag Chest': ("an #elevated alcove# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern MQ Compass Chest': ("#fire-breathing lizards# in Dodongo's Cavern guard", None, 'exclude'),
+ 'Dodongos Cavern MQ Larvae Room Chest': ("#baby spiders# in Dodongo's Cavern guard", None, 'exclude'),
+ 'Dodongos Cavern MQ Torch Puzzle Room Chest': ("above a #maze of stone# in Dodongo's Cavern lies", None, 'exclude'),
+ 'Dodongos Cavern MQ Under Grave Chest': ("#beneath a headstone# in Dodongo's Cavern lies", None, 'exclude'),
+
+ 'Jabu Jabus Belly Map Chest': ("#tentacle trouble# in a deity's belly guards", "a #slimy thing# guards", 'exclude'),
+ 'Jabu Jabus Belly Compass Chest': ("#bubble trouble# in a deity's belly guards", "#bubbles# guard", 'exclude'),
+
+ 'Jabu Jabus Belly MQ First Room Side Chest': ("shooting a #mouth cow# reveals", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Map Chest': (["#pop rocks# hide", "an #explosive palate# holds"], "a #boulder before cows# hides", 'exclude'),
+ 'Jabu Jabus Belly MQ Second Room Lower Chest': ("near a #spiked elevator# lies", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Compass Chest': ("a #drowning cow# unveils", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Second Room Upper Chest': ("#moving anatomy# creates a path to", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Basement Near Switches Chest': ("a #pair of digested cows# hold", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Basement Near Vines Chest': ("a #pair of digested cows# hold", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Near Boss Chest': ("the #final cows' reward# in a deity's belly is", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Falling Like Like Room Chest': ("#cows protected by falling monsters# in a deity's belly guard", None, 'exclude'),
+ 'Jabu Jabus Belly MQ Boomerang Room Small Chest': ("a school of #stingers swallowed by a deity# guard", "a school of #stingers swallowed by Jabu Jabu# guard", 'exclude'),
+ 'Jabu Jabus Belly MQ Boomerang Chest': ("a school of #stingers swallowed by a deity# guard", "a school of #stingers swallowed by Jabu Jabu# guard", 'exclude'),
+
+ 'Forest Temple First Room Chest': ("a #tree in the Forest Temple# supports", None, 'exclude'),
+ 'Forest Temple First Stalfos Chest': ("#defeating enemies beneath a falling ceiling# in Forest Temple yields", None, 'exclude'),
+ 'Forest Temple Well Chest': ("a #sunken chest deep in the woods# contains", None, 'exclude'),
+ 'Forest Temple Map Chest': ("a #fiery skull# in Forest Temple guards", None, 'exclude'),
+ 'Forest Temple Raised Island Courtyard Chest': ("a #chest on a small island# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple Falling Ceiling Room Chest': ("beneath a #checkerboard falling ceiling# lies", None, 'exclude'),
+ 'Forest Temple Eye Switch Chest': ("a #sharp eye# will spot", "#blocks of stone# in the Forest Temple surround", 'exclude'),
+ 'Forest Temple Boss Key Chest': ("a #turned trunk# contains", None, 'exclude'),
+ 'Forest Temple Floormaster Chest': ("deep in the forest #shadows guard a chest# containing", None, 'exclude'),
+ 'Forest Temple Bow Chest': ("an #army of the dead# guards", "#Stalfos deep in the Forest Temple# guard", 'exclude'),
+ 'Forest Temple Red Poe Chest': ("#Joelle# guards", "a #red ghost# guards", 'exclude'),
+ 'Forest Temple Blue Poe Chest': ("#Beth# guards", "a #blue ghost# guards", 'exclude'),
+ 'Forest Temple Basement Chest': ("#revolving walls# in the Forest Temple conceal", None, 'exclude'),
+
+ 'Forest Temple MQ First Room Chest': ("a #tree in the Forest Temple# supports", None, 'exclude'),
+ 'Forest Temple MQ Wolfos Chest': ("#defeating enemies beneath a falling ceiling# in Forest Temple yields", None, 'exclude'),
+ 'Forest Temple MQ Bow Chest': ("an #army of the dead# guards", "#Stalfos deep in the Forest Temple# guard", 'exclude'),
+ 'Forest Temple MQ Raised Island Courtyard Lower Chest': ("a #chest on a small island# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple MQ Raised Island Courtyard Upper Chest': ("#high in a courtyard# within the Forest Temple is", None, 'exclude'),
+ 'Forest Temple MQ Well Chest': ("a #sunken chest deep in the woods# contains", None, 'exclude'),
+ 'Forest Temple MQ Map Chest': ("#Joelle# guards", "a #red ghost# guards", 'exclude'),
+ 'Forest Temple MQ Compass Chest': ("#Beth# guards", "a #blue ghost# guards", 'exclude'),
+ 'Forest Temple MQ Falling Ceiling Room Chest': ("beneath a #checkerboard falling ceiling# lies", None, 'exclude'),
+ 'Forest Temple MQ Basement Chest': ("#revolving walls# in the Forest Temple conceal", None, 'exclude'),
+ 'Forest Temple MQ Redead Chest': ("deep in the forest #undead guard a chest# containing", None, 'exclude'),
+ 'Forest Temple MQ Boss Key Chest': ("a #turned trunk# contains", None, 'exclude'),
+
+ 'Fire Temple Near Boss Chest': ("#near a dragon# is", None, 'exclude'),
+ 'Fire Temple Flare Dancer Chest': ("the #Flare Dancer behind a totem# guards", None, 'exclude'),
+ 'Fire Temple Boss Key Chest': ("a #prison beyond a totem# holds", None, 'exclude'),
+ 'Fire Temple Big Lava Room Blocked Door Chest': ("#explosives over a lava pit# unveil", None, 'exclude'),
+ 'Fire Temple Big Lava Room Lower Open Door Chest': ("a #Goron trapped near lava# holds", None, 'exclude'),
+ 'Fire Temple Boulder Maze Lower Chest': ("a #Goron at the end of a maze# holds", None, 'exclude'),
+ 'Fire Temple Boulder Maze Upper Chest': ("a #Goron above a maze# holds", None, 'exclude'),
+ 'Fire Temple Boulder Maze Side Room Chest': ("a #Goron hidden near a maze# holds", None, 'exclude'),
+ 'Fire Temple Boulder Maze Shortcut Chest': ("a #blocked path# in Fire Temple holds", None, 'exclude'),
+ 'Fire Temple Map Chest': ("a #caged chest# in the Fire Temple hoards", None, 'exclude'),
+ 'Fire Temple Compass Chest': ("a #chest in a fiery maze# contains", None, 'exclude'),
+ 'Fire Temple Highest Goron Chest': ("a #Goron atop the Fire Temple# holds", None, 'exclude'),
+
+ 'Fire Temple MQ Near Boss Chest': ("#near a dragon# is", None, 'exclude'),
+ 'Fire Temple MQ Megaton Hammer Chest': ("the #Flare Dancer in the depths of a volcano# guards", "the #Flare Dancer in the depths of the Fire Temple# guards", 'exclude'),
+ 'Fire Temple MQ Compass Chest': ("a #blocked path# in Fire Temple holds", None, 'exclude'),
+ 'Fire Temple MQ Lizalfos Maze Lower Chest': ("#crates in a maze# contain", None, 'exclude'),
+ 'Fire Temple MQ Lizalfos Maze Upper Chest': ("#crates in a maze# contain", None, 'exclude'),
+ 'Fire Temple MQ Map Room Side Chest': ("a #falling slug# in the Fire Temple guards", None, 'exclude'),
+ 'Fire Temple MQ Map Chest': ("using a #hammer in the depths of the Fire Temple# reveals", None, 'exclude'),
+ 'Fire Temple MQ Boss Key Chest': ("#illuminating a lava pit# reveals the path to", None, 'exclude'),
+ 'Fire Temple MQ Big Lava Room Blocked Door Chest': ("#explosives over a lava pit# unveil", None, 'exclude'),
+ 'Fire Temple MQ Lizalfos Maze Side Room Chest': ("a #Goron hidden near a maze# holds", None, 'exclude'),
+ 'Fire Temple MQ Freestanding Key': ("hidden #beneath a block of stone# lies", None, 'exclude'),
+
+ 'Water Temple Map Chest': ("#rolling spikes# in the Water Temple surround", None, 'exclude'),
+ 'Water Temple Compass Chest': ("#roaming stingers in the Water Temple# guard", None, 'exclude'),
+ 'Water Temple Torches Chest': ("#fire in the Water Temple# reveals", None, 'exclude'),
+ 'Water Temple Dragon Chest': ("a #serpent's prize# in the Water Temple is", None, 'exclude'),
+ 'Water Temple Central Bow Target Chest': ("#blinding an eye# in the Water Temple leads to", None, 'exclude'),
+ 'Water Temple Central Pillar Chest': ("in the #depths of the Water Temple# lies", None, 'exclude'),
+ 'Water Temple Cracked Wall Chest': ("#through a crack# in the Water Temple is", None, 'exclude'),
+ 'Water Temple Longshot Chest': (["#facing yourself# reveals", "a #dark reflection# of yourself guards"], "#Dark Link# guards", 'exclude'),
+
+ 'Water Temple MQ Central Pillar Chest': ("in the #depths of the Water Temple# lies", None, 'exclude'),
+ 'Water Temple MQ Boss Key Chest': ("fire in the Water Temple unlocks a #vast gate# revealing a chest with", None, 'exclude'),
+ 'Water Temple MQ Longshot Chest': ("#through a crack# in the Water Temple is", None, 'exclude'),
+ 'Water Temple MQ Compass Chest': ("#fire in the Water Temple# reveals", None, 'exclude'),
+ 'Water Temple MQ Map Chest': ("#sparring soldiers# in the Water Temple guard", None, 'exclude'),
+
+ 'Spirit Temple Child Bridge Chest': ("a child conquers a #skull in green fire# in the Spirit Temple to reach", None, 'exclude'),
+ 'Spirit Temple Child Early Torches Chest': ("a child can find a #caged chest# in the Spirit Temple with", None, 'exclude'),
+ 'Spirit Temple Compass Chest': ("#across a pit of sand# in the Spirit Temple lies", None, 'exclude'),
+ 'Spirit Temple Early Adult Right Chest': ("#dodging boulders to collect silver rupees# in the Spirit Temple yields", None, 'exclude'),
+ 'Spirit Temple First Mirror Left Chest': ("a #shadow circling reflected light# in the Spirit Temple guards", None, 'exclude'),
+ 'Spirit Temple First Mirror Right Chest': ("a #shadow circling reflected light# in the Spirit Temple guards", None, 'exclude'),
+ 'Spirit Temple Map Chest': ("#before a giant statue# in the Spirit Temple lies", None, 'exclude'),
+ 'Spirit Temple Child Climb North Chest': ("#lizards in the Spirit Temple# guard", None, 'exclude'),
+ 'Spirit Temple Child Climb East Chest': ("#lizards in the Spirit Temple# guard", None, 'exclude'),
+ 'Spirit Temple Sun Block Room Chest': ("#torchlight among Beamos# in the Spirit Temple reveals", None, 'exclude'),
+ 'Spirit Temple Statue Room Hand Chest': ("a #statue in the Spirit Temple# holds", None, 'exclude'),
+ 'Spirit Temple Statue Room Northeast Chest': ("on a #ledge by a statue# in the Spirit Temple rests", None, 'exclude'),
+ 'Spirit Temple Near Four Armos Chest': ("those who #show the light among statues# in the Spirit Temple find", None, 'exclude'),
+ 'Spirit Temple Hallway Right Invisible Chest': ("the #Eye of Truth in the Spirit Temple# reveals", None, 'exclude'),
+ 'Spirit Temple Hallway Left Invisible Chest': ("the #Eye of Truth in the Spirit Temple# reveals", None, 'exclude'),
+ 'Spirit Temple Boss Key Chest': ("a #chest engulfed in flame# in the Spirit Temple holds", None, 'exclude'),
+ 'Spirit Temple Topmost Chest': ("those who #show the light above the Colossus# find", None, 'exclude'),
+
+ 'Spirit Temple MQ Entrance Front Left Chest': ("#lying unguarded# in the Spirit Temple is", None, 'exclude'),
+ 'Spirit Temple MQ Entrance Back Right Chest': ("a #switch in a pillar# within the Spirit Temple drops", None, 'exclude'),
+ 'Spirit Temple MQ Entrance Front Right Chest': ("#collecting rupees through a water jet# reveals", None, 'exclude'),
+ 'Spirit Temple MQ Entrance Back Left Chest': ("an #eye blinded by stone# within the Spirit Temple conceals", None, 'exclude'),
+ 'Spirit Temple MQ Map Chest': ("surrounded by #fire and wrappings# lies", None, 'exclude'),
+ 'Spirit Temple MQ Map Room Enemy Chest': ("a child defeats a #gauntlet of monsters# within the Spirit Temple to find", None, 'exclude'),
+ 'Spirit Temple MQ Child Climb North Chest': ("#explosive sunlight# within the Spirit Temple uncovers", None, 'exclude'),
+ 'Spirit Temple MQ Child Climb South Chest': ("#trapped by falling enemies# within the Spirit Temple is", None, 'exclude'),
+ 'Spirit Temple MQ Compass Chest': ("#blinding the colossus# unveils", None, 'exclude'),
+ 'Spirit Temple MQ Statue Room Lullaby Chest': ("a #royal melody awakens the colossus# to reveal", None, 'exclude'),
+ 'Spirit Temple MQ Statue Room Invisible Chest': ("the #Eye of Truth# finds the colossus's hidden", None, 'exclude'),
+ 'Spirit Temple MQ Silver Block Hallway Chest': ("#the old hide what the young find# to reveal", None, 'exclude'),
+ 'Spirit Temple MQ Sun Block Room Chest': ("#sunlight in a maze of fire# hides", None, 'exclude'),
+ 'Spirit Temple MQ Leever Room Chest': ("#across a pit of sand# in the Spirit Temple lies", None, 'exclude'),
+ 'Spirit Temple MQ Beamos Room Chest': ("where #temporal stone blocks the path# within the Spirit Temple lies", None, 'exclude'),
+ 'Spirit Temple MQ Chest Switch Chest': ("a #chest of double purpose# holds", None, 'exclude'),
+ 'Spirit Temple MQ Boss Key Chest': ("a #temporal stone blocks the light# leading to", None, 'exclude'),
+ 'Spirit Temple MQ Mirror Puzzle Invisible Chest': ("those who #show the light above the Colossus# find", None, 'exclude'),
+
+ 'Shadow Temple Map Chest': ("the #Eye of Truth# pierces a hall of faces to reveal", None, 'exclude'),
+ 'Shadow Temple Hover Boots Chest': ("a #nether dweller in the Shadow Temple# holds", "#Dead Hand in the Shadow Temple# holds", 'exclude'),
+ 'Shadow Temple Compass Chest': ("#mummies revealed by the Eye of Truth# guard", None, 'exclude'),
+ 'Shadow Temple Early Silver Rupee Chest': ("#spinning scythes# protect", None, 'exclude'),
+ 'Shadow Temple Invisible Blades Visible Chest': ("#invisible blades# guard", None, 'exclude'),
+ 'Shadow Temple Invisible Blades Invisible Chest': ("#invisible blades# guard", None, 'exclude'),
+ 'Shadow Temple Falling Spikes Lower Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple Falling Spikes Upper Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple Falling Spikes Switch Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple Invisible Spikes Chest': ("the #dead roam among invisible spikes# guarding", None, 'exclude'),
+ 'Shadow Temple Wind Hint Chest': ("an #invisible chest guarded by the dead# holds", None, 'exclude'),
+ 'Shadow Temple After Wind Enemy Chest': ("#mummies guarding a ferry# hide", None, 'exclude'),
+ 'Shadow Temple After Wind Hidden Chest': ("#mummies guarding a ferry# hide", None, 'exclude'),
+ 'Shadow Temple Spike Walls Left Chest': ("#walls consumed by a ball of fire# reveal", None, 'exclude'),
+ 'Shadow Temple Boss Key Chest': ("#walls consumed by a ball of fire# reveal", None, 'exclude'),
+ 'Shadow Temple Freestanding Key': ("#inside a burning skull# lies", None, 'exclude'),
+
+ 'Shadow Temple MQ Compass Chest': ("the #Eye of Truth# pierces a hall of faces to reveal", None, 'exclude'),
+ 'Shadow Temple MQ Hover Boots Chest': ("#Dead Hand in the Shadow Temple# holds", None, 'exclude'),
+ 'Shadow Temple MQ Early Gibdos Chest': ("#mummies revealed by the Eye of Truth# guard", None, 'exclude'),
+ 'Shadow Temple MQ Map Chest': ("#spinning scythes# protect", None, 'exclude'),
+ 'Shadow Temple MQ Beamos Silver Rupees Chest': ("#collecting rupees in a vast cavern# with the Shadow Temple unveils", None, 'exclude'),
+ 'Shadow Temple MQ Falling Spikes Switch Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple MQ Falling Spikes Lower Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple MQ Falling Spikes Upper Chest': ("#falling spikes# block the path to", None, 'exclude'),
+ 'Shadow Temple MQ Invisible Spikes Chest': ("the #dead roam among invisible spikes# guarding", None, 'exclude'),
+ 'Shadow Temple MQ Boss Key Chest': ("#walls consumed by a ball of fire# reveal", None, 'exclude'),
+ 'Shadow Temple MQ Spike Walls Left Chest': ("#walls consumed by a ball of fire# reveal", None, 'exclude'),
+ 'Shadow Temple MQ Stalfos Room Chest': ("near an #empty pedestal# within the Shadow Temple lies", None, 'exclude'),
+ 'Shadow Temple MQ Invisible Blades Invisible Chest': ("#invisible blades# guard", None, 'exclude'),
+ 'Shadow Temple MQ Invisible Blades Visible Chest': ("#invisible blades# guard", None, 'exclude'),
+ 'Shadow Temple MQ Wind Hint Chest': ("an #invisible chest guarded by the dead# holds", None, 'exclude'),
+ 'Shadow Temple MQ After Wind Hidden Chest': ("#mummies guarding a ferry# hide", None, 'exclude'),
+ 'Shadow Temple MQ After Wind Enemy Chest': ("#mummies guarding a ferry# hide", None, 'exclude'),
+ 'Shadow Temple MQ Near Ship Invisible Chest': ("#caged near a ship# lies", None, 'exclude'),
+ 'Shadow Temple MQ Freestanding Key': ("#behind three burning skulls# lies", None, 'exclude'),
+
+ 'Bottom of the Well Front Left Fake Wall Chest': ("the #Eye of Truth in the well# reveals", None, 'exclude'),
+ 'Bottom of the Well Front Center Bombable Chest': ("#gruesome debris# in the well hides", None, 'exclude'),
+ 'Bottom of the Well Right Bottom Fake Wall Chest': ("the #Eye of Truth in the well# reveals", None, 'exclude'),
+ 'Bottom of the Well Compass Chest': ("a #hidden entrance to a cage# in the well leads to", None, 'exclude'),
+ 'Bottom of the Well Center Skulltula Chest': ("a #spider guarding a cage# in the well protects", None, 'exclude'),
+ 'Bottom of the Well Back Left Bombable Chest': ("#gruesome debris# in the well hides", None, 'exclude'),
+ 'Bottom of the Well Invisible Chest': ("#Dead Hand's invisible secret# is", None, 'exclude'),
+ 'Bottom of the Well Underwater Front Chest': ("a #royal melody in the well# uncovers", None, 'exclude'),
+ 'Bottom of the Well Underwater Left Chest': ("a #royal melody in the well# uncovers", None, 'exclude'),
+ 'Bottom of the Well Map Chest': ("in the #depths of the well# lies", None, 'exclude'),
+ 'Bottom of the Well Fire Keese Chest': ("#perilous pits# in the well guard the path to", None, 'exclude'),
+ 'Bottom of the Well Like Like Chest': ("#locked in a cage# in the well lies", None, 'exclude'),
+ 'Bottom of the Well Freestanding Key': ("#inside a coffin# hides", None, 'exclude'),
+
+ 'Bottom of the Well MQ Map Chest': ("a #royal melody in the well# uncovers", None, 'exclude'),
+ 'Bottom of the Well MQ Lens of Truth Chest': ("an #army of the dead# in the well guards", None, 'exclude'),
+ 'Bottom of the Well MQ Dead Hand Freestanding Key': ("#Dead Hand's explosive secret# is", None, 'exclude'),
+ 'Bottom of the Well MQ East Inner Room Freestanding Key': ("an #invisible path in the well# leads to", None, 'exclude'),
+
+ 'Ice Cavern Map Chest': ("#winds of ice# surround", None, 'exclude'),
+ 'Ice Cavern Compass Chest': ("a #wall of ice# protects", None, 'exclude'),
+ 'Ice Cavern Iron Boots Chest': ("a #monster in a frozen cavern# guards", None, 'exclude'),
+ 'Ice Cavern Freestanding PoH': ("a #wall of ice# protects", None, 'exclude'),
+
+ 'Ice Cavern MQ Iron Boots Chest': ("a #monster in a frozen cavern# guards", None, 'exclude'),
+ 'Ice Cavern MQ Compass Chest': ("#winds of ice# surround", None, 'exclude'),
+ 'Ice Cavern MQ Map Chest': ("a #wall of ice# protects", None, 'exclude'),
+ 'Ice Cavern MQ Freestanding PoH': ("#winds of ice# surround", None, 'exclude'),
+
+ 'Gerudo Training Grounds Lobby Left Chest': ("a #blinded eye in the Gerudo Training Grounds# drops", None, 'exclude'),
+ 'Gerudo Training Grounds Lobby Right Chest': ("a #blinded eye in the Gerudo Training Grounds# drops", None, 'exclude'),
+ 'Gerudo Training Grounds Stalfos Chest': ("#soldiers walking on shifting sands# in the Gerudo Training Grounds guard", None, 'exclude'),
+ 'Gerudo Training Grounds Beamos Chest': ("#reptilian warriors# in the Gerudo Training Grounds protect", None, 'exclude'),
+ 'Gerudo Training Grounds Hidden Ceiling Chest': ("the #Eye of Truth# in the Gerudo Training Grounds reveals", None, 'exclude'),
+ 'Gerudo Training Grounds Maze Path First Chest': ("the first prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds Maze Path Second Chest': ("the second prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds Maze Path Third Chest': ("the third prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds Maze Right Central Chest': ("the #Song of Time# in the Gerudo Training Grounds leads to", None, 'exclude'),
+ 'Gerudo Training Grounds Maze Right Side Chest': ("the #Song of Time# in the Gerudo Training Grounds leads to", None, 'exclude'),
+ 'Gerudo Training Grounds Hammer Room Clear Chest': ("#fiery foes# in the Gerudo Training Grounds guard", None, 'exclude'),
+ 'Gerudo Training Grounds Hammer Room Switch Chest': ("#engulfed in flame# where thieves train lies", None, 'exclude'),
+ 'Gerudo Training Grounds Eye Statue Chest': ("thieves #blind four faces# to find", None, 'exclude'),
+ 'Gerudo Training Grounds Near Scarecrow Chest': ("thieves #blind four faces# to find", None, 'exclude'),
+ 'Gerudo Training Grounds Before Heavy Block Chest': ("#before a block of silver# thieves can find", None, 'exclude'),
+ 'Gerudo Training Grounds Heavy Block First Chest': ("a #feat of strength# rewards thieves with", None, 'exclude'),
+ 'Gerudo Training Grounds Heavy Block Second Chest': ("a #feat of strength# rewards thieves with", None, 'exclude'),
+ 'Gerudo Training Grounds Heavy Block Third Chest': ("a #feat of strength# rewards thieves with", None, 'exclude'),
+ 'Gerudo Training Grounds Heavy Block Fourth Chest': ("a #feat of strength# rewards thieves with", None, 'exclude'),
+ 'Gerudo Training Grounds Freestanding Key': ("the #Song of Time# in the Gerudo Training Grounds leads to", None, 'exclude'),
+
+ 'Gerudo Training Grounds MQ Lobby Right Chest': ("#thieves prepare for training# with", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Lobby Left Chest': ("#thieves prepare for training# with", None, 'exclude'),
+ 'Gerudo Training Grounds MQ First Iron Knuckle Chest': ("#soldiers walking on shifting sands# in the Gerudo Training Grounds guard", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Before Heavy Block Chest': ("#before a block of silver# thieves can find", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Eye Statue Chest': ("thieves #blind four faces# to find", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Flame Circle Chest': ("#engulfed in flame# where thieves train lies", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Second Iron Knuckle Chest': ("#fiery foes# in the Gerudo Training Grounds guard", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Dinolfos Chest': ("#reptilian warriors# in the Gerudo Training Grounds protect", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Maze Right Central Chest': ("a #path of fire# leads thieves to", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Maze Path First Chest': ("the first prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Maze Right Side Chest': ("a #path of fire# leads thieves to", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Maze Path Third Chest': ("the third prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Maze Path Second Chest': ("the second prize of #the thieves' training# is", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Hidden Ceiling Chest': ("the #Eye of Truth# in the Gerudo Training Grounds reveals", None, 'exclude'),
+ 'Gerudo Training Grounds MQ Heavy Block Chest': ("a #feat of strength# rewards thieves with", None, 'exclude'),
+
+ 'Ganons Tower Boss Key Chest': ("the #Evil King# hoards", None, 'exclude'),
+
+ 'Ganons Castle Forest Trial Chest': ("the #test of the wilds# holds", None, 'exclude'),
+ 'Ganons Castle Water Trial Left Chest': ("the #test of the seas# holds", None, 'exclude'),
+ 'Ganons Castle Water Trial Right Chest': ("the #test of the seas# holds", None, 'exclude'),
+ 'Ganons Castle Shadow Trial Front Chest': ("#music in the test of darkness# unveils", None, 'exclude'),
+ 'Ganons Castle Shadow Trial Golden Gauntlets Chest': ("#light in the test of darkness# unveils", None, 'exclude'),
+ 'Ganons Castle Spirit Trial Crystal Switch Chest': ("the #test of the sands# holds", None, 'exclude'),
+ 'Ganons Castle Spirit Trial Invisible Chest': ("the #test of the sands# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial First Left Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Second Left Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Third Left Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial First Right Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Second Right Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Third Right Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Invisible Enemies Chest': ("the #test of radiance# holds", None, 'exclude'),
+ 'Ganons Castle Light Trial Lullaby Chest': ("#music in the test of radiance# reveals", None, 'exclude'),
+
+ 'Ganons Castle MQ Water Trial Chest': ("the #test of the seas# holds", None, 'exclude'),
+ 'Ganons Castle MQ Forest Trial Eye Switch Chest': ("the #test of the wilds# holds", None, 'exclude'),
+ 'Ganons Castle MQ Forest Trial Frozen Eye Switch Chest': ("the #test of the wilds# holds", None, 'exclude'),
+ 'Ganons Castle MQ Light Trial Lullaby Chest': ("#music in the test of radiance# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Shadow Trial Bomb Flower Chest': ("the #test of darkness# holds", None, 'exclude'),
+ 'Ganons Castle MQ Shadow Trial Eye Switch Chest': ("the #test of darkness# holds", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial Golden Gauntlets Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial Sun Back Right Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial Sun Back Left Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial Sun Front Left Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial First Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Spirit Trial Invisible Chest': ("#reflected light in the test of the sands# reveals", None, 'exclude'),
+ 'Ganons Castle MQ Forest Trial Freestanding Key': ("the #test of the wilds# holds", None, 'exclude'),
+
+ 'Deku Tree Queen Gohma Heart': ("the #Parasitic Armored Arachnid# holds", "#Queen Gohma# holds", 'exclude'),
+ 'Dodongos Cavern King Dodongo Heart': ("the #Infernal Dinosaur# holds", "#King Dodongo# holds", 'exclude'),
+ 'Jabu Jabus Belly Barinade Heart': ("the #Bio-Electric Anemone# holds", "#Barinade# holds", 'exclude'),
+ 'Forest Temple Phantom Ganon Heart': ("the #Evil Spirit from Beyond# holds", "#Phantom Ganon# holds", 'exclude'),
+ 'Fire Temple Volvagia Heart': ("the #Subterranean Lava Dragon# holds", "#Volvagia# holds", 'exclude'),
+ 'Water Temple Morpha Heart': ("the #Giant Aquatic Amoeba# holds", "#Morpha# holds", 'exclude'),
+ 'Spirit Temple Twinrova Heart': ("the #Sorceress Sisters# hold", "#Twinrova# holds", 'exclude'),
+ 'Shadow Temple Bongo Bongo Heart': ("the #Phantom Shadow Beast# holds", "#Bongo Bongo# holds", 'exclude'),
+
+ 'Deku Tree GS Basement Back Room': ("a #spider deep within the Deku Tree# hides", None, 'exclude'),
+ 'Deku Tree GS Basement Gate': ("a #web protects a spider# within the Deku Tree holding", None, 'exclude'),
+ 'Deku Tree GS Basement Vines': ("a #web protects a spider# within the Deku Tree holding", None, 'exclude'),
+ 'Deku Tree GS Compass Room': ("a #spider atop the Deku Tree# holds", None, 'exclude'),
+
+ 'Deku Tree MQ GS Lobby': ("a #spider in a crate# within the Deku Tree hides", None, 'exclude'),
+ 'Deku Tree MQ GS Compass Room': ("a #wall of rock protects a spider# within the Deku Tree holding", None, 'exclude'),
+ 'Deku Tree MQ GS Basement Back Room': ("a #spider deep within the Deku Tree# hides", None, 'exclude'),
+
+ 'Dodongos Cavern GS Vines Above Stairs': ("a #spider entangled in vines# in Dodongo's Cavern guards", None, 'exclude'),
+ 'Dodongos Cavern GS Scarecrow': ("a #spider among explosive slugs# hides", None, 'exclude'),
+ 'Dodongos Cavern GS Alcove Above Stairs': ("a #spider just out of reach# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern GS Back Room': ("a #spider behind a statue# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern GS Side Room Near Lower Lizalfos': ("a #spider among bats# in Dodongo's Cavern holds", None, 'exclude'),
+
+ 'Dodongos Cavern MQ GS Scrub Room': ("a #spider high on a wall# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern MQ GS Lizalfos Room': ("a #spider on top of a pillar of rock# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern MQ GS Larvae Room': ("a #spider in a crate# in Dodongo's Cavern holds", None, 'exclude'),
+ 'Dodongos Cavern MQ GS Back Area': ("a #spider among graves# in Dodongo's Cavern holds", None, 'exclude'),
+
+ 'Jabu Jabus Belly GS Lobby Basement Lower': ("a #spider resting near a princess# in Jabu Jabu's Belly holds", None, 'exclude'),
+ 'Jabu Jabus Belly GS Lobby Basement Upper': ("a #spider resting near a princess# in Jabu Jabu's Belly holds", None, 'exclude'),
+ 'Jabu Jabus Belly GS Near Boss': ("#jellyfish surround a spider# holding", None, 'exclude'),
+ 'Jabu Jabus Belly GS Water Switch Room': ("a #spider guarded by a school of stingers# in Jabu Jabu's Belly holds", None, 'exclude'),
+
+ 'Jabu Jabus Belly MQ GS Tailpasaran Room': ("a #spider surrounded by electricity# in Jabu Jabu's Belly holds", None, 'exclude'),
+ 'Jabu Jabus Belly MQ GS Boomerang Chest Room': ("a #spider guarded by a school of stingers# in Jabu Jabu's Belly holds", None, 'exclude'),
+ 'Jabu Jabus Belly MQ GS Near Boss': ("a #spider in a web within Jabu Jabu's Belly# holds", None, 'exclude'),
+
+ 'Forest Temple GS Raised Island Courtyard': ("a #spider on a small island# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple GS First Room': ("a #spider high on a wall of vines# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple GS Level Island Courtyard': ("#stone columns# lead to a spider in the Forest Temple hiding", None, 'exclude'),
+ 'Forest Temple GS Lobby': ("a #spider among ghosts# in the Forest Temple guards", None, 'exclude'),
+ 'Forest Temple GS Basement': ("a #spider within revolving walls# in the Forest Temple holds", None, 'exclude'),
+
+ 'Forest Temple MQ GS First Hallway': ("an #ivy-hidden spider# in the Forest Temple hoards", None, 'exclude'),
+ 'Forest Temple MQ GS Block Push Room': ("a #spider in a hidden nook# within the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple MQ GS Raised Island Courtyard': ("a #spider on an arch# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple MQ GS Level Island Courtyard': ("a #spider on a ledge# in the Forest Temple holds", None, 'exclude'),
+ 'Forest Temple MQ GS Well': ("#draining a well# in Forest Temple uncovers a spider with", None, 'exclude'),
+
+ 'Fire Temple GS Song of Time Room': ("#eight tiles of malice# guard a spider holding", None, 'exclude'),
+ 'Fire Temple GS Boss Key Loop': ("#five tiles of malice# guard a spider holding", None, 'exclude'),
+ 'Fire Temple GS Boulder Maze': ("#explosives in a maze# unveil a spider hiding", None, 'exclude'),
+ 'Fire Temple GS Scarecrow Top': ("a #spider-friendly scarecrow# atop a volcano hides", "a #spider-friendly scarecrow# atop the Fire Temple hides", 'exclude'),
+ 'Fire Temple GS Scarecrow Climb': ("a #spider-friendly scarecrow# atop a volcano hides", "a #spider-friendly scarecrow# atop the Fire Temple hides", 'exclude'),
+
+ 'Fire Temple MQ GS Above Fire Wall Maze': ("a #spider above a fiery maze# holds", None, 'exclude'),
+ 'Fire Temple MQ GS Fire Wall Maze Center': ("a #spider within a fiery maze# holds", None, 'exclude'),
+ 'Fire Temple MQ GS Big Lava Room Open Door': ("a #Goron trapped near lava# befriended a spider with", None, 'exclude'),
+ 'Fire Temple MQ GS Fire Wall Maze Side Room': ("a #spider beside a fiery maze# holds", None, 'exclude'),
+
+ 'Water Temple GS Falling Platform Room': ("a #spider over a waterfall# in the Water Temple holds", None, 'exclude'),
+ 'Water Temple GS Central Pillar': ("a #spider in the center of the Water Temple# holds", None, 'exclude'),
+ 'Water Temple GS Near Boss Key Chest': ("a spider protected by #rolling boulders under the lake# hides", "a spider protected by #rolling boulders in the Water Temple# hides", 'exclude'),
+ 'Water Temple GS River': ("a #spider over a river# in the Water Temple holds", None, 'exclude'),
+
+ 'Water Temple MQ GS Before Upper Water Switch': ("#beyond a pit of lizards# is a spider holding", None, 'exclude'),
+ 'Water Temple MQ GS Lizalfos Hallway': ("#lizards guard a spider# in the Water Temple with", None, 'exclude'),
+ 'Water Temple MQ GS River': ("a #spider over a river# in the Water Temple holds", None, 'exclude'),
+
+ 'Spirit Temple GS Hall After Sun Block Room': ("a spider in the #hall of a knight# guards", None, 'exclude'),
+ 'Spirit Temple GS Boulder Room': ("a #spider behind a temporal stone# in the Spirit Temple yields", None, 'exclude'),
+ 'Spirit Temple GS Lobby': ("a #spider beside a statue# holds", None, 'exclude'),
+ 'Spirit Temple GS Sun on Floor Room': ("a #spider at the top of a deep shaft# in the Spirit Temple holds", None, 'exclude'),
+ 'Spirit Temple GS Metal Fence': ("a child defeats a #spider among bats# in the Spirit Temple to gain", None, 'exclude'),
+
+ 'Spirit Temple MQ GS Leever Room': ("#above a pit of sand# in the Spirit Temple hides", None, 'exclude'),
+ 'Spirit Temple MQ GS Nine Thrones Room West': ("a spider in the #hall of a knight# guards", None, 'exclude'),
+ 'Spirit Temple MQ GS Nine Thrones Room North': ("a spider in the #hall of a knight# guards", None, 'exclude'),
+ 'Spirit Temple MQ GS Sun Block Room': ("#upon a web of glass# in the Spirit Temple sits a spider holding", None, 'exclude'),
+
+ 'Shadow Temple GS Single Giant Pot': ("#beyond a burning skull# lies a spider with", None, 'exclude'),
+ 'Shadow Temple GS Falling Spikes Room': ("a #spider beyond falling spikes# holds", None, 'exclude'),
+ 'Shadow Temple GS Triple Giant Pot': ("#beyond three burning skulls# lies a spider with", None, 'exclude'),
+ 'Shadow Temple GS Like Like Room': ("a spider guarded by #invisible blades# holds", None, 'exclude'),
+ 'Shadow Temple GS Near Ship': ("a spider near a #docked ship# hoards", None, 'exclude'),
+
+ 'Shadow Temple MQ GS Falling Spikes Room': ("a #spider beyond falling spikes# holds", None, 'exclude'),
+ 'Shadow Temple MQ GS Wind Hint Room': ("a #spider amidst roaring winds# in the Shadow Temple holds", None, 'exclude'),
+ 'Shadow Temple MQ GS After Wind': ("a #spider beneath gruesome debris# in the Shadow Temple hides", None, 'exclude'),
+ 'Shadow Temple MQ GS After Ship': ("a #fallen statue# reveals a spider with", None, 'exclude'),
+ 'Shadow Temple MQ GS Near Boss': ("a #suspended spider# guards", None, 'exclude'),
+
+ 'Bottom of the Well GS Like Like Cage': ("a #spider locked in a cage# in the well holds", None, 'exclude'),
+ 'Bottom of the Well GS East Inner Room': ("an #invisible path in the well# leads to", None, 'exclude'),
+ 'Bottom of the Well GS West Inner Room': ("a #spider locked in a crypt# within the well guards", None, 'exclude'),
+
+ 'Bottom of the Well MQ GS Basement': ("a #gauntlet of invisible spiders# protects", None, 'exclude'),
+ 'Bottom of the Well MQ GS Coffin Room': ("a #spider crawling near the dead# in the well holds", None, 'exclude'),
+ 'Bottom of the Well MQ GS West Inner Room': ("a #spider locked in a crypt# within the well guards", None, 'exclude'),
+
+ 'Ice Cavern GS Push Block Room': ("a #spider above icy pits# holds", None, 'exclude'),
+ 'Ice Cavern GS Spinning Scythe Room': ("#spinning ice# guards a spider holding", None, 'exclude'),
+ 'Ice Cavern GS Heart Piece Room': ("a #spider behind a wall of ice# hides", None, 'exclude'),
+
+ 'Ice Cavern MQ GS Scarecrow': ("a #spider above icy pits# holds", None, 'exclude'),
+ 'Ice Cavern MQ GS Ice Block': ("a #web of ice# surrounds a spider with", None, 'exclude'),
+ 'Ice Cavern MQ GS Red Ice': ("a #spider in fiery ice# hoards", None, 'exclude'),
+
+ 'HF GS Near Kak Grotto': ("a #spider-guarded spider in a hole# hoards", None, 'exclude'),
+
+ 'LLR GS Back Wall': ("night reveals a #spider in a ranch# holding", None, 'exclude'),
+ 'LLR GS Rain Shed': ("night reveals a #spider in a ranch# holding", None, 'exclude'),
+ 'LLR GS House Window': ("night reveals a #spider in a ranch# holding", None, 'exclude'),
+ 'LLR GS Tree': ("a spider hiding in a #ranch tree# holds", None, 'exclude'),
+
+ 'KF GS Bean Patch': ("a #spider buried in a forest# holds", None, 'exclude'),
+ 'KF GS Know It All House': ("night in the past reveals a #spider in a forest# holding", None, 'exclude'),
+ 'KF GS House of Twins': ("night in the future reveals a #spider in a forest# holding", None, 'exclude'),
+
+ 'LW GS Bean Patch Near Bridge': ("a #spider buried deep in a forest maze# holds", None, 'exclude'),
+ 'LW GS Bean Patch Near Theater': ("a #spider buried deep in a forest maze# holds", None, 'exclude'),
+ 'LW GS Above Theater': ("night reveals a #spider deep in a forest maze# holding", None, 'exclude'),
+ 'SFM GS': ("night reveals a #spider in a forest meadow# holding", None, 'exclude'),
+
+ 'OGC GS': ("a #spider outside a tyrant's tower# holds", None, 'exclude'),
+ 'HC GS Tree': ("a spider hiding in a #tree outside of a castle# holds", None, 'exclude'),
+ 'Market GS Guard House': ("a #spider in a guarded crate# holds", None, 'exclude'),
+
+ 'DMC GS Bean Patch': ("a #spider buried in a volcano# holds", None, 'exclude'),
+
+ 'DMT GS Bean Patch': ("a #spider buried outside a cavern# holds", None, 'exclude'),
+ 'DMT GS Near Kak': ("a #spider hidden in a mountain nook# holds", None, 'exclude'),
+ 'DMT GS Above Dodongos Cavern': ("the hammer reveals a #spider on a mountain# holding", None, 'exclude'),
+ 'DMT GS Falling Rocks Path': ("the hammer reveals a #spider on a mountain# holding", None, 'exclude'),
+
+ 'GC GS Center Platform': ("a #suspended spider# in Goron City holds", None, 'exclude'),
+ 'GC GS Boulder Maze': ("a spider in a #Goron City crate# holds", None, 'exclude'),
+
+ 'Kak GS House Under Construction': ("night in the past reveals a #spider in a town# holding", None, 'exclude'),
+ 'Kak GS Skulltula House': ("night in the past reveals a #spider in a town# holding", None, 'exclude'),
+ 'Kak GS Guards House': ("night in the past reveals a #spider in a town# holding", None, 'exclude'),
+ 'Kak GS Tree': ("night in the past reveals a #spider in a town# holding", None, 'exclude'),
+ 'Kak GS Watchtower': ("night in the past reveals a #spider in a town# holding", None, 'exclude'),
+ 'Kak GS Above Impas House': ("night in the future reveals a #spider in a town# holding", None, 'exclude'),
+
+ 'Graveyard GS Wall': ("night reveals a #spider in a graveyard# holding", None, 'exclude'),
+ 'Graveyard GS Bean Patch': ("a #spider buried in a graveyard# holds", None, 'exclude'),
+
+ 'ZR GS Ladder': ("night in the past reveals a #spider in a river# holding", None, 'exclude'),
+ 'ZR GS Tree': ("a spider hiding in a #tree by a river# holds", None, 'exclude'),
+ 'ZR GS Above Bridge': ("night in the future reveals a #spider in a river# holding", None, 'exclude'),
+ 'ZR GS Near Raised Grottos': ("night in the future reveals a #spider in a river# holding", None, 'exclude'),
+
+ 'ZD GS Frozen Waterfall': ("night reveals a #spider by a frozen waterfall# holding", None, 'exclude'),
+ 'ZF GS Above the Log': ("night reveals a #spider near a deity# holding", None, 'exclude'),
+ 'ZF GS Tree': ("a spider hiding in a #tree near a deity# holds", None, 'exclude'),
+
+ 'LH GS Bean Patch': ("a #spider buried by a lake# holds", None, 'exclude'),
+ 'LH GS Small Island': ("night reveals a #spider by a lake# holding", None, 'exclude'),
+ 'LH GS Lab Wall': ("night reveals a #spider by a lake# holding", None, 'exclude'),
+ 'LH GS Lab Crate': ("a spider deed underwater in a #lab crate# holds", None, 'exclude'),
+ 'LH GS Tree': ("night reveals a #spider by a lake high in a tree# holding", None, 'exclude'),
+
+ 'GV GS Bean Patch': ("a #spider buried in a valley# holds", None, 'exclude'),
+ 'GV GS Small Bridge': ("night in the past reveals a #spider in a valley# holding", None, 'exclude'),
+ 'GV GS Pillar': ("night in the future reveals a #spider in a valley# holding", None, 'exclude'),
+ 'GV GS Behind Tent': ("night in the future reveals a #spider in a valley# holding", None, 'exclude'),
+
+ 'GF GS Archery Range': ("night reveals a #spider in a fortress# holding", None, 'exclude'),
+ 'GF GS Top Floor': ("night reveals a #spider in a fortress# holding", None, 'exclude'),
+
+ 'Colossus GS Bean Patch': ("a #spider buried in the desert# holds", None, 'exclude'),
+ 'Colossus GS Hill': ("night reveals a #spider deep in the desert# holding", None, 'exclude'),
+ 'Colossus GS Tree': ("night reveals a #spider deep in the desert# holding", None, 'exclude'),
+
+ 'KF Shop Item 1': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 2': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 3': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 4': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 5': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 6': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 7': ("a #child shopkeeper# sells", None, 'exclude'),
+ 'KF Shop Item 8': ("a #child shopkeeper# sells", None, 'exclude'),
+
+ 'Kak Potion Shop Item 1': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 2': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 3': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 4': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 5': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 6': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 7': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+ 'Kak Potion Shop Item 8': ("a #potion seller# offers", "the #Kakariko Potion Shop# offers", 'exclude'),
+
+ 'Market Bombchu Shop Item 1': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 2': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 3': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 4': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 5': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 6': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 7': ("a #Bombchu merchant# sells", None, 'exclude'),
+ 'Market Bombchu Shop Item 8': ("a #Bombchu merchant# sells", None, 'exclude'),
+
+ 'Market Potion Shop Item 1': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 2': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 3': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 4': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 5': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 6': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 7': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+ 'Market Potion Shop Item 8': ("a #potion seller# offers", "the #Market Potion Shop# offers", 'exclude'),
+
+ 'Market Bazaar Item 1': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 2': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 3': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 4': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 5': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 6': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 7': ("the #Market Bazaar# offers", None, 'exclude'),
+ 'Market Bazaar Item 8': ("the #Market Bazaar# offers", None, 'exclude'),
+
+ 'Kak Bazaar Item 1': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 2': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 3': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 4': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 5': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 6': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 7': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+ 'Kak Bazaar Item 8': ("the #Kakariko Bazaar# offers", None, 'exclude'),
+
+ 'ZD Shop Item 1': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 2': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 3': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 4': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 5': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 6': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 7': ("a #Zora shopkeeper# sells", None, 'exclude'),
+ 'ZD Shop Item 8': ("a #Zora shopkeeper# sells", None, 'exclude'),
+
+ 'GC Shop Item 1': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 2': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 3': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 4': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 5': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 6': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 7': ("a #Goron shopkeeper# sells", None, 'exclude'),
+ 'GC Shop Item 8': ("a #Goron shopkeeper# sells", None, 'exclude'),
+
+ 'Deku Tree MQ Deku Scrub': ("a #scrub in the Deku Tree# sells", None, 'exclude'),
+
+ 'HF Deku Scrub Grotto': ("a lonely #scrub in a hole# sells", None, 'exclude'),
+ 'LLR Deku Scrub Grotto Left': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'LLR Deku Scrub Grotto Right': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'LLR Deku Scrub Grotto Center': ("a #trio of scrubs# sells", None, 'exclude'),
+
+ 'LW Deku Scrub Near Deku Theater Right': ("a pair of #scrubs in the woods# sells", None, 'exclude'),
+ 'LW Deku Scrub Near Deku Theater Left': ("a pair of #scrubs in the woods# sells", None, 'exclude'),
+ 'LW Deku Scrub Near Bridge': ("a #scrub by a bridge# sells", None, 'exclude'),
+ 'LW Deku Scrub Grotto Rear': ("a #scrub underground duo# sells", None, 'exclude'),
+ 'LW Deku Scrub Grotto Front': ("a #scrub underground duo# sells", None, 'exclude'),
+
+ 'SFM Deku Scrub Grotto Rear': ("a #scrub underground duo# sells", None, 'exclude'),
+ 'SFM Deku Scrub Grotto Front': ("a #scrub underground duo# sells", None, 'exclude'),
+
+ 'GC Deku Scrub Grotto Left': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'GC Deku Scrub Grotto Right': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'GC Deku Scrub Grotto Center': ("a #trio of scrubs# sells", None, 'exclude'),
+
+ 'Dodongos Cavern Deku Scrub Near Bomb Bag Left': ("a pair of #scrubs in Dodongo's Cavern# sells", None, 'exclude'),
+ 'Dodongos Cavern Deku Scrub Side Room Near Dodongos': ("a #scrub guarded by Lizalfos# sells", None, 'exclude'),
+ 'Dodongos Cavern Deku Scrub Near Bomb Bag Right': ("a pair of #scrubs in Dodongo's Cavern# sells", None, 'exclude'),
+ 'Dodongos Cavern Deku Scrub Lobby': ("a #scrub in Dodongo's Cavern# sells", None, 'exclude'),
+
+ 'Dodongos Cavern MQ Deku Scrub Lobby Rear': ("a pair of #scrubs in Dodongo's Cavern# sells", None, 'exclude'),
+ 'Dodongos Cavern MQ Deku Scrub Lobby Front': ("a pair of #scrubs in Dodongo's Cavern# sells", None, 'exclude'),
+ 'Dodongos Cavern MQ Deku Scrub Staircase': ("a #scrub in Dodongo's Cavern# sells", None, 'exclude'),
+ 'Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos': ("a #scrub guarded by Lizalfos# sells", None, 'exclude'),
+
+ 'DMC Deku Scrub Grotto Left': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'DMC Deku Scrub Grotto Right': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'DMC Deku Scrub Grotto Center': ("a #trio of scrubs# sells", None, 'exclude'),
+
+ 'ZR Deku Scrub Grotto Rear': ("a #scrub underground duo# sells", None, 'exclude'),
+ 'ZR Deku Scrub Grotto Front': ("a #scrub underground duo# sells", None, 'exclude'),
+
+ 'Jabu Jabus Belly Deku Scrub': ("a #scrub in a deity# sells", None, 'exclude'),
+
+ 'LH Deku Scrub Grotto Left': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'LH Deku Scrub Grotto Right': ("a #trio of scrubs# sells", None, 'exclude'),
+ 'LH Deku Scrub Grotto Center': ("a #trio of scrubs# sells", None, 'exclude'),
+
+ 'GV Deku Scrub Grotto Rear': ("a #scrub underground duo# sells", None, 'exclude'),
+ 'GV Deku Scrub Grotto Front': ("a #scrub underground duo# sells", None, 'exclude'),
+
+ 'Colossus Deku Scrub Grotto Front': ("a #scrub underground duo# sells", None, 'exclude'),
+ 'Colossus Deku Scrub Grotto Rear': ("a #scrub underground duo# sells", None, 'exclude'),
+
+ 'Ganons Castle Deku Scrub Center-Left': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle Deku Scrub Center-Right': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle Deku Scrub Right': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle Deku Scrub Left': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+
+ 'Ganons Castle MQ Deku Scrub Right': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle MQ Deku Scrub Center-Left': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle MQ Deku Scrub Center': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle MQ Deku Scrub Center-Right': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+ 'Ganons Castle MQ Deku Scrub Left': ("#scrubs in Ganon's Castle# sell", None, 'exclude'),
+
+ 'LLR Stables Left Cow': ("a #cow in a stable# gifts", None, 'exclude'),
+ 'LLR Stables Right Cow': ("a #cow in a stable# gifts", None, 'exclude'),
+ 'LLR Tower Right Cow': ("a #cow in a ranch silo# gifts", None, 'exclude'),
+ 'LLR Tower Left Cow': ("a #cow in a ranch silo# gifts", None, 'exclude'),
+ 'Kak Impas House Cow': ("a #cow imprisoned in a house# protects", None, 'exclude'),
+ 'DMT Cow Grotto Cow': ("a #cow in a luxurious hole# offers", None, 'exclude'),
+
+ 'Desert Colossus -> Colossus Grotto': ("lifting a #rock in the desert# reveals", None, 'entrance'),
+ 'GV Grotto Ledge -> GV Octorok Grotto': ("a rock on #a ledge in the valley# hides", None, 'entrance'),
+ 'GC Grotto Platform -> GC Grotto': ("a #pool of lava# in Goron City blocks the way to", None, 'entrance'),
+ 'Gerudo Fortress -> GF Storms Grotto': ("a #storm within Gerudo's Fortress# reveals", None, 'entrance'),
+ 'Zoras Domain -> ZD Storms Grotto': ("a #storm within Zora's Domain# reveals", None, 'entrance'),
+ 'Hyrule Castle Grounds -> HC Storms Grotto': ("a #storm near the castle# reveals", None, 'entrance'),
+ 'GV Fortress Side -> GV Storms Grotto': ("a #storm in the valley# reveals", None, 'entrance'),
+ 'Desert Colossus -> Colossus Great Fairy Fountain': ("a #fractured desert wall# hides", None, 'entrance'),
+ 'Ganons Castle Grounds -> OGC Great Fairy Fountain': ("a #heavy pillar# outside the castle obstructs", None, 'entrance'),
+ 'Zoras Fountain -> ZF Great Fairy Fountain': ("a #fountain wall# hides", None, 'entrance'),
+ 'GV Fortress Side -> GV Carpenter Tent': ("a #tent in the valley# covers", None, 'entrance'),
+ 'Graveyard Warp Pad Region -> Shadow Temple Entryway': ("at the #back of the Graveyard#, there is", None, 'entrance'),
+ 'Lake Hylia -> Water Temple Lobby': ("deep #under a vast lake#, one can find", None, 'entrance'),
+ 'Gerudo Fortress -> Gerudo Training Grounds Lobby': ("paying a #fee to the Gerudos# grants access to", None, 'entrance'),
+ 'Zoras Fountain -> Jabu Jabus Belly Beginning': ("inside #Jabu Jabu#, one can find", None, 'entrance'),
+ 'Kakariko Village -> Bottom of the Well': ("a #village well# leads to", None, 'entrance'),
+
+ 'KF Links House': ("Link's House", None, 'region'),
+ 'Temple of Time': ("the #Temple of Time#", None, 'region'),
+ 'KF Midos House': ("Mido's house", None, 'region'),
+ 'KF Sarias House': ("Saria's House", None, 'region'),
+ 'KF House of Twins': ("the #House of Twins#", None, 'region'),
+ 'KF Know It All House': ("Know-It-All Brothers' House", None, 'region'),
+ 'KF Kokiri Shop': ("the #Kokiri Shop#", None, 'region'),
+ 'LH Lab': ("the #Lakeside Laboratory#", None, 'region'),
+ 'LH Fishing Hole': ("the #Fishing Pond#", None, 'region'),
+ 'GV Carpenter Tent': ("the #Carpenters' tent#", None, 'region'),
+ 'Market Guard House': ("the #Guard House#", None, 'region'),
+ 'Market Mask Shop': ("the #Happy Mask Shop#", None, 'region'),
+ 'Market Bombchu Bowling': ("the #Bombchu Bowling Alley#", None, 'region'),
+ 'Market Potion Shop': ("the #Market Potion Shop#", None, 'region'),
+ 'Market Treasure Chest Game': ("the #Treasure Box Shop#", None, 'region'),
+ 'Market Bombchu Shop': ("the #Bombchu Shop#", None, 'region'),
+ 'Market Man in Green House': ("Man in Green's House", None, 'region'),
+ 'Kak Windmill': ("the #Windmill#", None, 'region'),
+ 'Kak Carpenter Boss House': ("the #Carpenters' Boss House#", None, 'region'),
+ 'Kak House of Skulltula': ("the #House of Skulltula#", None, 'region'),
+ 'Kak Impas House': ("Impa's House", None, 'region'),
+ 'Kak Impas House Back': ("Impa's cow cage", None, 'region'),
+ 'Kak Odd Medicine Building': ("Granny's Potion Shop", None, 'region'),
+ 'Graveyard Dampes House': ("Dampé's Hut", None, 'region'),
+ 'GC Shop': ("the #Goron Shop#", None, 'region'),
+ 'ZD Shop': ("the #Zora Shop#", None, 'region'),
+ 'LLR Talons House': ("Talon's House", None, 'region'),
+ 'LLR Stables': ("a #stable#", None, 'region'),
+ 'LLR Tower': ("the #Lon Lon Tower#", None, 'region'),
+ 'Market Bazaar': ("the #Market Bazaar#", None, 'region'),
+ 'Market Shooting Gallery': ("a #Slingshot Shooting Gallery#", None, 'region'),
+ 'Kak Bazaar': ("the #Kakariko Bazaar#", None, 'region'),
+ 'Kak Potion Shop Front': ("the #Kakariko Potion Shop#", None, 'region'),
+ 'Kak Potion Shop Back': ("the #Kakariko Potion Shop#", None, 'region'),
+ 'Kak Shooting Gallery': ("a #Bow Shooting Gallery#", None, 'region'),
+ 'Colossus Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'HC Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'OGC Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'DMC Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'DMT Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'ZF Great Fairy Fountain': ("a #Great Fairy Fountain#", None, 'region'),
+ 'Graveyard Shield Grave': ("a #grave with a free chest#", None, 'region'),
+ 'Graveyard Heart Piece Grave': ("a chest spawned by #Sun's Song#", None, 'region'),
+ 'Graveyard Composers Grave': ("the #Composers' Grave#", None, 'region'),
+ 'Graveyard Dampes Grave': ("Dampé's Grave", None, 'region'),
+ 'DMT Cow Grotto': ("a solitary #Cow#", None, 'region'),
+ 'HC Storms Grotto': ("a sandy grotto with #fragile walls#", None, 'region'),
+ 'HF Tektite Grotto': ("a pool guarded by a #Tektite#", None, 'region'),
+ 'HF Near Kak Grotto': ("a #Big Skulltula# guarding a Gold one", None, 'region'),
+ 'HF Cow Grotto': ("a grotto full of #spider webs#", None, 'region'),
+ 'Kak Redead Grotto': ("#ReDeads# guarding a chest", None, 'region'),
+ 'SFM Wolfos Grotto': ("#Wolfos# guarding a chest", None, 'region'),
+ 'GV Octorok Grotto': ("an #Octorok# guarding a rich pool", None, 'region'),
+ 'Deku Theater': ("the #Lost Woods Stage#", None, 'region'),
+ 'ZR Open Grotto': ("a #generic grotto#", None, 'region'),
+ 'DMC Upper Grotto': ("a #generic grotto#", None, 'region'),
+ 'DMT Storms Grotto': ("a #generic grotto#", None, 'region'),
+ 'Kak Open Grotto': ("a #generic grotto#", None, 'region'),
+ 'HF Near Market Grotto': ("a #generic grotto#", None, 'region'),
+ 'HF Open Grotto': ("a #generic grotto#", None, 'region'),
+ 'HF Southeast Grotto': ("a #generic grotto#", None, 'region'),
+ 'KF Storms Grotto': ("a #generic grotto#", None, 'region'),
+ 'LW Near Shortcuts Grotto': ("a #generic grotto#", None, 'region'),
+ 'HF Inside Fence Grotto': ("a #single Upgrade Deku Scrub#", None, 'region'),
+ 'LW Scrubs Grotto': ("#2 Deku Scrubs# including an Upgrade one", None, 'region'),
+ 'Colossus Grotto': ("2 Deku Scrubs", None, 'region'),
+ 'ZR Storms Grotto': ("2 Deku Scrubs", None, 'region'),
+ 'SFM Storms Grotto': ("2 Deku Scrubs", None, 'region'),
+ 'GV Storms Grotto': ("2 Deku Scrubs", None, 'region'),
+ 'LH Grotto': ("3 Deku Scrubs", None, 'region'),
+ 'DMC Hammer Grotto': ("3 Deku Scrubs", None, 'region'),
+ 'GC Grotto': ("3 Deku Scrubs", None, 'region'),
+ 'LLR Grotto': ("3 Deku Scrubs", None, 'region'),
+ 'ZR Fairy Grotto': ("a small #Fairy Fountain#", None, 'region'),
+ 'HF Fairy Grotto': ("a small #Fairy Fountain#", None, 'region'),
+ 'SFM Fairy Grotto': ("a small #Fairy Fountain#", None, 'region'),
+ 'ZD Storms Grotto': ("a small #Fairy Fountain#", None, 'region'),
+ 'GF Storms Grotto': ("a small #Fairy Fountain#", None, 'region'),
+
+ '1001': ("Ganondorf 2022!", None, 'junk'),
+ '1002': ("They say that monarchy is a terrible system of governance.", None, 'junk'),
+ '1003': ("They say that Zelda is a poor leader.", None, 'junk'),
+ '1004': ("These hints can be quite useful. This is an exception.", None, 'junk'),
+ '1006': ("They say that all the Zora drowned in Wind Waker.", None, 'junk'),
+ '1008': ("'Member when Ganon was a blue pig?^I 'member.", None, 'junk'),
+ '1009': ("One who does not have Triforce can't go in.", None, 'junk'),
+ '1010': ("Save your future, end the Happy Mask Salesman.", None, 'junk'),
+ '1012': ("I'm stoned. Get it?", None, 'junk'),
+ '1013': ("Hoot! Hoot! Would you like me to repeat that?", None, 'junk'),
+ '1014': ("Gorons are stupid. They eat rocks.", None, 'junk'),
+ '1015': ("They say that Lon Lon Ranch prospered under Ingo.", None, 'junk'),
+ '1016': ("The single rupee is a unique item.", None, 'junk'),
+ '1017': ("Without the Lens of Truth, the Treasure Chest Mini-Game is a 1 out of 32 chance.^Good luck!", None, 'junk'),
+ '1018': ("Use bombs wisely.", None, 'junk'),
+ '1021': ("I found you, faker!", None, 'junk'),
+ '1022': ("You're comparing yourself to me?^Ha! You're not even good enough to be my fake.", None, 'junk'),
+ '1023': ("I'll make you eat those words.", None, 'junk'),
+ '1024': ("What happened to Sheik?", None, 'junk'),
+ '1025': ("L2P @.", None, 'junk'),
+ '1026': ("I've heard Sploosh Kaboom is a tricky game.", None, 'junk'),
+ '1027': ("I'm Lonk from Pennsylvania.", None, 'junk'),
+ '1028': ("I bet you'd like to have more bombs.", None, 'junk'),
+ '1029': ("When all else fails, use Fire.", None, 'junk'),
+ '1030': ("Here's a hint, @. Don't be bad.", None, 'junk'),
+ '1031': ("Game Over. Return of Ganon.", None, 'junk'),
+ '1032': ("May the way of the Hero lead to the Triforce.", None, 'junk'),
+ '1033': ("Can't find an item? Scan an Amiibo.", None, 'junk'),
+ '1034': ("They say this game has just a few glitches.", None, 'junk'),
+ '1035': ("BRRING BRRING This is Ulrira. Wrong number?", None, 'junk'),
+ '1036': ("Tingle Tingle Kooloo Limpah", None, 'junk'),
+ '1037': ("L is real 2041", None, 'junk'),
+ '1038': ("They say that Ganondorf will appear in the next Mario Tennis.", None, 'junk'),
+ '1039': ("Medigoron sells the earliest Breath of the Wild demo.", None, 'junk'),
+ '1040': ("There's a reason why I am special inquisitor!", None, 'junk'),
+ '1041': ("You were almost a @ sandwich.", None, 'junk'),
+ '1042': ("I'm a helpful hint Gossip Stone!^See, I'm helping.", None, 'junk'),
+ '1043': ("Dear @, please come to the castle. I've baked a cake for you.&Yours truly, princess Zelda.", None, 'junk'),
+ '1044': ("They say all toasters toast toast.", None, 'junk'),
+ '1045': ("They say that Okami is the best Zelda game.", None, 'junk'),
+ '1046': ("They say that quest guidance can be found at a talking rock.", None, 'junk'),
+ '1047': ("They say that the final item you're looking for can be found somewhere in Hyrule.", None, 'junk'),
+ '1048': ("Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.^Mweep.", None, 'junk'),
+ '1049': ("They say that Barinade fears Deku Nuts.", None, 'junk'),
+ '1050': ("They say that Flare Dancers do not fear Goron-crafted blades.", None, 'junk'),
+ '1051': ("They say that Morpha is easily trapped in a corner.", None, 'junk'),
+ '1052': ("They say that Bongo Bongo really hates the cold.", None, 'junk'),
+ '1053': ("They say that crouch stabs mimic the effects of your last attack.", None, 'junk'),
+ '1054': ("They say that bombing the hole Volvagia last flew into can be rewarding.", None, 'junk'),
+ '1055': ("They say that invisible ghosts can be exposed with Deku Nuts.", None, 'junk'),
+ '1056': ("They say that the real Phantom Ganon is bright and loud.", None, 'junk'),
+ '1057': ("They say that walking backwards is very fast.", None, 'junk'),
+ '1058': ("They say that leaping above the Market entrance enriches most children.", None, 'junk'),
+ '1059': ("They say that looking into darkness may find darkness looking back into you.", None, 'junk'),
+ '1060': ("You found a spiritual Stone! By which I mean, I worship Nayru.", None, 'junk'),
+ '1061': ("They say that the stick is mightier than the sword.", None, 'junk'),
+ '1062': ("Open your eyes.^Open your eyes.^Wake up, @.", None, 'junk'),
+ '1063': ("They say that arbitrary code execution leads to the credits sequence.", None, 'junk'),
+ '1064': ("They say that Twinrova always casts the same spell the first three times.", None, 'junk'),
+ '1065': ("They say that the Development branch may be unstable.", None, 'junk'),
+ '1066': ("You're playing a Randomizer. I'm randomized!^Here's a random number: #4#.&Enjoy your Randomizer!", None, 'junk'),
+ '1067': ("They say Ganondorf's bolts can be reflected with glass or steel.", None, 'junk'),
+ '1068': ("They say Ganon's tail is vulnerable to nuts, arrows, swords, explosives, hammers...^...sticks, seeds, boomerangs...^...rods, shovels, iron balls, angry bees...", None, 'junk'),
+ '1069': ("They say that you're wasting time reading this hint, but I disagree. Talk to me again!", None, 'junk'),
+ '1070': ("They say Ganondorf knows where to find the instrument of his doom.", None, 'junk'),
+ '1071': ("I heard @ is pretty good at Zelda.", None, 'junk'),
+
+ 'Deku Tree': ("an ancient tree", "Deku Tree", 'dungeonName'),
+ 'Dodongos Cavern': ("an immense cavern", "Dodongo's Cavern", 'dungeonName'),
+ 'Jabu Jabus Belly': ("the belly of a deity", "Jabu Jabu's Belly", 'dungeonName'),
+ 'Forest Temple': ("a deep forest", "Forest Temple", 'dungeonName'),
+ 'Fire Temple': ("a high mountain", "Fire Temple", 'dungeonName'),
+ 'Water Temple': ("a vast lake", "Water Temple", 'dungeonName'),
+ 'Shadow Temple': ("the house of the dead", "Shadow Temple", 'dungeonName'),
+ 'Spirit Temple': ("the goddess of the sand", "Spirit Temple", 'dungeonName'),
+ 'Ice Cavern': ("a frozen maze", "Ice Cavern", 'dungeonName'),
+ 'Bottom of the Well': ("a shadow\'s prison", "Bottom of the Well", 'dungeonName'),
+ 'Gerudo Training Grounds': ("the test of thieves", "Gerudo Training Grounds", 'dungeonName'),
+ 'Ganons Castle': ("a conquered citadel", "Inside Ganon's Castle", 'dungeonName'),
+
+ 'Queen Gohma': ("One inside an #ancient tree#...", "One in the #Deku Tree#...", 'boss'),
+ 'King Dodongo': ("One within an #immense cavern#...", "One in #Dodongo's Cavern#...", 'boss'),
+ 'Barinade': ("One in the #belly of a deity#...", "One in #Jabu Jabu's Belly#...", 'boss'),
+ 'Phantom Ganon': ("One in a #deep forest#...", "One in the #Forest Temple#...", 'boss'),
+ 'Volvagia': ("One on a #high mountain#...", "One in the #Fire Temple#...", 'boss'),
+ 'Morpha': ("One under a #vast lake#...", "One in the #Water Temple#...", 'boss'),
+ 'Bongo Bongo': ("One within the #house of the dead#...", "One in the #Shadow Temple#...", 'boss'),
+ 'Twinrova': ("One inside a #goddess of the sand#...", "One in the #Spirit Temple#...", 'boss'),
+ 'Links Pocket': ("One in #@'s pocket#...", "One #@ already has#...", 'boss'),
+
+ 'bridge_vanilla': ("the #Shadow and Spirit Medallions# as well as the #Light Arrows#", None, 'bridge'),
+ 'bridge_stones': ("Spiritual Stones", None, 'bridge'),
+ 'bridge_medallions': ("Medallions", None, 'bridge'),
+ 'bridge_dungeons': ("Spiritual Stones and Medallions", None, 'bridge'),
+ 'bridge_tokens': ("Gold Skulltula Tokens", None, 'bridge'),
+
+ 'ganonBK_dungeon': ("hidden somewhere #inside its castle#", None, 'ganonBossKey'),
+ 'ganonBK_vanilla': ("kept in a big chest #inside its tower#", None, 'ganonBossKey'),
+ 'ganonBK_overworld': ("hidden #outside of dungeons# in Hyrule", None, 'ganonBossKey'),
+ 'ganonBK_any_dungeon': ("hidden #inside a dungeon# in Hyrule", None, 'ganonBossKey'),
+ 'ganonBK_keysanity': ("hidden somewhere #in Hyrule#", None, 'ganonBossKey'),
+ 'ganonBK_triforce': ("given to the Hero once the #Triforce# is completed", None, 'ganonBossKey'),
+
+ 'lacs_vanilla': ("the #Shadow and Spirit Medallions#", None, 'lacs'),
+ 'lacs_medallions': ("Medallions", None, 'lacs'),
+ 'lacs_stones': ("Spiritual Stones", None, 'lacs'),
+ 'lacs_dungeons': ("Spiritual Stones and Medallions", None, 'lacs'),
+ 'lacs_tokens': ("Gold Skulltula Tokens", None, 'lacs'),
+
+ 'Spiritual Stone Text Start': ("3 Spiritual Stones found in Hyrule...", None, 'altar'),
+ 'Child Altar Text End': ("\x13\x07Ye who may become a Hero...&Stand with the Ocarina and&play the Song of Time.", None, 'altar'),
+ 'Adult Altar Text Start': ("When evil rules all, an awakening&voice from the Sacred Realm will&call those destined to be Sages,&who dwell in the \x05\x41five temples\x05\x40.", None, 'altar'),
+ 'Adult Altar Text End': ("Together with the Hero of Time,&the awakened ones will bind the&evil and return the light of peace&to the world...", None, 'altar'),
+
+ 'Validation Line': ("Hmph... Since you made it this far,&I'll let you know what glorious&prize of Ganon's you likely&missed out on in my tower.^Behold...^", None, 'validation line'),
+ 'Light Arrow Location': ("Ha ha ha... You'll never beat me by&reflecting my lightning bolts&and unleashing the arrows from&", None, 'Light Arrow Location'),
+ '2001': ("Oh! It's @.&I was expecting someone called&Sheik. Do you know what&happened to them?", None, 'ganonLine'),
+ '2002': ("I knew I shouldn't have put the key&on the other side of my door.", None, 'ganonLine'),
+ '2003': ("Looks like it's time for a&round of tennis.", None, 'ganonLine'),
+ '2004': ("You'll never deflect my bolts of&energy with your sword,&then shoot me with those Light&Arrows you happen to have.", None, 'ganonLine'),
+ '2005': ("Why did I leave my trident&back in the desert?", None, 'ganonLine'),
+ '2006': ("Zelda is probably going to do&something stupid, like send you&back to your own timeline.^So this is quite meaningless.&Do you really want&to save this moron?", None, 'ganonLine'),
+ '2007': ("What about Zelda makes you think&she'd be a better ruler than I?^I saved Lon Lon Ranch,&fed the hungry,&and my castle floats.", None, 'ganonLine'),
+ '2008': ("I've learned this spell,&it's really neat,&I'll keep it later&for your treat!", None, 'ganonLine'),
+ '2009': ("Many tricks are up my sleeve,&to save yourself&you'd better leave!", None, 'ganonLine'),
+ '2010': ("After what you did to&Koholint Island, how can&you call me the bad guy?", None, 'ganonLine'),
+ '2011': ("Today, let's begin down&'The Hero is Defeated' timeline.", None, 'ganonLine'),
+}
+
+
+# This specifies which hints will never appear due to either having known or known useless contents or due to the locations not existing.
+def hintExclusions(world, clear_cache=False):
+ if not clear_cache and world.hint_exclusions is not None:
+ return world.hint_exclusions
+
+ world.hint_exclusions = []
+
+ for location in world.get_locations():
+ if location.locked or location.excluded:
+ world.hint_exclusions.append(location.name)
+
+ world_location_names = [
+ location.name for location in world.get_locations()]
+
+ location_hints = []
+ for name in hintTable:
+ hint = getHint(name, world.clearer_hints)
+ if any(item in hint.type for item in
+ ['always',
+ 'sometimes',
+ 'overworld',
+ 'dungeon',
+ 'song']):
+ location_hints.append(hint)
+
+ for hint in location_hints:
+ if hint.name not in world_location_names and hint.name not in world.hint_exclusions:
+ world.hint_exclusions.append(hint.name)
+
+ return world.hint_exclusions
+
+def nameIsLocation(name, hint_type, world):
+ if isinstance(hint_type, (list, tuple)):
+ for htype in hint_type:
+ if htype in ['sometimes', 'song', 'overworld', 'dungeon', 'always'] and name not in hintExclusions(world):
+ return True
+ elif hint_type in ['sometimes', 'song', 'overworld', 'dungeon', 'always'] and name not in hintExclusions(world):
+ return True
+ return False
+
+hintExclusions.exclusions = None
diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py
new file mode 100644
index 00000000..f7af1d12
--- /dev/null
+++ b/worlds/oot/Hints.py
@@ -0,0 +1,1070 @@
+import io
+import hashlib
+import logging
+import os
+import struct
+import random
+from collections import OrderedDict
+import urllib.request
+from urllib.error import URLError, HTTPError
+import json
+from enum import Enum
+
+from .HintList import getHint, getHintGroup, Hint, hintExclusions
+from .Messages import update_message_by_id
+from .TextBox import line_wrap
+from .Utils import data_path, read_json
+
+
+bingoBottlesForHints = (
+ "Bottle", "Bottle with Red Potion","Bottle with Green Potion", "Bottle with Blue Potion",
+ "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs",
+ "Bottle with Big Poe", "Bottle with Poe",
+)
+
+defaultHintDists = [
+ 'balanced.json', 'bingo.json', 'ddr.json', 'scrubs.json', 'strong.json', 'tournament.json', 'useless.json', 'very_strong.json'
+]
+
+class RegionRestriction(Enum):
+ NONE = 0,
+ DUNGEON = 1,
+ OVERWORLD = 2,
+
+
+class GossipStone():
+ def __init__(self, name, location):
+ self.name = name
+ self.location = location
+ self.reachable = True
+
+
+class GossipText():
+ def __init__(self, text, colors=None, prefix="They say that "):
+ text = prefix + text
+ text = text[:1].upper() + text[1:]
+ self.text = text
+ self.colors = colors
+
+
+ def to_json(self):
+ return {'text': self.text, 'colors': self.colors}
+
+
+ def __str__(self):
+ return get_raw_text(line_wrap(colorText(self)))
+
+# Abbreviations
+# DMC Death Mountain Crater
+# DMT Death Mountain Trail
+# GC Goron City
+# GV Gerudo Valley
+# HC Hyrule Castle
+# HF Hyrule Field
+# KF Kokiri Forest
+# LH Lake Hylia
+# LW Lost Woods
+# SFM Sacred Forest Meadow
+# ToT Temple of Time
+# ZD Zora's Domain
+# ZF Zora's Fountain
+# ZR Zora's River
+
+gossipLocations = {
+ 0x0405: GossipStone('DMC (Bombable Wall)', 'DMC Gossip Stone'),
+ 0x0404: GossipStone('DMT (Biggoron)', 'DMT Gossip Stone'),
+ 0x041A: GossipStone('Colossus (Spirit Temple)', 'Colossus Gossip Stone'),
+ 0x0414: GossipStone('Dodongos Cavern (Bombable Wall)', 'Dodongos Cavern Gossip Stone'),
+ 0x0411: GossipStone('GV (Waterfall)', 'GV Gossip Stone'),
+ 0x0415: GossipStone('GC (Maze)', 'GC Maze Gossip Stone'),
+ 0x0419: GossipStone('GC (Medigoron)', 'GC Medigoron Gossip Stone'),
+ 0x040A: GossipStone('Graveyard (Shadow Temple)', 'Graveyard Gossip Stone'),
+ 0x0412: GossipStone('HC (Malon)', 'HC Malon Gossip Stone'),
+ 0x040B: GossipStone('HC (Rock Wall)', 'HC Rock Wall Gossip Stone'),
+ 0x0413: GossipStone('HC (Storms Grotto)', 'HC Storms Grotto Gossip Stone'),
+ 0x041F: GossipStone('KF (Deku Tree Left)', 'KF Deku Tree Gossip Stone (Left)'),
+ 0x0420: GossipStone('KF (Deku Tree Right)', 'KF Deku Tree Gossip Stone (Right)'),
+ 0x041E: GossipStone('KF (Outside Storms)', 'KF Gossip Stone'),
+ 0x0403: GossipStone('LH (Lab)', 'LH Lab Gossip Stone'),
+ 0x040F: GossipStone('LH (Southeast Corner)', 'LH Gossip Stone (Southeast)'),
+ 0x0408: GossipStone('LH (Southwest Corner)', 'LH Gossip Stone (Southwest)'),
+ 0x041D: GossipStone('LW (Bridge)', 'LW Gossip Stone'),
+ 0x0416: GossipStone('SFM (Maze Lower)', 'SFM Maze Gossip Stone (Lower)'),
+ 0x0417: GossipStone('SFM (Maze Upper)', 'SFM Maze Gossip Stone (Upper)'),
+ 0x041C: GossipStone('SFM (Saria)', 'SFM Saria Gossip Stone'),
+ 0x0406: GossipStone('ToT (Left)', 'ToT Gossip Stone (Left)'),
+ 0x0407: GossipStone('ToT (Left-Center)', 'ToT Gossip Stone (Left-Center)'),
+ 0x0410: GossipStone('ToT (Right)', 'ToT Gossip Stone (Right)'),
+ 0x040E: GossipStone('ToT (Right-Center)', 'ToT Gossip Stone (Right-Center)'),
+ 0x0409: GossipStone('ZD (Mweep)', 'ZD Gossip Stone'),
+ 0x0401: GossipStone('ZF (Fairy)', 'ZF Fairy Gossip Stone'),
+ 0x0402: GossipStone('ZF (Jabu)', 'ZF Jabu Gossip Stone'),
+ 0x040D: GossipStone('ZR (Near Grottos)', 'ZR Near Grottos Gossip Stone'),
+ 0x040C: GossipStone('ZR (Near Domain)', 'ZR Near Domain Gossip Stone'),
+ 0x041B: GossipStone('HF (Cow Grotto)', 'HF Cow Grotto Gossip Stone'),
+
+ 0x0430: GossipStone('HF (Near Market Grotto)', 'HF Near Market Grotto Gossip Stone'),
+ 0x0432: GossipStone('HF (Southeast Grotto)', 'HF Southeast Grotto Gossip Stone'),
+ 0x0433: GossipStone('HF (Open Grotto)', 'HF Open Grotto Gossip Stone'),
+ 0x0438: GossipStone('Kak (Open Grotto)', 'Kak Open Grotto Gossip Stone'),
+ 0x0439: GossipStone('ZR (Open Grotto)', 'ZR Open Grotto Gossip Stone'),
+ 0x043C: GossipStone('KF (Storms Grotto)', 'KF Storms Grotto Gossip Stone'),
+ 0x0444: GossipStone('LW (Near Shortcuts Grotto)', 'LW Near Shortcuts Grotto Gossip Stone'),
+ 0x0447: GossipStone('DMT (Storms Grotto)', 'DMT Storms Grotto Gossip Stone'),
+ 0x044A: GossipStone('DMC (Upper Grotto)', 'DMC Upper Grotto Gossip Stone'),
+}
+
+gossipLocations_reversemap = {
+ stone.name : stone_id for stone_id, stone in gossipLocations.items()
+}
+
+def getItemGenericName(item):
+ if item.game != "Ocarina of Time":
+ return item.name
+ elif item.dungeonitem:
+ return item.type
+ else:
+ return item.name
+
+
+def isRestrictedDungeonItem(dungeon, item):
+ if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon':
+ return item in dungeon.dungeon_items
+ if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon':
+ return item in dungeon.small_keys
+ if item.type == 'BossKey' and dungeon.world.shuffle_bosskeys == 'dungeon':
+ return item in dungeon.boss_key
+ if item.type == 'GanonBossKey' and dungeon.world.shuffle_ganon_bosskey == 'dungeon':
+ return item in dungeon.boss_key
+ return False
+
+
+# Attach a player name to the item or location text.
+# If the associated player of the item/location and the world are the same, does nothing.
+# Otherwise, attaches the object's player's name to the string.
+def attach_name(text, hinted_object, world):
+ if hinted_object.player == world.player:
+ return text
+ return f"{text} for {world.world.get_player_name(hinted_object.player)}"
+
+
+def add_hint(world, groups, gossip_text, count, location=None, force_reachable=False):
+ world.hint_rng.shuffle(groups)
+ skipped_groups = []
+ duplicates = []
+ first = True
+ success = True
+ # early failure if not enough
+ if len(groups) < int(count):
+ return False
+ # Randomly round up, if we have enough groups left
+ total = int(world.hint_rng.random() + count) if len(groups) > count else int(count)
+ while total:
+ if groups:
+ group = groups.pop(0)
+
+ if any(map(lambda id: gossipLocations[id].reachable, group)):
+ stone_names = [gossipLocations[id].location for id in group]
+ # stone_locations = [world.get_location(stone_name) for stone_name in stone_names]
+ # Taking out all checks on gossip stone reachability and hint logic
+ if not first or True: # or any(map(lambda stone_location: can_reach_hint(worlds, stone_location, location), stone_locations)):
+ # if first and location:
+ # # just name the event item after the gossip stone directly
+ # event_item = None
+ # for i, stone_name in enumerate(stone_names):
+ # # place the same event item in each location in the group
+ # if event_item is None:
+ # event_item = MakeEventItem(stone_name, stone_locations[i], event_item)
+ # else:
+ # MakeEventItem(stone_name, stone_locations[i], event_item)
+
+ # # This mostly guarantees that we don't lock the player out of an item hint
+ # # by establishing a (hint -> item) -> hint -> item -> (first hint) loop
+ # location.add_rule(world.parser.parse_rule(repr(event_item.name)))
+
+ total -= 1
+ first = False
+ for id in group:
+ world.gossip_hints[id] = gossip_text
+ # Immediately start choosing duplicates from stones we passed up earlier
+ while duplicates and total:
+ group = duplicates.pop(0)
+ total -= 1
+ for id in group:
+ world.gossip_hints[id] = gossip_text
+ else:
+ # Temporarily skip this stone but consider it for duplicates
+ duplicates.append(group)
+ else:
+ if not force_reachable:
+ # The stones are not readable at all in logic, so we ignore any kind of logic here
+ if not first:
+ total -= 1
+ for id in group:
+ world.gossip_hints[id] = gossip_text
+ else:
+ # Temporarily skip this stone but consider it for duplicates
+ duplicates.append(group)
+ else:
+ # If flagged to guarantee reachable, then skip
+ # If no stones are reachable, then this will place nothing
+ skipped_groups.append(group)
+ else:
+ # Out of groups
+ if not force_reachable and len(duplicates) >= total:
+ # Didn't find any appropriate stones for this hint, but maybe enough completely unreachable ones.
+ # We'd rather not use reachable stones for this.
+ unr = [group for group in duplicates if all(map(lambda id: not gossipLocations[id].reachable, group))]
+ if len(unr) >= total:
+ duplicates = [group for group in duplicates if group not in unr[:total]]
+ for group in unr[:total]:
+ for id in group:
+ world.gossip_hints[id] = gossip_text
+ # Success
+ break
+ # Failure
+ success = False
+ break
+ groups.extend(duplicates)
+ groups.extend(skipped_groups)
+ return success
+
+
+
+def writeGossipStoneHints(world, messages):
+ for id, gossip_text in world.gossip_hints.items():
+ update_message_by_id(messages, id, str(gossip_text), 0x23)
+
+
+def filterTrailingSpace(text):
+ if text.endswith('& '):
+ return text[:-1]
+ else:
+ return text
+
+
+hintPrefixes = [
+ 'a few ',
+ 'some ',
+ 'plenty of ',
+ 'a ',
+ 'an ',
+ 'the ',
+ '',
+]
+
+def getSimpleHintNoPrefix(item):
+ hint = getHint(item.name, True).text
+
+ for prefix in hintPrefixes:
+ if hint.startswith(prefix):
+ # return without the prefix
+ return hint[len(prefix):]
+
+ # no prefex
+ return hint
+
+
+def colorText(gossip_text):
+ colorMap = {
+ 'White': '\x40',
+ 'Red': '\x41',
+ 'Green': '\x42',
+ 'Blue': '\x43',
+ 'Light Blue': '\x44',
+ 'Pink': '\x45',
+ 'Yellow': '\x46',
+ 'Black': '\x47',
+ }
+
+ text = gossip_text.text
+ colors = list(gossip_text.colors) if gossip_text.colors is not None else []
+ color = 'White'
+
+ while '#' in text:
+ splitText = text.split('#', 2)
+ if len(colors) > 0:
+ color = colors.pop()
+
+ for prefix in hintPrefixes:
+ if splitText[1].startswith(prefix):
+ splitText[0] += splitText[1][:len(prefix)]
+ splitText[1] = splitText[1][len(prefix):]
+ break
+
+ splitText[1] = '\x05' + colorMap[color] + splitText[1] + '\x05\x40'
+ text = ''.join(splitText)
+
+ return text
+
+
+class HintAreaNotFound(RuntimeError):
+ pass
+
+
+# Peforms a breadth first search to find the closest hint area from a given spot (location or entrance)
+# May fail to find a hint if the given spot is only accessible from the root and not from any other region with a hint area
+# Returns the name of the location if the spot is not in OoT
+def get_hint_area(spot):
+ if spot.game == 'Ocarina of Time':
+ already_checked = []
+ spot_queue = [spot]
+
+ while spot_queue:
+ current_spot = spot_queue.pop(0)
+ already_checked.append(current_spot)
+
+ parent_region = current_spot.parent_region
+
+ if parent_region.dungeon:
+ return parent_region.dungeon.hint_text
+ elif parent_region.hint_text and (spot.parent_region.name == 'Root' or parent_region.name != 'Root'):
+ return parent_region.hint_text
+
+ spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances)))
+
+ raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id))
+ else:
+ return spot.name
+
+
+def get_woth_hint(world, checked):
+ locations = world.required_locations
+ locations = list(filter(lambda location:
+ location.name not in checked[location.player]
+ and not (world.woth_dungeon >= world.hint_dist_user['dungeons_woth_limit'] and location.parent_region.dungeon)
+ and location.name not in world.hint_exclusions
+ and location.name not in world.hint_type_overrides['woth']
+ and location.item.name not in world.item_hint_type_overrides['woth'],
+ locations))
+
+ if not locations:
+ return None
+
+ location = world.hint_rng.choice(locations)
+ checked[location.player].add(location.name)
+
+ if location.parent_region.dungeon:
+ world.woth_dungeon += 1
+ location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ else:
+ location_text = get_hint_area(location)
+
+ if world.triforce_hunt:
+ return (GossipText('#%s# is on the path of gold.' % location_text, ['Light Blue']), location)
+ else:
+ return (GossipText('#%s# is on the way of the hero.' % location_text, ['Light Blue']), location)
+
+
+def get_barren_hint(world, checked):
+ if not hasattr(world, 'get_barren_hint_prev'):
+ world.get_barren_hint_prev = RegionRestriction.NONE
+
+ areas = list(filter(lambda area:
+ area not in checked[world.player]
+ and area not in world.hint_type_overrides['barren']
+ and not (world.barren_dungeon >= world.hint_dist_user['dungeons_barren_limit'] and world.empty_areas[area]['dungeon']),
+ world.empty_areas.keys()))
+
+ if not areas:
+ return None
+
+ # Randomly choose between overworld or dungeon
+ dungeon_areas = list(filter(lambda area: world.empty_areas[area]['dungeon'], areas))
+ overworld_areas = list(filter(lambda area: not world.empty_areas[area]['dungeon'], areas))
+ if not dungeon_areas:
+ # no dungeons left, default to overworld
+ world.get_barren_hint_prev = RegionRestriction.OVERWORLD
+ elif not overworld_areas:
+ # no overworld left, default to dungeons
+ world.get_barren_hint_prev = RegionRestriction.DUNGEON
+ else:
+ if world.get_barren_hint_prev == RegionRestriction.NONE:
+ # 50/50 draw on the first hint
+ world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.5, 0.5])[0]
+ elif world.get_barren_hint_prev == RegionRestriction.DUNGEON:
+ # weights 75% against drawing dungeon again
+ world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.25, 0.75])[0]
+ elif world.get_barren_hint_prev == RegionRestriction.OVERWORLD:
+ # weights 75% against drawing overworld again
+ world.get_barren_hint_prev = world.hint_rng.choices([RegionRestriction.DUNGEON, RegionRestriction.OVERWORLD], [0.75, 0.25])[0]
+
+ if world.get_barren_hint_prev == RegionRestriction.DUNGEON:
+ areas = dungeon_areas
+ else:
+ areas = overworld_areas
+ if not areas:
+ return None
+
+ area_weights = [world.empty_areas[area]['weight'] for area in areas]
+
+ area = world.hint_rng.choices(areas, weights=area_weights)[0]
+ if world.empty_areas[area]['dungeon']:
+ world.barren_dungeon += 1
+
+ checked[world.player].add(area)
+
+ return (GossipText("plundering #%s# is a foolish choice." % area, ['Pink']), None)
+
+
+def is_not_checked(location, checked):
+ return not (location.name in checked[location.player] or get_hint_area(location) in checked)
+
+
+def get_good_item_hint(world, checked):
+ locations = list(filter(lambda location:
+ is_not_checked(location, checked)
+ and not location.locked
+ and location.name not in world.hint_exclusions
+ and location.name not in world.hint_type_overrides['item']
+ and location.item.name not in world.item_hint_type_overrides['item'],
+ world.major_item_locations))
+ if not locations:
+ return None
+
+ location = world.hint_rng.choice(locations)
+ checked[location.player].add(location.name)
+
+ item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ if location.parent_region.dungeon:
+ location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
+ ['Green', 'Red']), location)
+ else:
+ location_text = get_hint_area(location)
+ return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),
+ ['Red', 'Green']), location)
+
+
+def get_specific_item_hint(world, checked):
+ if len(world.named_item_pool) == 0:
+ logger = logging.getLogger('')
+ logger.info("Named item hint requested, but pool is empty.")
+ return None
+ while True:
+ itemname = world.named_item_pool.pop(0)
+ if itemname == "Bottle" and world.hint_dist == "bingo":
+ locations = [
+ location for location in world.world.get_filled_locations()
+ if (is_not_checked(location, checked)
+ and location.name not in world.hint_exclusions
+ and location.item.name in bingoBottlesForHints
+ and not location.locked
+ and location.name not in world.hint_type_overrides['named-item'])
+ ]
+ else:
+ locations = [
+ location for location in world.world.get_filled_locations()
+ if (is_not_checked(location, checked)
+ and location.name not in world.hint_exclusions
+ and location.item.name == itemname
+ and not location.locked
+ and location.name not in world.hint_type_overrides['named-item'])
+ ]
+ if len(locations) > 0:
+ break
+ if len(world.named_item_pool) == 0:
+ return None
+
+ location = world.hint_rng.choice(locations)
+ checked[location.player].add(location.name)
+ item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+
+ if location.parent_region.dungeon:
+ location_text = getHint(location.parent_region.dungeon.name, world.clearer_hints).text
+ if world.hint_dist_user.get('vague_named_items', False):
+ return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
+ else:
+ return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
+ ['Green', 'Red']), location)
+ else:
+ location_text = get_hint_area(location)
+ if world.hint_dist_user.get('vague_named_items', False):
+ return (GossipText('#%s# may be on the hero\'s path.' % (location_text), ['Green']), location)
+ else:
+ return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),
+ ['Red', 'Green']), location)
+
+
+def get_random_location_hint(world, checked):
+ locations = list(filter(lambda location:
+ is_not_checked(location, checked)
+ and location.item.type not in ('Drop', 'Event', 'Shop', 'DungeonReward')
+ # and not (location.parent_region.dungeon and isRestrictedDungeonItem(location.parent_region.dungeon, location.item)) # AP already locks dungeon items
+ and not location.locked
+ and location.name not in world.hint_exclusions
+ and location.name not in world.hint_type_overrides['item']
+ and location.item.name not in world.item_hint_type_overrides['item'],
+ world.world.get_filled_locations(world.player)))
+ if not locations:
+ return None
+
+ location = world.hint_rng.choice(locations)
+ checked[location.player].add(location.name)
+ dungeon = location.parent_region.dungeon
+
+ item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ if dungeon:
+ location_text = getHint(dungeon.name, world.clearer_hints).text
+ return (GossipText('#%s# hoards #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
+ ['Green', 'Red']), location)
+ else:
+ location_text = get_hint_area(location)
+ return (GossipText('#%s# can be found at #%s#.' % (attach_name(item_text, location.item, world), attach_name(location_text, location, world)),
+ ['Red', 'Green']), location)
+
+
+def get_specific_hint(world, checked, type):
+ hintGroup = getHintGroup(type, world)
+ hintGroup = list(filter(lambda hint: is_not_checked(world.get_location(hint.name), checked), hintGroup))
+ if not hintGroup:
+ return None
+
+ hint = world.hint_rng.choice(hintGroup)
+ location = world.get_location(hint.name)
+ checked[location.player].add(location.name)
+
+ if location.name in world.hint_text_overrides:
+ location_text = world.hint_text_overrides[location.name]
+ else:
+ location_text = hint.text
+ if '#' not in location_text:
+ location_text = '#%s#' % location_text
+ item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+
+ return (GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
+ ['Green', 'Red']), location)
+
+
+def get_sometimes_hint(world, checked):
+ return get_specific_hint(world, checked, 'sometimes')
+
+
+def get_song_hint(world, checked):
+ return get_specific_hint(world, checked, 'song')
+
+
+def get_overworld_hint(world, checked):
+ return get_specific_hint(world, checked, 'overworld')
+
+
+def get_dungeon_hint(world, checked):
+ return get_specific_hint(world, checked, 'dungeon')
+
+
+# probably broken
+def get_entrance_hint(world, checked):
+ if not world.entrance_shuffle:
+ return None
+
+ entrance_hints = list(filter(lambda hint: hint.name not in checked[world.player], getHintGroup('entrance', world)))
+ shuffled_entrance_hints = list(filter(lambda entrance_hint: world.get_entrance(entrance_hint.name).shuffled, entrance_hints))
+
+ regions_with_hint = [hint.name for hint in getHintGroup('region', world)]
+ valid_entrance_hints = list(filter(lambda entrance_hint:
+ (world.get_entrance(entrance_hint.name).connected_region.name in regions_with_hint or
+ world.get_entrance(entrance_hint.name).connected_region.dungeon), shuffled_entrance_hints))
+
+ if not valid_entrance_hints:
+ return None
+
+ entrance_hint = world.hint_rng.choice(valid_entrance_hints)
+ entrance = world.get_entrance(entrance_hint.name)
+ checked[world.player].add(entrance.name)
+
+ entrance_text = entrance_hint.text
+
+ if '#' not in entrance_text:
+ entrance_text = '#%s#' % entrance_text
+
+ connected_region = entrance.connected_region
+ if connected_region.dungeon:
+ region_text = getHint(connected_region.dungeon.name, world.clearer_hints).text
+ else:
+ region_text = getHint(connected_region.name, world.clearer_hints).text
+
+ if '#' not in region_text:
+ region_text = '#%s#' % region_text
+
+ return (GossipText('%s %s.' % (entrance_text, region_text), ['Light Blue', 'Green']), None)
+
+
+def get_junk_hint(world, checked):
+ hints = getHintGroup('junk', world)
+ hints = list(filter(lambda hint: hint.name not in checked[world.player], hints))
+ if not hints:
+ return None
+
+ hint = world.hint_rng.choice(hints)
+ checked[world.player].add(hint.name)
+
+ return (GossipText(hint.text, prefix=''), None)
+
+
+hint_func = {
+ 'trial': lambda world, checked: None,
+ 'always': lambda world, checked: None,
+ 'woth': get_woth_hint,
+ 'barren': get_barren_hint,
+ 'item': get_good_item_hint,
+ 'sometimes': get_sometimes_hint,
+ 'song': get_song_hint,
+ 'overworld': get_overworld_hint,
+ 'dungeon': get_dungeon_hint,
+ 'entrance': get_entrance_hint,
+ 'random': get_random_location_hint,
+ 'junk': get_junk_hint,
+ 'named-item': get_specific_item_hint
+}
+
+hint_dist_keys = {
+ 'trial',
+ 'always',
+ 'woth',
+ 'barren',
+ 'item',
+ 'song',
+ 'overworld',
+ 'dungeon',
+ 'entrance',
+ 'sometimes',
+ 'random',
+ 'junk',
+ 'named-item'
+}
+
+
+
+# builds out general hints based on location and whether an item is required or not
+def buildWorldGossipHints(world, checkedLocations=None):
+ # Seed the RNG
+ world.hint_rng = world.world.slot_seeds[world.player]
+
+ # Gather woth, barren, major items
+ world.gather_hint_data()
+
+ # rebuild hint exclusion list
+ hintExclusions(world, clear_cache=True)
+
+ world.barren_dungeon = 0
+ world.woth_dungeon = 0
+
+ # search = Search.max_explore([w.state for w in orlds])
+ # for stone in gossipLocations.values():
+ # stone.reachable = (
+ # search.spot_access(world.get_location(stone.location))
+ # and search.state_list[world.id].guarantee_hint())
+
+ if checkedLocations is None:
+ checkedLocations = {player: set() for player in world.world.player_ids}
+
+ # If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting
+ # Can only be forced with vanilla bridge
+ if world.bridge != 'vanilla':
+ try:
+ light_arrow_location = world.world.find_item("Light Arrows", world.player)
+ checkedLocations[light_arrow_location.player].add(light_arrow_location.name)
+ except StopIteration: # start with them
+ pass
+
+ stoneIDs = list(gossipLocations.keys())
+
+ # world.distribution.configure_gossip(stoneIDs)
+
+ if 'disabled' in world.hint_dist_user:
+ for stone_name in world.hint_dist_user['disabled']:
+ try:
+ stone_id = gossipLocations_reversemap[stone_name]
+ except KeyError:
+ raise ValueError(f'Gossip stone location "{stone_name}" is not valid')
+ stoneIDs.remove(stone_id)
+ (gossip_text, _) = get_junk_hint(world, checkedLocations)
+ world.gossip_hints[stone_id] = gossip_text
+
+ stoneGroups = []
+ if 'groups' in world.hint_dist_user:
+ for group_names in world.hint_dist_user['groups']:
+ group = []
+ for stone_name in group_names:
+ try:
+ stone_id = gossipLocations_reversemap[stone_name]
+ except KeyError:
+ raise ValueError(f'Gossip stone location "{stone_name}" is not valid')
+
+ stoneIDs.remove(stone_id)
+ group.append(stone_id)
+ stoneGroups.append(group)
+ # put the remaining locations into singleton groups
+ stoneGroups.extend([[id] for id in stoneIDs])
+
+ world.hint_rng.shuffle(stoneGroups)
+
+
+ # Load hint distro from distribution file or pre-defined settings
+ #
+ # 'fixed' key is used to mimic the tournament distribution, creating a list of fixed hint types to fill
+ # Once the fixed hint type list is exhausted, weighted random choices are taken like all non-tournament sets
+ # This diverges from the tournament distribution where leftover stones are filled with sometimes hints (or random if no sometimes locations remain to be hinted)
+ sorted_dist = {}
+ type_count = 1
+ hint_dist = OrderedDict({})
+ fixed_hint_types = []
+ max_order = 0
+ for hint_type in world.hint_dist_user['distribution']:
+ if world.hint_dist_user['distribution'][hint_type]['order'] > 0:
+ hint_order = int(world.hint_dist_user['distribution'][hint_type]['order'])
+ sorted_dist[hint_order] = hint_type
+ if max_order < hint_order:
+ max_order = hint_order
+ type_count = type_count + 1
+ if (type_count - 1) < max_order:
+ raise Exception("There are gaps in the custom hint orders. Please revise your plando file to remove them.")
+ for i in range(1, type_count):
+ hint_type = sorted_dist[i]
+ if world.hint_dist_user['distribution'][hint_type]['copies'] > 0:
+ fixed_num = world.hint_dist_user['distribution'][hint_type]['fixed']
+ hint_weight = world.hint_dist_user['distribution'][hint_type]['weight']
+ else:
+ logging.getLogger('').warning("Hint copies is zero for type %s. Assuming this hint type should be disabled.", hint_type)
+ fixed_num = 0
+ hint_weight = 0
+ hint_dist[hint_type] = (hint_weight, world.hint_dist_user['distribution'][hint_type]['copies'])
+ hint_dist.move_to_end(hint_type)
+ fixed_hint_types.extend([hint_type] * int(fixed_num))
+
+ hint_types, hint_prob = zip(*hint_dist.items())
+ hint_prob, _ = zip(*hint_prob)
+
+ # Add required location hints, only if hint copies > 0
+ if hint_dist['always'][1] > 0:
+ alwaysLocations = getHintGroup('always', world)
+ for hint in alwaysLocations:
+ location = world.get_location(hint.name)
+ checkedLocations[location.player].add(hint.name)
+ if location.item.name in bingoBottlesForHints and world.hint_dist == 'bingo':
+ always_item = 'Bottle'
+ else:
+ always_item = location.item.name
+ if always_item in world.named_item_pool:
+ world.named_item_pool.remove(always_item)
+
+ if location.name in world.hint_text_overrides:
+ location_text = world.hint_text_overrides[location.name]
+ else:
+ location_text = getHint(location.name, world.clearer_hints).text
+ if '#' not in location_text:
+ location_text = '#%s#' % location_text
+ item_text = getHint(getItemGenericName(location.item), world.clearer_hints).text
+ add_hint(world, stoneGroups, GossipText('%s #%s#.' % (attach_name(location_text, location, world), attach_name(item_text, location.item, world)),
+ ['Green', 'Red']), hint_dist['always'][1], location, force_reachable=True)
+ logging.getLogger('').debug('Placed always hint for %s.', location.name)
+
+ # Add trial hints, only if hint copies > 0
+ if hint_dist['trial'][1] > 0:
+ if world.trials == 6:
+ add_hint(world, stoneGroups, GossipText("#Ganon's Tower# is protected by a powerful barrier.", ['Pink']), hint_dist['trial'][1], force_reachable=True)
+ elif world.trials == 0:
+ add_hint(world, stoneGroups, GossipText("Sheik dispelled the barrier around #Ganon's Tower#.", ['Yellow']), hint_dist['trial'][1], force_reachable=True)
+ elif world.trials < 6 and world.trials > 3:
+ for trial,skipped in world.skipped_trials.items():
+ if skipped:
+ add_hint(world, stoneGroups,GossipText("the #%s Trial# was dispelled by Sheik." % trial, ['Yellow']), hint_dist['trial'][1], force_reachable=True)
+ elif world.trials <= 3 and world.trials > 0:
+ for trial,skipped in world.skipped_trials.items():
+ if not skipped:
+ add_hint(world, stoneGroups, GossipText("the #%s Trial# protects Ganon's Tower." % trial, ['Pink']), hint_dist['trial'][1], force_reachable=True)
+
+ # Add user-specified hinted item locations if using a built-in hint distribution
+ # Raise error if hint copies is zero
+ if len(world.named_item_pool) > 0 and world.hint_dist_user['named_items_required']:
+ if hint_dist['named-item'][1] == 0:
+ raise Exception('User-provided item hints were requested, but copies per named-item hint is zero')
+ else:
+ for i in range(0, len(world.named_item_pool)):
+ hint = get_specific_item_hint(world, checkedLocations)
+ if hint == None:
+ raise Exception('No valid hints for user-provided item')
+ else:
+ gossip_text, location = hint
+ place_ok = add_hint(world, stoneGroups, gossip_text, hint_dist['named-item'][1], location)
+ if not place_ok:
+ raise Exception('Not enough gossip stones for user-provided item hints')
+
+ # Shuffle named items hints
+ # When all items are not required to be hinted, this allows for
+ # opportunity-style hints to be drawn at random from the defined list.
+ world.hint_rng.shuffle(world.named_item_pool)
+
+ hint_types = list(hint_types)
+ hint_prob = list(hint_prob)
+ hint_counts = {}
+
+ custom_fixed = True
+ while stoneGroups:
+ if fixed_hint_types:
+ hint_type = fixed_hint_types.pop(0)
+ copies = hint_dist[hint_type][1]
+ if copies > len(stoneGroups):
+ # Quiet to avoid leaking information.
+ logging.getLogger('').debug(f'Not enough gossip stone locations ({len(stoneGroups)} groups) for fixed hint type {hint_type} with {copies} copies, proceeding with available stones.')
+ copies = len(stoneGroups)
+ else:
+ custom_fixed = False
+ # Make sure there are enough stones left for each hint type
+ num_types = len(hint_types)
+ hint_types = list(filter(lambda htype: hint_dist[htype][1] <= len(stoneGroups), hint_types))
+ new_num_types = len(hint_types)
+ if new_num_types == 0:
+ raise Exception('Not enough gossip stone locations for remaining weighted hint types.')
+ elif new_num_types < num_types:
+ hint_prob = []
+ for htype in hint_types:
+ hint_prob.append(hint_dist[htype][0])
+ try:
+ # Weight the probabilities such that hints that are over the expected proportion
+ # will be drawn less, and hints that are under will be drawn more.
+ # This tightens the variance quite a bit. The variance can be adjusted via the power
+ weighted_hint_prob = []
+ for w1_type, w1_prob in zip(hint_types, hint_prob):
+ p = w1_prob
+ if p != 0: # If the base prob is 0, then it's 0
+ for w2_type, w2_prob in zip(hint_types, hint_prob):
+ if w2_prob != 0: # If the other prob is 0, then it has no effect
+ # Raising this term to a power greater than 1 will decrease variance
+ # Conversely, a power less than 1 will increase variance
+ p = p * (((hint_counts.get(w2_type, 0) / w2_prob) + 1) / ((hint_counts.get(w1_type, 0) / w1_prob) + 1))
+ weighted_hint_prob.append(p)
+
+ hint_type = world.hint_rng.choices(hint_types, weights=weighted_hint_prob)[0]
+ copies = hint_dist[hint_type][1]
+ except IndexError:
+ raise Exception('Not enough valid hints to fill gossip stone locations.')
+
+ hint = hint_func[hint_type](world, checkedLocations)
+
+ if hint == None:
+ index = hint_types.index(hint_type)
+ hint_prob[index] = 0
+ # Zero out the probability in the base distribution in case the probability list is modified
+ # to fit hint types in remaining gossip stones
+ hint_dist[hint_type] = (0.0, copies)
+ else:
+ gossip_text, location = hint
+ place_ok = add_hint(world, stoneGroups, gossip_text, copies, location)
+ if place_ok:
+ hint_counts[hint_type] = hint_counts.get(hint_type, 0) + 1
+ if location is None:
+ logging.getLogger('').debug('Placed %s hint.', hint_type)
+ else:
+ logging.getLogger('').debug('Placed %s hint for %s.', hint_type, location.name)
+ if not place_ok and custom_fixed:
+ logging.getLogger('').debug('Failed to place %s fixed hint for %s.', hint_type, location.name)
+ fixed_hint_types.insert(0, hint_type)
+
+
+# builds text that is displayed at the temple of time altar for child and adult, rewards pulled based off of item in a fixed order.
+def buildAltarHints(world, messages, include_rewards=True, include_wincons=True):
+ # text that appears at altar as a child.
+ child_text = '\x08'
+ if include_rewards:
+ bossRewardsSpiritualStones = [
+ ('Kokiri Emerald', 'Green'),
+ ('Goron Ruby', 'Red'),
+ ('Zora Sapphire', 'Blue'),
+ ]
+ child_text += getHint('Spiritual Stone Text Start', world.clearer_hints).text + '\x04'
+ for (reward, color) in bossRewardsSpiritualStones:
+ child_text += buildBossString(reward, color, world)
+ child_text += getHint('Child Altar Text End', world.clearer_hints).text
+ child_text += '\x0B'
+ update_message_by_id(messages, 0x707A, get_raw_text(child_text), 0x20)
+
+ # text that appears at altar as an adult.
+ adult_text = '\x08'
+ adult_text += getHint('Adult Altar Text Start', world.clearer_hints).text + '\x04'
+ if include_rewards:
+ bossRewardsMedallions = [
+ ('Light Medallion', 'Light Blue'),
+ ('Forest Medallion', 'Green'),
+ ('Fire Medallion', 'Red'),
+ ('Water Medallion', 'Blue'),
+ ('Shadow Medallion', 'Pink'),
+ ('Spirit Medallion', 'Yellow'),
+ ]
+ for (reward, color) in bossRewardsMedallions:
+ adult_text += buildBossString(reward, color, world)
+ if include_wincons:
+ adult_text += buildBridgeReqsString(world)
+ adult_text += '\x04'
+ adult_text += buildGanonBossKeyString(world)
+ else:
+ adult_text += getHint('Adult Altar Text End', world.clearer_hints).text
+ adult_text += '\x0B'
+ update_message_by_id(messages, 0x7057, get_raw_text(adult_text), 0x20)
+
+
+# pulls text string from hintlist for reward after sending the location to hintlist.
+def buildBossString(reward, color, world):
+ for location in world.world.get_filled_locations(world.player):
+ if location.item.name == reward:
+ item_icon = chr(location.item.special['item_id'])
+ location_text = getHint(location.name, world.clearer_hints).text
+ return str(GossipText("\x08\x13%s%s" % (item_icon, location_text), [color], prefix='')) + '\x04'
+ return ''
+
+
+def buildBridgeReqsString(world):
+ string = "\x13\x12" # Light Arrow Icon
+ if world.bridge == 'open':
+ string += "The awakened ones will have #already created a bridge# to the castle where the evil dwells."
+ else:
+ item_req_string = getHint('bridge_' + world.bridge, world.clearer_hints).text
+ if world.bridge == 'medallions':
+ item_req_string = str(world.bridge_medallions) + ' ' + item_req_string
+ elif world.bridge == 'stones':
+ item_req_string = str(world.bridge_stones) + ' ' + item_req_string
+ elif world.bridge == 'dungeons':
+ item_req_string = str(world.bridge_rewards) + ' ' + item_req_string
+ elif world.bridge == 'tokens':
+ item_req_string = str(world.bridge_tokens) + ' ' + item_req_string
+ if '#' not in item_req_string:
+ item_req_string = '#%s#' % item_req_string
+ string += "The awakened ones will await for the Hero to collect %s." % item_req_string
+ return str(GossipText(string, ['Green'], prefix=''))
+
+
+def buildGanonBossKeyString(world):
+ string = "\x13\x74" # Boss Key Icon
+ if world.shuffle_ganon_bosskey == 'remove':
+ string += "And the door to the \x05\x41evil one\x05\x40's chamber will be left #unlocked#."
+ else:
+ if world.shuffle_ganon_bosskey == 'on_lacs':
+ item_req_string = getHint('lacs_' + world.lacs_condition, world.clearer_hints).text
+ if world.lacs_condition == 'medallions':
+ item_req_string = str(world.lacs_medallions) + ' ' + item_req_string
+ elif world.lacs_condition == 'stones':
+ item_req_string = str(world.lacs_stones) + ' ' + item_req_string
+ elif world.lacs_condition == 'dungeons':
+ item_req_string = str(world.lacs_rewards) + ' ' + item_req_string
+ elif world.lacs_condition == 'tokens':
+ item_req_string = str(world.lacs_tokens) + ' ' + item_req_string
+ if '#' not in item_req_string:
+ item_req_string = '#%s#' % item_req_string
+ bk_location_string = "provided by Zelda once %s are retrieved" % item_req_string
+ else:
+ bk_location_string = getHint('ganonBK_' + world.shuffle_ganon_bosskey, world.clearer_hints).text
+ string += "And the \x05\x41evil one\x05\x40's key will be %s." % bk_location_string
+ return str(GossipText(string, ['Yellow'], prefix=''))
+
+
+# fun new lines for Ganon during the final battle
+def buildGanonText(world, messages):
+ # empty now unused messages to make space for ganon lines
+ update_message_by_id(messages, 0x70C8, " ")
+ update_message_by_id(messages, 0x70C9, " ")
+ update_message_by_id(messages, 0x70CA, " ")
+
+ # lines before battle
+ ganonLines = getHintGroup('ganonLine', world)
+ world.hint_rng.shuffle(ganonLines)
+ text = get_raw_text(ganonLines.pop().text)
+ update_message_by_id(messages, 0x70CB, text)
+
+ # light arrow hint or validation chest item
+ if world.starting_items['Light Arrows'] > 0:
+ text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
+ text += "\x05\x42your pocket\x05\x40"
+ else:
+ try:
+ find_light_arrows = world.world.find_item('Light Arrows', world.player)
+ text = get_raw_text(getHint('Light Arrow Location', world.clearer_hints).text)
+ location = find_light_arrows
+ location_hint = get_hint_area(location)
+ if world.player != location.player:
+ text += "\x05\x42%s's\x05\x40 %s" % (world.world.get_player_name(location.player), get_raw_text(location_hint))
+ else:
+ location_hint = location_hint.replace('Ganon\'s Castle', 'my castle')
+ text += get_raw_text(location_hint)
+ except StopIteration:
+ text = get_raw_text(getHint('Validation Line', world.clearer_hints).text)
+ for location in world.world.get_filled_locations(world.player):
+ if location.name == 'Ganons Tower Boss Key Chest':
+ text += get_raw_text(getHint(getItemGenericName(location.item), world.clearer_hints).text)
+ break
+ text += '!'
+
+ update_message_by_id(messages, 0x70CC, text)
+
+
+def get_raw_text(string):
+ text = ''
+ for char in string:
+ if char == '^':
+ text += '\x04' # box break
+ elif char == '&':
+ text += '\x01' # new line
+ elif char == '@':
+ text += '\x0F' # print player name
+ elif char == '#':
+ text += '\x05\x40' # sets color to white
+ else:
+ text += char
+ return text
+
+
+def HintDistFiles():
+ return [os.path.join(data_path('Hints/'), d) for d in defaultHintDists] + [
+ os.path.join(data_path('Hints/'), d)
+ for d in sorted(os.listdir(data_path('Hints/')))
+ if d.endswith('.json') and d not in defaultHintDists]
+
+
+def HintDistList():
+ dists = {}
+ for d in HintDistFiles():
+ dist = read_json(d)
+ dist_name = dist['name']
+ gui_name = dist['gui_name']
+ dists.update({ dist_name: gui_name })
+ return dists
+
+
+def HintDistTips():
+ tips = ""
+ first_dist = True
+ line_char_limit = 33
+ for d in HintDistFiles():
+ if not first_dist:
+ tips = tips + "\n"
+ else:
+ first_dist = False
+ dist = read_json(d)
+ gui_name = dist['gui_name']
+ desc = dist['description']
+ i = 0
+ end_of_line = False
+ tips = tips + ""
+ for c in gui_name:
+ if c == " " and end_of_line:
+ tips = tips + "\n"
+ end_of_line = False
+ else:
+ tips = tips + c
+ i = i + 1
+ if i > line_char_limit:
+ end_of_line = True
+ i = 0
+ tips = tips + ": "
+ i = i + 2
+ for c in desc:
+ if c == " " and end_of_line:
+ tips = tips + "\n"
+ end_of_line = False
+ else:
+ tips = tips + c
+ i = i + 1
+ if i > line_char_limit:
+ end_of_line = True
+ i = 0
+ tips = tips + "\n"
+ return tips
diff --git a/worlds/oot/IconManip.py b/worlds/oot/IconManip.py
new file mode 100644
index 00000000..5b573881
--- /dev/null
+++ b/worlds/oot/IconManip.py
@@ -0,0 +1,112 @@
+from .Utils import data_path
+
+# TODO
+# Move the tunic to the generalized system
+
+# Function for adding hue to a greyscaled icon
+def add_hue(image, color, tiff=False):
+ start = 154 if tiff else 0
+ for i in range(start, len(image), 4):
+ try:
+ for x in range(3):
+ image[i+x] = int(((image[i+x]/255) * (color[x]/255)) * 255)
+ except:
+ pass
+ return image
+
+
+# Function for adding belt to tunic
+def add_belt(tunic, belt, tiff=False):
+ start = 154 if tiff else 0
+ for i in range(start, len(tunic), 4):
+ try:
+ if belt[i+3] != 0:
+ alpha = belt[i+3] / 255
+ for x in range(3):
+ tunic[i+x] = int((belt[i+x] * alpha) + (tunic[i+x] * (1 - alpha)))
+ except:
+ pass
+ return tunic
+
+
+# Function for putting tunic colors together
+def generate_tunic_icon(color):
+ with open(data_path('icons/grey.tiff'), 'rb') as grey_fil, open(data_path('icons/belt.tiff'), 'rb') as belt_fil:
+ grey = list(grey_fil.read())
+ belt = list(belt_fil.read())
+ return add_belt(add_hue(grey, color, True), belt, True)[154:]
+
+# END TODO
+
+# Function to add extra data on top of icon
+def add_extra_data(rgbValues, fileName, intensity = 0.5):
+ fileRGB = []
+ with open(fileName, "rb") as fil:
+ data = fil.read()
+ for i in range(0, len(data), 4):
+ fileRGB.append([data[i+0], data[i+1], data[i+2], data[i+3]])
+ for i in range(len(rgbValues)):
+ alpha = fileRGB[i][3] / 255
+ for x in range(3):
+ rgbValues[i][x] = int((fileRGB[i][x] * alpha + intensity) + (rgbValues[i][x] * (1 - alpha - intensity)))
+
+# Function for desaturating RGB values
+def greyscaleRGB(rgbValues, intensity: int = 2):
+ for rgb in rgbValues:
+ rgb[0] = rgb[1] = rgb[2] = int((rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722) * intensity)
+ return rgbValues
+
+# Converts rgb5a1 values to RGBA lists
+def rgb5a1ToRGB(rgb5a1Bytes):
+ pixels = []
+ for i in range(0, len(rgb5a1Bytes), 2):
+ bits = format(rgb5a1Bytes[i], '#010b')[2:] + format(rgb5a1Bytes[i+1], '#010b')[2:]
+ r = int(int(bits[0:5], 2) * (255/31))
+ g = int(int(bits[5:10], 2) * (255/31))
+ b = int(int(bits[10:15], 2) * (255/31))
+ a = int(bits[15], 2) * 255
+ pixels.append([r,g,b,a])
+ return pixels
+
+# Adds a hue to RGB values
+def addHueToRGB(rgbValues, color):
+ for rgb in rgbValues:
+ for i in range(3):
+ rgb[i] = int(((rgb[i]/255) * (color[i]/255)) * 255)
+ return rgbValues
+
+# Convert RGB to RGB5a1 format
+def rgbToRGB5a1(rgbValues):
+ rgb5a1 = []
+ for rgb in rgbValues:
+ r = int(rgb[0] / (255/31))
+ r = r if r <= 31 else 31
+ r = r if r >= 0 else 0
+ g = int(rgb[1] / (255/31))
+ g = g if g <= 31 else 31
+ g = g if g >= 0 else 0
+ b = int(rgb[2] / (255/31))
+ b = b if b <= 31 else 31
+ b = b if b >= 0 else 0
+ a = int(rgb[3] / 255)
+ bits = format(r, '#07b')[2:] + format(g, '#07b')[2:] + format(b, '#07b')[2:] + format(a, '#03b')[2:]
+ rgb5a1.append(int(bits[:8], 2))
+ rgb5a1.append(int(bits[8:], 2))
+ for i in rgb5a1:
+ assert i <= 255, i
+ return bytes(rgb5a1)
+
+# Patch overworld icons
+def patch_overworld_icon(rom, color, address, fileName = None):
+ original = rom.original.read_bytes(address, 0x800)
+
+ if color is None:
+ rom.write_bytes(address, original)
+ return
+
+ rgbBytes = rgb5a1ToRGB(original)
+ greyscaled = greyscaleRGB(rgbBytes)
+ rgbBytes = addHueToRGB(greyscaled, color)
+ if fileName != None:
+ add_extra_data(rgbBytes, fileName)
+ rom.write_bytes(address, rgbToRGB5a1(rgbBytes))
diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py
new file mode 100644
index 00000000..96af9ad0
--- /dev/null
+++ b/worlds/oot/ItemPool.py
@@ -0,0 +1,1410 @@
+from collections import namedtuple
+from itertools import chain
+from .Items import item_table
+from .LocationList import location_groups
+from decimal import Decimal, ROUND_HALF_UP
+
+
+# Generates itempools and places fixed items based on settings.
+
+alwaysitems = ([
+ 'Biggoron Sword',
+ 'Boomerang',
+ 'Lens of Truth',
+ 'Megaton Hammer',
+ 'Iron Boots',
+ 'Goron Tunic',
+ 'Zora Tunic',
+ 'Hover Boots',
+ 'Mirror Shield',
+ 'Stone of Agony',
+ 'Fire Arrows',
+ 'Ice Arrows',
+ 'Light Arrows',
+ 'Dins Fire',
+ 'Farores Wind',
+ 'Nayrus Love',
+ 'Rupee (1)']
+ + ['Progressive Hookshot'] * 2
+ + ['Deku Shield']
+ + ['Hylian Shield']
+ + ['Progressive Strength Upgrade'] * 3
+ + ['Progressive Scale'] * 2
+ + ['Recovery Heart'] * 6
+ + ['Bow'] * 3
+ + ['Slingshot'] * 3
+ + ['Bomb Bag'] * 3
+ + ['Bombs (5)'] * 2
+ + ['Bombs (10)']
+ + ['Bombs (20)']
+ + ['Arrows (5)']
+ + ['Arrows (10)'] * 5
+ + ['Progressive Wallet'] * 2
+ + ['Magic Meter'] * 2
+ + ['Double Defense']
+ + ['Deku Stick Capacity'] * 2
+ + ['Deku Nut Capacity'] * 2
+ + ['Piece of Heart (Treasure Chest Game)'])
+
+
+easy_items = ([
+ 'Biggoron Sword',
+ 'Kokiri Sword',
+ 'Boomerang',
+ 'Lens of Truth',
+ 'Megaton Hammer',
+ 'Iron Boots',
+ 'Goron Tunic',
+ 'Zora Tunic',
+ 'Hover Boots',
+ 'Mirror Shield',
+ 'Fire Arrows',
+ 'Light Arrows',
+ 'Dins Fire',
+ 'Progressive Hookshot',
+ 'Progressive Strength Upgrade',
+ 'Progressive Scale',
+ 'Progressive Wallet',
+ 'Magic Meter',
+ 'Deku Stick Capacity',
+ 'Deku Nut Capacity',
+ 'Bow',
+ 'Slingshot',
+ 'Bomb Bag',
+ 'Double Defense'] +
+ ['Heart Container'] * 16 +
+ ['Piece of Heart'] * 3)
+
+normal_items = (
+ ['Heart Container'] * 8 +
+ ['Piece of Heart'] * 35)
+
+
+item_difficulty_max = {
+ 'plentiful': {},
+ 'balanced': {},
+ 'scarce': {
+ 'Bombchus': 3,
+ 'Bombchus (5)': 1,
+ 'Bombchus (10)': 2,
+ 'Bombchus (20)': 0,
+ 'Magic Meter': 1,
+ 'Double Defense': 0,
+ 'Deku Stick Capacity': 1,
+ 'Deku Nut Capacity': 1,
+ 'Bow': 2,
+ 'Slingshot': 2,
+ 'Bomb Bag': 2,
+ 'Heart Container': 0,
+ },
+ 'minimal': {
+ 'Bombchus': 1,
+ 'Bombchus (5)': 1,
+ 'Bombchus (10)': 0,
+ 'Bombchus (20)': 0,
+ 'Nayrus Love': 0,
+ 'Magic Meter': 1,
+ 'Double Defense': 0,
+ 'Deku Stick Capacity': 0,
+ 'Deku Nut Capacity': 0,
+ 'Bow': 1,
+ 'Slingshot': 1,
+ 'Bomb Bag': 1,
+ 'Heart Container': 0,
+ 'Piece of Heart': 0,
+ },
+}
+
+TriforceCounts = {
+ 'plentiful': Decimal(2.00),
+ 'balanced': Decimal(1.50),
+ 'scarce': Decimal(1.25),
+ 'minimal': Decimal(1.00),
+}
+
+DT_vanilla = (
+ ['Recovery Heart'] * 2)
+
+DT_MQ = (
+ ['Deku Shield'] * 2 +
+ ['Rupees (50)'])
+
+DC_vanilla = (
+ ['Rupees (20)'])
+
+DC_MQ = (
+ ['Hylian Shield'] +
+ ['Rupees (5)'])
+
+JB_MQ = (
+ ['Deku Nuts (5)'] * 4 +
+ ['Recovery Heart'] +
+ ['Deku Shield'] +
+ ['Deku Stick (1)'])
+
+FoT_vanilla = (
+ ['Recovery Heart'] +
+ ['Arrows (10)'] +
+ ['Arrows (30)'])
+
+FoT_MQ = (
+ ['Arrows (5)'])
+
+FiT_vanilla = (
+ ['Rupees (200)'])
+
+FiT_MQ = (
+ ['Bombs (20)'] +
+ ['Hylian Shield'])
+
+SpT_vanilla = (
+ ['Deku Shield'] * 2 +
+ ['Recovery Heart'] +
+ ['Bombs (20)'])
+
+SpT_MQ = (
+ ['Rupees (50)'] * 2 +
+ ['Arrows (30)'])
+
+ShT_vanilla = (
+ ['Arrows (30)'])
+
+ShT_MQ = (
+ ['Arrows (5)'] * 2 +
+ ['Rupees (20)'])
+
+BW_vanilla = (
+ ['Recovery Heart'] +
+ ['Bombs (10)'] +
+ ['Rupees (200)'] +
+ ['Deku Nuts (5)'] +
+ ['Deku Nuts (10)'] +
+ ['Deku Shield'] +
+ ['Hylian Shield'])
+
+GTG_vanilla = (
+ ['Arrows (30)'] * 3 +
+ ['Rupees (200)'])
+
+GTG_MQ = (
+ ['Rupee (Treasure Chest Game)'] * 2 +
+ ['Arrows (10)'] +
+ ['Rupee (1)'] +
+ ['Rupees (50)'])
+
+GC_vanilla = (
+ ['Rupees (5)'] * 3 +
+ ['Arrows (30)'])
+
+GC_MQ = (
+ ['Arrows (10)'] * 2 +
+ ['Bombs (5)'] +
+ ['Rupees (20)'] +
+ ['Recovery Heart'])
+
+
+normal_bottles = [
+ 'Bottle',
+ 'Bottle with Milk',
+ 'Bottle with Red Potion',
+ 'Bottle with Green Potion',
+ 'Bottle with Blue Potion',
+ 'Bottle with Fairy',
+ 'Bottle with Fish',
+ 'Bottle with Bugs',
+ 'Bottle with Poe',
+ 'Bottle with Big Poe',
+ 'Bottle with Blue Fire']
+
+bottle_count = 4
+
+
+dungeon_rewards = [
+ 'Kokiri Emerald',
+ 'Goron Ruby',
+ 'Zora Sapphire',
+ 'Forest Medallion',
+ 'Fire Medallion',
+ 'Water Medallion',
+ 'Shadow Medallion',
+ 'Spirit Medallion',
+ 'Light Medallion'
+]
+
+
+normal_rupees = (
+ ['Rupees (5)'] * 13 +
+ ['Rupees (20)'] * 5 +
+ ['Rupees (50)'] * 7 +
+ ['Rupees (200)'] * 3)
+
+shopsanity_rupees = (
+ ['Rupees (5)'] * 2 +
+ ['Rupees (20)'] * 10 +
+ ['Rupees (50)'] * 10 +
+ ['Rupees (200)'] * 5 +
+ ['Progressive Wallet'])
+
+
+vanilla_shop_items = {
+ 'KF Shop Item 1': 'Buy Deku Shield',
+ 'KF Shop Item 2': 'Buy Deku Nut (5)',
+ 'KF Shop Item 3': 'Buy Deku Nut (10)',
+ 'KF Shop Item 4': 'Buy Deku Stick (1)',
+ 'KF Shop Item 5': 'Buy Deku Seeds (30)',
+ 'KF Shop Item 6': 'Buy Arrows (10)',
+ 'KF Shop Item 7': 'Buy Arrows (30)',
+ 'KF Shop Item 8': 'Buy Heart',
+ 'Kak Potion Shop Item 1': 'Buy Deku Nut (5)',
+ 'Kak Potion Shop Item 2': 'Buy Fish',
+ 'Kak Potion Shop Item 3': 'Buy Red Potion [30]',
+ 'Kak Potion Shop Item 4': 'Buy Green Potion',
+ 'Kak Potion Shop Item 5': 'Buy Blue Fire',
+ 'Kak Potion Shop Item 6': 'Buy Bottle Bug',
+ 'Kak Potion Shop Item 7': 'Buy Poe',
+ 'Kak Potion Shop Item 8': 'Buy Fairy\'s Spirit',
+ 'Market Bombchu Shop Item 1': 'Buy Bombchu (5)',
+ 'Market Bombchu Shop Item 2': 'Buy Bombchu (10)',
+ 'Market Bombchu Shop Item 3': 'Buy Bombchu (10)',
+ 'Market Bombchu Shop Item 4': 'Buy Bombchu (10)',
+ 'Market Bombchu Shop Item 5': 'Buy Bombchu (20)',
+ 'Market Bombchu Shop Item 6': 'Buy Bombchu (20)',
+ 'Market Bombchu Shop Item 7': 'Buy Bombchu (20)',
+ 'Market Bombchu Shop Item 8': 'Buy Bombchu (20)',
+ 'Market Potion Shop Item 1': 'Buy Green Potion',
+ 'Market Potion Shop Item 2': 'Buy Blue Fire',
+ 'Market Potion Shop Item 3': 'Buy Red Potion [30]',
+ 'Market Potion Shop Item 4': 'Buy Fairy\'s Spirit',
+ 'Market Potion Shop Item 5': 'Buy Deku Nut (5)',
+ 'Market Potion Shop Item 6': 'Buy Bottle Bug',
+ 'Market Potion Shop Item 7': 'Buy Poe',
+ 'Market Potion Shop Item 8': 'Buy Fish',
+ 'Market Bazaar Item 1': 'Buy Hylian Shield',
+ 'Market Bazaar Item 2': 'Buy Bombs (5) [35]',
+ 'Market Bazaar Item 3': 'Buy Deku Nut (5)',
+ 'Market Bazaar Item 4': 'Buy Heart',
+ 'Market Bazaar Item 5': 'Buy Arrows (10)',
+ 'Market Bazaar Item 6': 'Buy Arrows (50)',
+ 'Market Bazaar Item 7': 'Buy Deku Stick (1)',
+ 'Market Bazaar Item 8': 'Buy Arrows (30)',
+ 'Kak Bazaar Item 1': 'Buy Hylian Shield',
+ 'Kak Bazaar Item 2': 'Buy Bombs (5) [35]',
+ 'Kak Bazaar Item 3': 'Buy Deku Nut (5)',
+ 'Kak Bazaar Item 4': 'Buy Heart',
+ 'Kak Bazaar Item 5': 'Buy Arrows (10)',
+ 'Kak Bazaar Item 6': 'Buy Arrows (50)',
+ 'Kak Bazaar Item 7': 'Buy Deku Stick (1)',
+ 'Kak Bazaar Item 8': 'Buy Arrows (30)',
+ 'ZD Shop Item 1': 'Buy Zora Tunic',
+ 'ZD Shop Item 2': 'Buy Arrows (10)',
+ 'ZD Shop Item 3': 'Buy Heart',
+ 'ZD Shop Item 4': 'Buy Arrows (30)',
+ 'ZD Shop Item 5': 'Buy Deku Nut (5)',
+ 'ZD Shop Item 6': 'Buy Arrows (50)',
+ 'ZD Shop Item 7': 'Buy Fish',
+ 'ZD Shop Item 8': 'Buy Red Potion [50]',
+ 'GC Shop Item 1': 'Buy Bombs (5) [25]',
+ 'GC Shop Item 2': 'Buy Bombs (10)',
+ 'GC Shop Item 3': 'Buy Bombs (20)',
+ 'GC Shop Item 4': 'Buy Bombs (30)',
+ 'GC Shop Item 5': 'Buy Goron Tunic',
+ 'GC Shop Item 6': 'Buy Heart',
+ 'GC Shop Item 7': 'Buy Red Potion [40]',
+ 'GC Shop Item 8': 'Buy Heart',
+}
+
+
+min_shop_items = (
+ ['Buy Deku Shield'] +
+ ['Buy Hylian Shield'] +
+ ['Buy Goron Tunic'] +
+ ['Buy Zora Tunic'] +
+ ['Buy Deku Nut (5)'] * 2 + ['Buy Deku Nut (10)'] +
+ ['Buy Deku Stick (1)'] * 2 +
+ ['Buy Deku Seeds (30)'] +
+ ['Buy Arrows (10)'] * 2 + ['Buy Arrows (30)'] + ['Buy Arrows (50)'] +
+ ['Buy Bombchu (5)'] + ['Buy Bombchu (10)'] * 2 + ['Buy Bombchu (20)'] +
+ ['Buy Bombs (5) [25]'] + ['Buy Bombs (5) [35]'] + ['Buy Bombs (10)'] + ['Buy Bombs (20)'] +
+ ['Buy Green Potion'] +
+ ['Buy Red Potion [30]'] +
+ ['Buy Blue Fire'] +
+ ['Buy Fairy\'s Spirit'] +
+ ['Buy Bottle Bug'] +
+ ['Buy Fish'])
+
+
+vanilla_deku_scrubs = {
+ 'ZR Deku Scrub Grotto Rear': 'Buy Red Potion [30]',
+ 'ZR Deku Scrub Grotto Front': 'Buy Green Potion',
+ 'SFM Deku Scrub Grotto Rear': 'Buy Red Potion [30]',
+ 'SFM Deku Scrub Grotto Front': 'Buy Green Potion',
+ 'LH Deku Scrub Grotto Left': 'Buy Deku Nut (5)',
+ 'LH Deku Scrub Grotto Right': 'Buy Bombs (5) [35]',
+ 'LH Deku Scrub Grotto Center': 'Buy Arrows (30)',
+ 'GV Deku Scrub Grotto Rear': 'Buy Red Potion [30]',
+ 'GV Deku Scrub Grotto Front': 'Buy Green Potion',
+ 'LW Deku Scrub Near Deku Theater Right': 'Buy Deku Nut (5)',
+ 'LW Deku Scrub Near Deku Theater Left': 'Buy Deku Stick (1)',
+ 'LW Deku Scrub Grotto Rear': 'Buy Arrows (30)',
+ 'Colossus Deku Scrub Grotto Rear': 'Buy Red Potion [30]',
+ 'Colossus Deku Scrub Grotto Front': 'Buy Green Potion',
+ 'DMC Deku Scrub': 'Buy Bombs (5) [35]',
+ 'DMC Deku Scrub Grotto Left': 'Buy Deku Nut (5)',
+ 'DMC Deku Scrub Grotto Right': 'Buy Bombs (5) [35]',
+ 'DMC Deku Scrub Grotto Center': 'Buy Arrows (30)',
+ 'GC Deku Scrub Grotto Left': 'Buy Deku Nut (5)',
+ 'GC Deku Scrub Grotto Right': 'Buy Bombs (5) [35]',
+ 'GC Deku Scrub Grotto Center': 'Buy Arrows (30)',
+ 'LLR Deku Scrub Grotto Left': 'Buy Deku Nut (5)',
+ 'LLR Deku Scrub Grotto Right': 'Buy Bombs (5) [35]',
+ 'LLR Deku Scrub Grotto Center': 'Buy Arrows (30)',
+}
+
+
+deku_scrubs_items = (
+ ['Deku Nuts (5)'] * 5 +
+ ['Deku Stick (1)'] +
+ ['Bombs (5)'] * 5 +
+ ['Recovery Heart'] * 4 +
+ ['Rupees (5)'] * 4) # ['Green Potion']
+
+
+songlist = [
+ 'Zeldas Lullaby',
+ 'Eponas Song',
+ 'Suns Song',
+ 'Sarias Song',
+ 'Song of Time',
+ 'Song of Storms',
+ 'Minuet of Forest',
+ 'Prelude of Light',
+ 'Bolero of Fire',
+ 'Serenade of Water',
+ 'Nocturne of Shadow',
+ 'Requiem of Spirit']
+
+
+skulltula_locations = ([
+ 'KF GS Know It All House',
+ 'KF GS Bean Patch',
+ 'KF GS House of Twins',
+ 'LW GS Bean Patch Near Bridge',
+ 'LW GS Bean Patch Near Theater',
+ 'LW GS Above Theater',
+ 'SFM GS',
+ 'HF GS Near Kak Grotto',
+ 'HF GS Cow Grotto',
+ 'Market GS Guard House',
+ 'HC GS Tree',
+ 'HC GS Storms Grotto',
+ 'OGC GS',
+ 'LLR GS Tree',
+ 'LLR GS Rain Shed',
+ 'LLR GS House Window',
+ 'LLR GS Back Wall',
+ 'Kak GS House Under Construction',
+ 'Kak GS Skulltula House',
+ 'Kak GS Guards House',
+ 'Kak GS Tree',
+ 'Kak GS Watchtower',
+ 'Kak GS Above Impas House',
+ 'Graveyard GS Wall',
+ 'Graveyard GS Bean Patch',
+ 'DMT GS Bean Patch',
+ 'DMT GS Near Kak',
+ 'DMT GS Falling Rocks Path',
+ 'DMT GS Above Dodongos Cavern',
+ 'GC GS Boulder Maze',
+ 'GC GS Center Platform',
+ 'DMC GS Crate',
+ 'DMC GS Bean Patch',
+ 'ZR GS Ladder',
+ 'ZR GS Tree',
+ 'ZR GS Near Raised Grottos',
+ 'ZR GS Above Bridge',
+ 'ZD GS Frozen Waterfall',
+ 'ZF GS Tree',
+ 'ZF GS Above the Log',
+ 'ZF GS Hidden Cave',
+ 'LH GS Bean Patch',
+ 'LH GS Lab Wall',
+ 'LH GS Small Island',
+ 'LH GS Tree',
+ 'LH GS Lab Crate',
+ 'GV GS Small Bridge',
+ 'GV GS Bean Patch',
+ 'GV GS Behind Tent',
+ 'GV GS Pillar',
+ 'GF GS Archery Range',
+ 'GF GS Top Floor',
+ 'Wasteland GS',
+ 'Colossus GS Bean Patch',
+ 'Colossus GS Tree',
+ 'Colossus GS Hill'])
+
+
+tradeitems = (
+ 'Pocket Egg',
+ 'Pocket Cucco',
+ 'Cojiro',
+ 'Odd Mushroom',
+ 'Poachers Saw',
+ 'Broken Sword',
+ 'Prescription',
+ 'Eyeball Frog',
+ 'Eyedrops',
+ 'Claim Check')
+
+tradeitemoptions = (
+ 'pocket_egg',
+ 'pocket_cucco',
+ 'cojiro',
+ 'odd_mushroom',
+ 'poachers_saw',
+ 'broken_sword',
+ 'prescription',
+ 'eyeball_frog',
+ 'eyedrops',
+ 'claim_check')
+
+
+fixedlocations = {
+ 'Ganon': 'Triforce',
+ 'Pierre': 'Scarecrow Song',
+ 'Deliver Rutos Letter': 'Deliver Letter',
+ 'Master Sword Pedestal': 'Time Travel',
+ 'Market Bombchu Bowling Bombchus': 'Bombchu Drop',
+}
+
+droplocations = {
+ 'Deku Baba Sticks': 'Deku Stick Drop',
+ 'Deku Baba Nuts': 'Deku Nut Drop',
+ 'Stick Pot': 'Deku Stick Drop',
+ 'Nut Pot': 'Deku Nut Drop',
+ 'Nut Crate': 'Deku Nut Drop',
+ 'Blue Fire': 'Blue Fire',
+ 'Lone Fish': 'Fish',
+ 'Fish Group': 'Fish',
+ 'Bug Rock': 'Bugs',
+ 'Bug Shrub': 'Bugs',
+ 'Wandering Bugs': 'Bugs',
+ 'Fairy Pot': 'Fairy',
+ 'Free Fairies': 'Fairy',
+ 'Wall Fairy': 'Fairy',
+ 'Butterfly Fairy': 'Fairy',
+ 'Gossip Stone Fairy': 'Fairy',
+ 'Bean Plant Fairy': 'Fairy',
+ 'Fairy Pond': 'Fairy',
+ 'Big Poe Kill': 'Big Poe',
+}
+
+vanillaBK = {
+ 'Fire Temple Boss Key Chest': 'Boss Key (Fire Temple)',
+ 'Shadow Temple Boss Key Chest': 'Boss Key (Shadow Temple)',
+ 'Spirit Temple Boss Key Chest': 'Boss Key (Spirit Temple)',
+ 'Water Temple Boss Key Chest': 'Boss Key (Water Temple)',
+ 'Forest Temple Boss Key Chest': 'Boss Key (Forest Temple)',
+
+ 'Fire Temple MQ Boss Key Chest': 'Boss Key (Fire Temple)',
+ 'Shadow Temple MQ Boss Key Chest': 'Boss Key (Shadow Temple)',
+ 'Spirit Temple MQ Boss Key Chest': 'Boss Key (Spirit Temple)',
+ 'Water Temple MQ Boss Key Chest': 'Boss Key (Water Temple)',
+ 'Forest Temple MQ Boss Key Chest': 'Boss Key (Forest Temple)',
+}
+
+vanillaMC = {
+ 'Bottom of the Well Compass Chest': 'Compass (Bottom of the Well)',
+ 'Deku Tree Compass Chest': 'Compass (Deku Tree)',
+ 'Dodongos Cavern Compass Chest': 'Compass (Dodongos Cavern)',
+ 'Fire Temple Compass Chest': 'Compass (Fire Temple)',
+ 'Forest Temple Blue Poe Chest': 'Compass (Forest Temple)',
+ 'Ice Cavern Compass Chest': 'Compass (Ice Cavern)',
+ 'Jabu Jabus Belly Compass Chest': 'Compass (Jabu Jabus Belly)',
+ 'Shadow Temple Compass Chest': 'Compass (Shadow Temple)',
+ 'Spirit Temple Compass Chest': 'Compass (Spirit Temple)',
+ 'Water Temple Compass Chest': 'Compass (Water Temple)',
+
+ 'Bottom of the Well Map Chest': 'Map (Bottom of the Well)',
+ 'Deku Tree Map Chest': 'Map (Deku Tree)',
+ 'Dodongos Cavern Map Chest': 'Map (Dodongos Cavern)',
+ 'Fire Temple Map Chest': 'Map (Fire Temple)',
+ 'Forest Temple Map Chest': 'Map (Forest Temple)',
+ 'Ice Cavern Map Chest': 'Map (Ice Cavern)',
+ 'Jabu Jabus Belly Map Chest': 'Map (Jabu Jabus Belly)',
+ 'Shadow Temple Map Chest': 'Map (Shadow Temple)',
+ 'Spirit Temple Map Chest': 'Map (Spirit Temple)',
+ 'Water Temple Map Chest': 'Map (Water Temple)',
+
+ 'Bottom of the Well MQ Compass Chest': 'Compass (Bottom of the Well)',
+ 'Deku Tree MQ Compass Chest': 'Compass (Deku Tree)',
+ 'Dodongos Cavern MQ Compass Chest': 'Compass (Dodongos Cavern)',
+ 'Fire Temple MQ Compass Chest': 'Compass (Fire Temple)',
+ 'Forest Temple MQ Compass Chest': 'Compass (Forest Temple)',
+ 'Ice Cavern MQ Compass Chest': 'Compass (Ice Cavern)',
+ 'Jabu Jabus Belly MQ Compass Chest': 'Compass (Jabu Jabus Belly)',
+ 'Shadow Temple MQ Compass Chest': 'Compass (Shadow Temple)',
+ 'Spirit Temple MQ Compass Chest': 'Compass (Spirit Temple)',
+ 'Water Temple MQ Compass Chest': 'Compass (Water Temple)',
+
+ 'Bottom of the Well MQ Map Chest': 'Map (Bottom of the Well)',
+ 'Deku Tree MQ Map Chest': 'Map (Deku Tree)',
+ 'Dodongos Cavern MQ Map Chest': 'Map (Dodongos Cavern)',
+ 'Fire Temple MQ Map Chest': 'Map (Fire Temple)',
+ 'Forest Temple MQ Map Chest': 'Map (Forest Temple)',
+ 'Ice Cavern MQ Map Chest': 'Map (Ice Cavern)',
+ 'Jabu Jabus Belly MQ Map Chest': 'Map (Jabu Jabus Belly)',
+ 'Shadow Temple MQ Map Chest': 'Map (Shadow Temple)',
+ 'Spirit Temple MQ Map Chest': 'Map (Spirit Temple)',
+ 'Water Temple MQ Map Chest': 'Map (Water Temple)',
+}
+
+vanillaSK = {
+ 'Bottom of the Well Front Left Fake Wall Chest': 'Small Key (Bottom of the Well)',
+ 'Bottom of the Well Right Bottom Fake Wall Chest': 'Small Key (Bottom of the Well)',
+ 'Bottom of the Well Freestanding Key': 'Small Key (Bottom of the Well)',
+ 'Fire Temple Big Lava Room Blocked Door Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Big Lava Room Lower Open Door Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Boulder Maze Shortcut Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Boulder Maze Lower Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Boulder Maze Side Room Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Boulder Maze Upper Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Near Boss Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple Highest Goron Chest': 'Small Key (Fire Temple)',
+ 'Forest Temple First Stalfos Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple First Room Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple Floormaster Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple Red Poe Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple Well Chest': 'Small Key (Forest Temple)',
+ 'Ganons Castle Light Trial Invisible Enemies Chest': 'Small Key (Ganons Castle)',
+ 'Ganons Castle Light Trial Lullaby Chest': 'Small Key (Ganons Castle)',
+ 'Gerudo Training Grounds Beamos Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Eye Statue Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Hammer Room Switch Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Heavy Block Third Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Hidden Ceiling Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Near Scarecrow Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Stalfos Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Underwater Silver Rupee Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds Freestanding Key': 'Small Key (Gerudo Training Grounds)',
+ 'Shadow Temple After Wind Hidden Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple Early Silver Rupee Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple Falling Spikes Switch Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple Invisible Floormaster Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple Freestanding Key': 'Small Key (Shadow Temple)',
+ 'Spirit Temple Child Early Torches Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple Early Adult Right Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple Near Four Armos Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple Statue Room Hand Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple Sun Block Room Chest': 'Small Key (Spirit Temple)',
+ 'Water Temple Central Bow Target Chest': 'Small Key (Water Temple)',
+ 'Water Temple Central Pillar Chest': 'Small Key (Water Temple)',
+ 'Water Temple Cracked Wall Chest': 'Small Key (Water Temple)',
+ 'Water Temple Dragon Chest': 'Small Key (Water Temple)',
+ 'Water Temple River Chest': 'Small Key (Water Temple)',
+ 'Water Temple Torches Chest': 'Small Key (Water Temple)',
+
+ 'Bottom of the Well MQ Dead Hand Freestanding Key': 'Small Key (Bottom of the Well)',
+ 'Bottom of the Well MQ East Inner Room Freestanding Key': 'Small Key (Bottom of the Well)',
+ 'Fire Temple MQ Big Lava Room Blocked Door Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple MQ Near Boss Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple MQ Lizalfos Maze Side Room Chest': 'Small Key (Fire Temple)',
+ 'Fire Temple MQ Chest On Fire': 'Small Key (Fire Temple)',
+ 'Fire Temple MQ Freestanding Key': 'Small Key (Fire Temple)',
+ 'Forest Temple MQ Wolfos Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple MQ First Room Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple MQ Raised Island Courtyard Lower Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple MQ Raised Island Courtyard Upper Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple MQ Redead Chest': 'Small Key (Forest Temple)',
+ 'Forest Temple MQ Well Chest': 'Small Key (Forest Temple)',
+ 'Ganons Castle MQ Shadow Trial Eye Switch Chest': 'Small Key (Ganons Castle)',
+ 'Ganons Castle MQ Spirit Trial Sun Back Left Chest': 'Small Key (Ganons Castle)',
+ 'Ganons Castle MQ Forest Trial Freestanding Key': 'Small Key (Ganons Castle)',
+ 'Gerudo Training Grounds MQ Dinolfos Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds MQ Flame Circle Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Gerudo Training Grounds MQ Underwater Silver Rupee Chest': 'Small Key (Gerudo Training Grounds)',
+ 'Shadow Temple MQ Falling Spikes Switch Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple MQ Invisible Blades Invisible Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple MQ Early Gibdos Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple MQ Near Ship Invisible Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple MQ Wind Hint Chest': 'Small Key (Shadow Temple)',
+ 'Shadow Temple MQ Freestanding Key': 'Small Key (Shadow Temple)',
+ 'Spirit Temple MQ Child Hammer Switch Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Child Climb South Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Map Room Enemy Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Entrance Back Left Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Entrance Front Right Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Mirror Puzzle Invisible Chest': 'Small Key (Spirit Temple)',
+ 'Spirit Temple MQ Silver Block Hallway Chest': 'Small Key (Spirit Temple)',
+ 'Water Temple MQ Central Pillar Chest': 'Small Key (Water Temple)',
+ 'Water Temple MQ Freestanding Key': 'Small Key (Water Temple)',
+}
+
+junk_pool_base = [
+ ('Bombs (5)', 8),
+ ('Bombs (10)', 2),
+ ('Arrows (5)', 8),
+ ('Arrows (10)', 2),
+ ('Deku Stick (1)', 5),
+ ('Deku Nuts (5)', 5),
+ ('Deku Seeds (30)', 5),
+ ('Rupees (5)', 10),
+ ('Rupees (20)', 4),
+ ('Rupees (50)', 1),
+]
+
+pending_junk_pool = []
+junk_pool = []
+
+
+remove_junk_items = [
+ 'Bombs (5)',
+ 'Deku Nuts (5)',
+ 'Deku Stick (1)',
+ 'Recovery Heart',
+ 'Arrows (5)',
+ 'Arrows (10)',
+ 'Arrows (30)',
+ 'Rupees (5)',
+ 'Rupees (20)',
+ 'Rupees (50)',
+ 'Rupees (200)',
+ 'Deku Nuts (10)',
+ 'Bombs (10)',
+ 'Bombs (20)',
+ 'Deku Seeds (30)',
+ 'Ice Trap',
+]
+remove_junk_set = set(remove_junk_items)
+
+exclude_from_major = [
+ 'Deliver Letter',
+ 'Sell Big Poe',
+ 'Magic Bean',
+ 'Zeldas Letter',
+ 'Bombchus (5)',
+ 'Bombchus (10)',
+ 'Bombchus (20)',
+ 'Odd Potion',
+ 'Triforce Piece'
+]
+
+item_groups = {
+ 'Junk': remove_junk_items,
+ 'JunkSong': ('Prelude of Light', 'Serenade of Water'),
+ 'AdultTrade': tradeitems,
+ 'Bottle': normal_bottles,
+ 'Spell': ('Dins Fire', 'Farores Wind', 'Nayrus Love'),
+ 'Shield': ('Deku Shield', 'Hylian Shield'),
+ 'Song': songlist,
+ 'NonWarpSong': songlist[0:6],
+ 'WarpSong': songlist[6:],
+ 'HealthUpgrade': ('Heart Container', 'Piece of Heart'),
+ 'ProgressItem': [name for (name, data) in item_table.items() if data[0] == 'Item' and data[1]],
+ 'MajorItem': [name for (name, data) in item_table.items() if (data[0] == 'Item' or data[0] == 'Song') and data[1] and name not in exclude_from_major],
+ 'DungeonReward': dungeon_rewards,
+
+ 'ForestFireWater': ('Forest Medallion', 'Fire Medallion', 'Water Medallion'),
+ 'FireWater': ('Fire Medallion', 'Water Medallion'),
+}
+
+random = None
+
+
+def get_junk_pool(ootworld):
+ junk_pool[:] = list(junk_pool_base)
+ if ootworld.junk_ice_traps == 'on':
+ junk_pool.append(('Ice Trap', 10))
+ elif ootworld.junk_ice_traps in ['mayhem', 'onslaught']:
+ junk_pool[:] = [('Ice Trap', 1)]
+ return junk_pool
+
+
+def get_junk_item(count=1, pool=None, plando_pool=None):
+ global random
+
+ if count < 1:
+ raise ValueError("get_junk_item argument 'count' must be greater than 0.")
+
+ return_pool = []
+ if pending_junk_pool:
+ pending_count = min(len(pending_junk_pool), count)
+ return_pool = [pending_junk_pool.pop() for _ in range(pending_count)]
+ count -= pending_count
+
+ if pool and plando_pool:
+ jw_list = [(junk, weight) for (junk, weight) in junk_pool
+ if junk not in plando_pool or pool.count(junk) < plando_pool[junk].count]
+ try:
+ junk_items, junk_weights = zip(*jw_list)
+ except ValueError:
+ raise RuntimeError("Not enough junk is available in the item pool to replace removed items.")
+ else:
+ junk_items, junk_weights = zip(*junk_pool)
+ return_pool.extend(random.choices(junk_items, weights=junk_weights, k=count))
+
+ return return_pool
+
+
+def replace_max_item(items, item, max):
+ count = 0
+ for i,val in enumerate(items):
+ if val == item:
+ if count >= max:
+ items[i] = get_junk_item()[0]
+ count += 1
+
+
+def generate_itempool(ootworld):
+ world = ootworld.world
+ player = ootworld.player
+ global random
+ random = world.random
+
+ junk_pool = get_junk_pool(ootworld)
+
+ fixed_locations = list(filter(lambda loc: loc.name in fixedlocations, ootworld.get_locations()))
+ for location in fixed_locations:
+ item = fixedlocations[location.name]
+ world.push_item(location, ootworld.create_item(item), collect=False)
+ location.locked = True
+
+ drop_locations = list(filter(lambda loc: loc.type == 'Drop', ootworld.get_locations()))
+ for drop_location in drop_locations:
+ item = droplocations[drop_location.name]
+ world.push_item(drop_location, ootworld.create_item(item), collect=False)
+ drop_location.locked = True
+
+ # set up item pool
+ (pool, placed_items, skip_in_spoiler_locations) = get_pool_core(ootworld)
+ ootworld.itempool = [ootworld.create_item(item) for item in pool]
+ for (location_name, item) in placed_items.items():
+ location = world.get_location(location_name, player)
+ world.push_item(location, ootworld.create_item(item), collect=False)
+ location.locked = True
+ location.event = True # make sure it's checked during fill
+ if location_name in skip_in_spoiler_locations:
+ location.show_in_spoiler = False
+
+
+
+# def try_collect_heart_container(world, pool):
+# if 'Heart Container' in pool:
+# pool.remove('Heart Container')
+# pool.extend(get_junk_item())
+# world.state.collect(ItemFactory('Heart Container'))
+# return True
+# return False
+
+
+# def try_collect_pieces_of_heart(world, pool):
+# n = pool.count('Piece of Heart') + pool.count('Piece of Heart (Treasure Chest Game)')
+# if n >= 4:
+# for i in range(4):
+# if 'Piece of Heart' in pool:
+# pool.remove('Piece of Heart')
+# world.state.collect(ItemFactory('Piece of Heart'))
+# else:
+# pool.remove('Piece of Heart (Treasure Chest Game)')
+# world.state.collect(ItemFactory('Piece of Heart (Treasure Chest Game)'))
+# pool.extend(get_junk_item())
+# return True
+# return False
+
+
+# def collect_pieces_of_heart(world, pool):
+# success = try_collect_pieces_of_heart(world, pool)
+# if not success:
+# try_collect_heart_container(world, pool)
+
+
+# def collect_heart_container(world, pool):
+# success = try_collect_heart_container(world, pool)
+# if not success:
+# try_collect_pieces_of_heart(world, pool)
+
+
+def get_pool_core(world):
+ global random
+
+ pool = []
+ placed_items = {
+ 'HC Zeldas Letter': 'Zeldas Letter',
+ }
+ skip_in_spoiler_locations = []
+
+ if world.shuffle_kokiri_sword:
+ pool.append('Kokiri Sword')
+ else:
+ placed_items['KF Kokiri Sword Chest'] = 'Kokiri Sword'
+
+ ruto_bottles = 1
+ if world.zora_fountain == 'open':
+ ruto_bottles = 0
+ elif world.item_pool_value == 'plentiful':
+ ruto_bottles += 1
+
+ if world.skip_child_zelda:
+ placed_items['HC Malon Egg'] = 'Recovery Heart'
+ skip_in_spoiler_locations.append('HC Malon Egg')
+ elif world.shuffle_weird_egg:
+ pool.append('Weird Egg')
+ else:
+ placed_items['HC Malon Egg'] = 'Weird Egg'
+
+ if world.shuffle_ocarinas:
+ pool.extend(['Ocarina'] * 2)
+ if world.item_pool_value == 'plentiful':
+ pending_junk_pool.append('Ocarina')
+ else:
+ placed_items['LW Gift from Saria'] = 'Ocarina'
+ placed_items['HF Ocarina of Time Item'] = 'Ocarina'
+
+ if world.shuffle_cows:
+ pool.extend(get_junk_item(10 if world.dungeon_mq['Jabu Jabus Belly'] else 9))
+ else:
+ cow_locations = ['LLR Stables Left Cow', 'LLR Stables Right Cow', 'LLR Tower Left Cow', 'LLR Tower Right Cow',
+ 'KF Links House Cow', 'Kak Impas House Cow', 'GV Cow', 'DMT Cow Grotto Cow', 'HF Cow Grotto Cow']
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ cow_locations.append('Jabu Jabus Belly MQ Cow')
+ for loc in cow_locations:
+ placed_items[loc] = 'Milk'
+ skip_in_spoiler_locations.append(loc)
+
+ if world.shuffle_beans:
+ pool.append('Magic Bean Pack')
+ if world.item_pool_value == 'plentiful':
+ pending_junk_pool.append('Magic Bean Pack')
+ else:
+ placed_items['ZR Magic Bean Salesman'] = 'Magic Bean'
+ skip_in_spoiler_locations.append('ZR Magic Bean Salesman')
+
+ if world.shuffle_medigoron_carpet_salesman:
+ pool.append('Giants Knife')
+ else:
+ placed_items['GC Medigoron'] = 'Giants Knife'
+ skip_in_spoiler_locations.append('GC Medigoron')
+
+ if world.dungeon_mq['Deku Tree']:
+ skulltula_locations_final = skulltula_locations + [
+ 'Deku Tree MQ GS Lobby',
+ 'Deku Tree MQ GS Compass Room',
+ 'Deku Tree MQ GS Basement Graves Room',
+ 'Deku Tree MQ GS Basement Back Room']
+ else:
+ skulltula_locations_final = skulltula_locations + [
+ 'Deku Tree GS Compass Room',
+ 'Deku Tree GS Basement Vines',
+ 'Deku Tree GS Basement Gate',
+ 'Deku Tree GS Basement Back Room']
+ if world.dungeon_mq['Dodongos Cavern']:
+ skulltula_locations_final.extend([
+ 'Dodongos Cavern MQ GS Scrub Room',
+ 'Dodongos Cavern MQ GS Song of Time Block Room',
+ 'Dodongos Cavern MQ GS Lizalfos Room',
+ 'Dodongos Cavern MQ GS Larvae Room',
+ 'Dodongos Cavern MQ GS Back Area'])
+ else:
+ skulltula_locations_final.extend([
+ 'Dodongos Cavern GS Side Room Near Lower Lizalfos',
+ 'Dodongos Cavern GS Vines Above Stairs',
+ 'Dodongos Cavern GS Back Room',
+ 'Dodongos Cavern GS Alcove Above Stairs',
+ 'Dodongos Cavern GS Scarecrow'])
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ skulltula_locations_final.extend([
+ 'Jabu Jabus Belly MQ GS Tailpasaran Room',
+ 'Jabu Jabus Belly MQ GS Invisible Enemies Room',
+ 'Jabu Jabus Belly MQ GS Boomerang Chest Room',
+ 'Jabu Jabus Belly MQ GS Near Boss'])
+ else:
+ skulltula_locations_final.extend([
+ 'Jabu Jabus Belly GS Water Switch Room',
+ 'Jabu Jabus Belly GS Lobby Basement Lower',
+ 'Jabu Jabus Belly GS Lobby Basement Upper',
+ 'Jabu Jabus Belly GS Near Boss'])
+ if world.dungeon_mq['Forest Temple']:
+ skulltula_locations_final.extend([
+ 'Forest Temple MQ GS First Hallway',
+ 'Forest Temple MQ GS Block Push Room',
+ 'Forest Temple MQ GS Raised Island Courtyard',
+ 'Forest Temple MQ GS Level Island Courtyard',
+ 'Forest Temple MQ GS Well'])
+ else:
+ skulltula_locations_final.extend([
+ 'Forest Temple GS First Room',
+ 'Forest Temple GS Lobby',
+ 'Forest Temple GS Raised Island Courtyard',
+ 'Forest Temple GS Level Island Courtyard',
+ 'Forest Temple GS Basement'])
+ if world.dungeon_mq['Fire Temple']:
+ skulltula_locations_final.extend([
+ 'Fire Temple MQ GS Above Fire Wall Maze',
+ 'Fire Temple MQ GS Fire Wall Maze Center',
+ 'Fire Temple MQ GS Big Lava Room Open Door',
+ 'Fire Temple MQ GS Fire Wall Maze Side Room',
+ 'Fire Temple MQ GS Skull On Fire'])
+ else:
+ skulltula_locations_final.extend([
+ 'Fire Temple GS Song of Time Room',
+ 'Fire Temple GS Boulder Maze',
+ 'Fire Temple GS Scarecrow Climb',
+ 'Fire Temple GS Scarecrow Top',
+ 'Fire Temple GS Boss Key Loop'])
+ if world.dungeon_mq['Water Temple']:
+ skulltula_locations_final.extend([
+ 'Water Temple MQ GS Before Upper Water Switch',
+ 'Water Temple MQ GS Freestanding Key Area',
+ 'Water Temple MQ GS Lizalfos Hallway',
+ 'Water Temple MQ GS River',
+ 'Water Temple MQ GS Triple Wall Torch'])
+ else:
+ skulltula_locations_final.extend([
+ 'Water Temple GS Behind Gate',
+ 'Water Temple GS River',
+ 'Water Temple GS Falling Platform Room',
+ 'Water Temple GS Central Pillar',
+ 'Water Temple GS Near Boss Key Chest'])
+ if world.dungeon_mq['Spirit Temple']:
+ skulltula_locations_final.extend([
+ 'Spirit Temple MQ GS Symphony Room',
+ 'Spirit Temple MQ GS Leever Room',
+ 'Spirit Temple MQ GS Nine Thrones Room West',
+ 'Spirit Temple MQ GS Nine Thrones Room North',
+ 'Spirit Temple MQ GS Sun Block Room'])
+ else:
+ skulltula_locations_final.extend([
+ 'Spirit Temple GS Metal Fence',
+ 'Spirit Temple GS Sun on Floor Room',
+ 'Spirit Temple GS Hall After Sun Block Room',
+ 'Spirit Temple GS Boulder Room',
+ 'Spirit Temple GS Lobby'])
+ if world.dungeon_mq['Shadow Temple']:
+ skulltula_locations_final.extend([
+ 'Shadow Temple MQ GS Falling Spikes Room',
+ 'Shadow Temple MQ GS Wind Hint Room',
+ 'Shadow Temple MQ GS After Wind',
+ 'Shadow Temple MQ GS After Ship',
+ 'Shadow Temple MQ GS Near Boss'])
+ else:
+ skulltula_locations_final.extend([
+ 'Shadow Temple GS Like Like Room',
+ 'Shadow Temple GS Falling Spikes Room',
+ 'Shadow Temple GS Single Giant Pot',
+ 'Shadow Temple GS Near Ship',
+ 'Shadow Temple GS Triple Giant Pot'])
+ if world.dungeon_mq['Bottom of the Well']:
+ skulltula_locations_final.extend([
+ 'Bottom of the Well MQ GS Basement',
+ 'Bottom of the Well MQ GS Coffin Room',
+ 'Bottom of the Well MQ GS West Inner Room'])
+ else:
+ skulltula_locations_final.extend([
+ 'Bottom of the Well GS West Inner Room',
+ 'Bottom of the Well GS East Inner Room',
+ 'Bottom of the Well GS Like Like Cage'])
+ if world.dungeon_mq['Ice Cavern']:
+ skulltula_locations_final.extend([
+ 'Ice Cavern MQ GS Scarecrow',
+ 'Ice Cavern MQ GS Ice Block',
+ 'Ice Cavern MQ GS Red Ice'])
+ else:
+ skulltula_locations_final.extend([
+ 'Ice Cavern GS Spinning Scythe Room',
+ 'Ice Cavern GS Heart Piece Room',
+ 'Ice Cavern GS Push Block Room'])
+ if world.tokensanity == 'off':
+ for location in skulltula_locations_final:
+ placed_items[location] = 'Gold Skulltula Token'
+ skip_in_spoiler_locations.append(location)
+ elif world.tokensanity == 'dungeons':
+ for location in skulltula_locations_final:
+ if world.get_location(location).scene >= 0x0A:
+ placed_items[location] = 'Gold Skulltula Token'
+ skip_in_spoiler_locations.append(location)
+ else:
+ pool.append('Gold Skulltula Token')
+ elif world.tokensanity == 'overworld':
+ for location in skulltula_locations_final:
+ if world.get_location(location).scene < 0x0A:
+ placed_items[location] = 'Gold Skulltula Token'
+ skip_in_spoiler_locations.append(location)
+ else:
+ pool.append('Gold Skulltula Token')
+ else:
+ pool.extend(['Gold Skulltula Token'] * 100)
+
+
+ if world.bombchus_in_logic:
+ pool.extend(['Bombchus'] * 4)
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ pool.extend(['Bombchus'])
+ if world.dungeon_mq['Spirit Temple']:
+ pool.extend(['Bombchus'] * 2)
+ if not world.dungeon_mq['Bottom of the Well']:
+ pool.extend(['Bombchus'])
+ if world.dungeon_mq['Gerudo Training Grounds']:
+ pool.extend(['Bombchus'])
+ if world.shuffle_medigoron_carpet_salesman:
+ pool.append('Bombchus')
+
+ else:
+ pool.extend(['Bombchus (5)'] + ['Bombchus (10)'] * 2)
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ pool.extend(['Bombchus (10)'])
+ if world.dungeon_mq['Spirit Temple']:
+ pool.extend(['Bombchus (10)'] * 2)
+ if not world.dungeon_mq['Bottom of the Well']:
+ pool.extend(['Bombchus (10)'])
+ if world.dungeon_mq['Gerudo Training Grounds']:
+ pool.extend(['Bombchus (10)'])
+ if world.dungeon_mq['Ganons Castle']:
+ pool.extend(['Bombchus (10)'])
+ else:
+ pool.extend(['Bombchus (20)'])
+ if world.shuffle_medigoron_carpet_salesman:
+ pool.append('Bombchus (10)')
+
+ if not world.shuffle_medigoron_carpet_salesman:
+ placed_items['Wasteland Bombchu Salesman'] = 'Bombchus (10)'
+ skip_in_spoiler_locations.append('Wasteland Bombchu Salesman')
+
+ pool.extend(['Ice Trap'])
+ if not world.dungeon_mq['Gerudo Training Grounds']:
+ pool.extend(['Ice Trap'])
+ if not world.dungeon_mq['Ganons Castle']:
+ pool.extend(['Ice Trap'] * 4)
+
+ if world.gerudo_fortress == 'open':
+ placed_items['GF North F1 Carpenter'] = 'Recovery Heart'
+ placed_items['GF North F2 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F1 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F2 Carpenter'] = 'Recovery Heart'
+ skip_in_spoiler_locations.extend(['GF North F1 Carpenter', 'GF North F2 Carpenter', 'GF South F1 Carpenter', 'GF South F2 Carpenter'])
+ elif world.shuffle_fortresskeys in ['any_dungeon', 'overworld', 'keysanity']:
+ if world.gerudo_fortress == 'fast':
+ pool.append('Small Key (Gerudo Fortress)')
+ placed_items['GF North F2 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F1 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F2 Carpenter'] = 'Recovery Heart'
+ skip_in_spoiler_locations.extend(['GF North F2 Carpenter', 'GF South F1 Carpenter', 'GF South F2 Carpenter'])
+ else:
+ pool.extend(['Small Key (Gerudo Fortress)'] * 4)
+ if world.item_pool_value == 'plentiful':
+ pending_junk_pool.append('Small Key (Gerudo Fortress)')
+ else:
+ if world.gerudo_fortress == 'fast':
+ placed_items['GF North F1 Carpenter'] = 'Small Key (Gerudo Fortress)'
+ placed_items['GF North F2 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F1 Carpenter'] = 'Recovery Heart'
+ placed_items['GF South F2 Carpenter'] = 'Recovery Heart'
+ skip_in_spoiler_locations.extend(['GF North F2 Carpenter', 'GF South F1 Carpenter', 'GF South F2 Carpenter'])
+ else:
+ placed_items['GF North F1 Carpenter'] = 'Small Key (Gerudo Fortress)'
+ placed_items['GF North F2 Carpenter'] = 'Small Key (Gerudo Fortress)'
+ placed_items['GF South F1 Carpenter'] = 'Small Key (Gerudo Fortress)'
+ placed_items['GF South F2 Carpenter'] = 'Small Key (Gerudo Fortress)'
+
+ if world.shuffle_gerudo_card and world.gerudo_fortress != 'open':
+ pool.append('Gerudo Membership Card')
+ elif world.shuffle_gerudo_card:
+ pending_junk_pool.append('Gerudo Membership Card')
+ placed_items['GF Gerudo Membership Card'] = 'Ice Trap'
+ skip_in_spoiler_locations.append('GF Gerudo Membership Card')
+ else:
+ placed_items['GF Gerudo Membership Card'] = 'Gerudo Membership Card'
+ if world.shuffle_gerudo_card and world.item_pool_value == 'plentiful':
+ pending_junk_pool.append('Gerudo Membership Card')
+
+ if world.item_pool_value == 'plentiful' and world.shuffle_smallkeys in ['any_dungeon', 'overworld', 'keysanity']:
+ pending_junk_pool.append('Small Key (Bottom of the Well)')
+ pending_junk_pool.append('Small Key (Forest Temple)')
+ pending_junk_pool.append('Small Key (Fire Temple)')
+ pending_junk_pool.append('Small Key (Water Temple)')
+ pending_junk_pool.append('Small Key (Shadow Temple)')
+ pending_junk_pool.append('Small Key (Spirit Temple)')
+ pending_junk_pool.append('Small Key (Gerudo Training Grounds)')
+ pending_junk_pool.append('Small Key (Ganons Castle)')
+
+ if world.item_pool_value == 'plentiful' and world.shuffle_bosskeys in ['any_dungeon', 'overworld', 'keysanity']:
+ pending_junk_pool.append('Boss Key (Forest Temple)')
+ pending_junk_pool.append('Boss Key (Fire Temple)')
+ pending_junk_pool.append('Boss Key (Water Temple)')
+ pending_junk_pool.append('Boss Key (Shadow Temple)')
+ pending_junk_pool.append('Boss Key (Spirit Temple)')
+
+ if world.item_pool_value == 'plentiful' and world.shuffle_ganon_bosskey in ['any_dungeon', 'overworld', 'keysanity']:
+ pending_junk_pool.append('Boss Key (Ganons Castle)')
+
+ if world.shopsanity == 'off':
+ placed_items.update(vanilla_shop_items)
+ if world.bombchus_in_logic:
+ placed_items['KF Shop Item 8'] = 'Buy Bombchu (5)'
+ placed_items['Market Bazaar Item 4'] = 'Buy Bombchu (5)'
+ placed_items['Kak Bazaar Item 4'] = 'Buy Bombchu (5)'
+ pool.extend(normal_rupees)
+ skip_in_spoiler_locations.extend(vanilla_shop_items.keys())
+ if world.bombchus_in_logic:
+ skip_in_spoiler_locations.remove('KF Shop Item 8')
+ skip_in_spoiler_locations.remove('Market Bazaar Item 4')
+ skip_in_spoiler_locations.remove('Kak Bazaar Item 4')
+
+ else:
+ remain_shop_items = list(vanilla_shop_items.values())
+ pool.extend(min_shop_items)
+ for item in min_shop_items:
+ remain_shop_items.remove(item)
+
+ shop_slots_count = len(remain_shop_items)
+ shop_nonitem_count = len(world.shop_prices)
+ shop_item_count = shop_slots_count - shop_nonitem_count
+
+ pool.extend(random.sample(remain_shop_items, shop_item_count))
+ if shop_nonitem_count:
+ pool.extend(get_junk_item(shop_nonitem_count))
+ if world.shopsanity == '0':
+ pool.extend(normal_rupees)
+ else:
+ pool.extend(shopsanity_rupees)
+
+ if world.shuffle_scrubs != 'off':
+ if world.dungeon_mq['Deku Tree']:
+ pool.append('Deku Shield')
+ if world.dungeon_mq['Dodongos Cavern']:
+ pool.extend(['Deku Stick (1)', 'Deku Shield', 'Recovery Heart'])
+ else:
+ pool.extend(['Deku Nuts (5)', 'Deku Stick (1)', 'Deku Shield'])
+ if not world.dungeon_mq['Jabu Jabus Belly']:
+ pool.append('Deku Nuts (5)')
+ if world.dungeon_mq['Ganons Castle']:
+ pool.extend(['Bombs (5)', 'Recovery Heart', 'Rupees (5)', 'Deku Nuts (5)'])
+ else:
+ pool.extend(['Bombs (5)', 'Recovery Heart', 'Rupees (5)'])
+ pool.extend(deku_scrubs_items)
+ for _ in range(7):
+ pool.append('Arrows (30)' if random.randint(0,3) > 0 else 'Deku Seeds (30)')
+
+ else:
+ if world.dungeon_mq['Deku Tree']:
+ placed_items['Deku Tree MQ Deku Scrub'] = 'Buy Deku Shield'
+ skip_in_spoiler_locations.append('Deku Tree MQ Deku Scrub')
+ if world.dungeon_mq['Dodongos Cavern']:
+ placed_items['Dodongos Cavern MQ Deku Scrub Lobby Rear'] = 'Buy Deku Stick (1)'
+ placed_items['Dodongos Cavern MQ Deku Scrub Lobby Front'] = 'Buy Deku Seeds (30)'
+ placed_items['Dodongos Cavern MQ Deku Scrub Staircase'] = 'Buy Deku Shield'
+ placed_items['Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos'] = 'Buy Red Potion [30]'
+ skip_in_spoiler_locations.extend(['Dodongos Cavern MQ Deku Scrub Lobby Rear',
+ 'Dodongos Cavern MQ Deku Scrub Lobby Front',
+ 'Dodongos Cavern MQ Deku Scrub Staircase',
+ 'Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos'])
+ else:
+ placed_items['Dodongos Cavern Deku Scrub Near Bomb Bag Left'] = 'Buy Deku Nut (5)'
+ placed_items['Dodongos Cavern Deku Scrub Side Room Near Dodongos'] = 'Buy Deku Stick (1)'
+ placed_items['Dodongos Cavern Deku Scrub Near Bomb Bag Right'] = 'Buy Deku Seeds (30)'
+ placed_items['Dodongos Cavern Deku Scrub Lobby'] = 'Buy Deku Shield'
+ skip_in_spoiler_locations.extend(['Dodongos Cavern Deku Scrub Near Bomb Bag Left',
+ 'Dodongos Cavern Deku Scrub Side Room Near Dodongos',
+ 'Dodongos Cavern Deku Scrub Near Bomb Bag Right',
+ 'Dodongos Cavern Deku Scrub Lobby'])
+ if not world.dungeon_mq['Jabu Jabus Belly']:
+ placed_items['Jabu Jabus Belly Deku Scrub'] = 'Buy Deku Nut (5)'
+ skip_in_spoiler_locations.append('Jabu Jabus Belly Deku Scrub')
+ if world.dungeon_mq['Ganons Castle']:
+ placed_items['Ganons Castle MQ Deku Scrub Right'] = 'Buy Deku Nut (5)'
+ placed_items['Ganons Castle MQ Deku Scrub Center-Left'] = 'Buy Bombs (5) [35]'
+ placed_items['Ganons Castle MQ Deku Scrub Center'] = 'Buy Arrows (30)'
+ placed_items['Ganons Castle MQ Deku Scrub Center-Right'] = 'Buy Red Potion [30]'
+ placed_items['Ganons Castle MQ Deku Scrub Left'] = 'Buy Green Potion'
+ skip_in_spoiler_locations.extend(['Ganons Castle MQ Deku Scrub Right',
+ 'Ganons Castle MQ Deku Scrub Center-Left',
+ 'Ganons Castle MQ Deku Scrub Center',
+ 'Ganons Castle MQ Deku Scrub Center-Right',
+ 'Ganons Castle MQ Deku Scrub Left'])
+ else:
+ placed_items['Ganons Castle Deku Scrub Center-Left'] = 'Buy Bombs (5) [35]'
+ placed_items['Ganons Castle Deku Scrub Center-Right'] = 'Buy Arrows (30)'
+ placed_items['Ganons Castle Deku Scrub Right'] = 'Buy Red Potion [30]'
+ placed_items['Ganons Castle Deku Scrub Left'] = 'Buy Green Potion'
+ skip_in_spoiler_locations.extend(['Ganons Castle Deku Scrub Right',
+ 'Ganons Castle Deku Scrub Center-Left',
+ 'Ganons Castle Deku Scrub Center-Right',
+ 'Ganons Castle Deku Scrub Left'])
+ placed_items.update(vanilla_deku_scrubs)
+ skip_in_spoiler_locations.extend(vanilla_deku_scrubs.keys())
+
+ pool.extend(alwaysitems)
+
+ if world.dungeon_mq['Deku Tree']:
+ pool.extend(DT_MQ)
+ else:
+ pool.extend(DT_vanilla)
+ if world.dungeon_mq['Dodongos Cavern']:
+ pool.extend(DC_MQ)
+ else:
+ pool.extend(DC_vanilla)
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ pool.extend(JB_MQ)
+ if world.dungeon_mq['Forest Temple']:
+ pool.extend(FoT_MQ)
+ else:
+ pool.extend(FoT_vanilla)
+ if world.dungeon_mq['Fire Temple']:
+ pool.extend(FiT_MQ)
+ else:
+ pool.extend(FiT_vanilla)
+ if world.dungeon_mq['Spirit Temple']:
+ pool.extend(SpT_MQ)
+ else:
+ pool.extend(SpT_vanilla)
+ if world.dungeon_mq['Shadow Temple']:
+ pool.extend(ShT_MQ)
+ else:
+ pool.extend(ShT_vanilla)
+ if not world.dungeon_mq['Bottom of the Well']:
+ pool.extend(BW_vanilla)
+ if world.dungeon_mq['Gerudo Training Grounds']:
+ pool.extend(GTG_MQ)
+ else:
+ pool.extend(GTG_vanilla)
+ if world.dungeon_mq['Ganons Castle']:
+ pool.extend(GC_MQ)
+ else:
+ pool.extend(GC_vanilla)
+
+ for i in range(bottle_count):
+ if i >= ruto_bottles:
+ bottle = random.choice(normal_bottles)
+ pool.append(bottle)
+ else:
+ pool.append('Rutos Letter')
+
+ earliest_trade = tradeitemoptions.index(world.logic_earliest_adult_trade)
+ latest_trade = tradeitemoptions.index(world.logic_latest_adult_trade)
+ if earliest_trade > latest_trade:
+ earliest_trade, latest_trade = latest_trade, earliest_trade
+ tradeitem = random.choice(tradeitems[earliest_trade:latest_trade+1])
+ world.selected_adult_trade_item = tradeitem
+ pool.append(tradeitem)
+
+ pool.extend(songlist)
+ if world.shuffle_song_items == 'any' and world.item_pool_value == 'plentiful':
+ pending_junk_pool.extend(songlist)
+
+ if world.free_scarecrow:
+ item = world.create_item('Scarecrow Song')
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+
+ if world.no_epona_race:
+ item = world.create_item('Epona')
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+
+ if world.shuffle_mapcompass == 'remove' or world.shuffle_mapcompass == 'startwith':
+ for item in [item for dungeon in world.dungeons for item in dungeon.dungeon_items]:
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+ pool.extend(get_junk_item())
+ if world.shuffle_smallkeys == 'remove':
+ for item in [item for dungeon in world.dungeons for item in dungeon.small_keys]:
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+ pool.extend(get_junk_item())
+ if world.shuffle_bosskeys == 'remove':
+ for item in [item for dungeon in world.dungeons if dungeon.name != 'Ganons Castle' for item in dungeon.boss_key]:
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+ pool.extend(get_junk_item())
+ if world.shuffle_ganon_bosskey in ['remove', 'triforce']:
+ for item in [item for dungeon in world.dungeons if dungeon.name == 'Ganons Castle' for item in dungeon.boss_key]:
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+ pool.extend(get_junk_item())
+
+ if world.shuffle_mapcompass == 'vanilla':
+ for location, item in vanillaMC.items():
+ try:
+ world.get_location(location)
+ placed_items[location] = item
+ except KeyError:
+ continue
+ if world.shuffle_smallkeys == 'vanilla':
+ for location, item in vanillaSK.items():
+ try:
+ world.get_location(location)
+ placed_items[location] = item
+ except KeyError:
+ continue
+ # Logic cannot handle vanilla key layout in some dungeons
+ # this is because vanilla expects the dungeon major item to be
+ # locked behind the keys, which is not always true in rando.
+ # We can resolve this by starting with some extra keys
+ if world.dungeon_mq['Spirit Temple']:
+ # Yes somehow you need 3 keys. This dungeon is bonkers
+ world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
+ world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
+ world.world.push_precollected(world.create_item('Small Key (Spirit Temple)'))
+ #if not world.dungeon_mq['Fire Temple']:
+ # world.state.collect(ItemFactory('Small Key (Fire Temple)'))
+ if world.shuffle_bosskeys == 'vanilla':
+ for location, item in vanillaBK.items():
+ try:
+ world.get_location(location)
+ placed_items[location] = item
+ except KeyError:
+ continue
+
+
+ if not world.keysanity and not world.dungeon_mq['Fire Temple']:
+ item = world.create_item('Small Key (Fire Temple)')
+ world.world.push_precollected(item)
+ world.remove_from_start_inventory.append(item.name)
+
+ if world.triforce_hunt:
+ triforce_count = int((TriforceCounts[world.item_pool_value] * world.triforce_goal).to_integral_value(rounding=ROUND_HALF_UP))
+ pending_junk_pool.extend(['Triforce Piece'] * triforce_count)
+
+ if world.shuffle_ganon_bosskey == 'on_lacs':
+ placed_items['ToT Light Arrows Cutscene'] = 'Boss Key (Ganons Castle)'
+ elif world.shuffle_ganon_bosskey == 'vanilla':
+ placed_items['Ganons Tower Boss Key Chest'] = 'Boss Key (Ganons Castle)'
+
+ if world.item_pool_value == 'plentiful':
+ pool.extend(easy_items)
+ else:
+ pool.extend(normal_items)
+
+ if not world.shuffle_kokiri_sword:
+ replace_max_item(pool, 'Kokiri Sword', 0)
+
+ if world.junk_ice_traps == 'off':
+ replace_max_item(pool, 'Ice Trap', 0)
+ elif world.junk_ice_traps == 'onslaught':
+ for item in [item for item, weight in junk_pool_base] + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)']:
+ replace_max_item(pool, item, 0)
+
+ for item,max in item_difficulty_max[world.item_pool_value].items():
+ replace_max_item(pool, item, max)
+
+ if world.damage_multiplier in ['ohko', 'quadruple'] and world.item_pool_value == 'minimal':
+ pending_junk_pool.append('Nayrus Love')
+
+ # world.distribution.alter_pool(world, pool)
+
+ # Make sure our pending_junk_pool is empty. If not, remove some random junk here.
+ if pending_junk_pool:
+
+ remove_junk_pool, _ = zip(*junk_pool_base)
+ # Omits Rupees (200) and Deku Nuts (10)
+ remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap']
+
+ junk_candidates = [item for item in pool if item in remove_junk_pool]
+ while pending_junk_pool:
+ pending_item = pending_junk_pool.pop()
+ if not junk_candidates:
+ raise RuntimeError("Not enough junk exists in item pool for %s to be added." % pending_item)
+ junk_item = random.choice(junk_candidates)
+ junk_candidates.remove(junk_item)
+ pool.remove(junk_item)
+ pool.append(pending_item)
+
+ return (pool, placed_items, skip_in_spoiler_locations)
diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py
new file mode 100644
index 00000000..fcdb2237
--- /dev/null
+++ b/worlds/oot/Items.py
@@ -0,0 +1,414 @@
+import typing
+
+from BaseClasses import Item
+
+def oot_data_to_ap_id(data, event):
+ if event or data[2] is None or data[0] == 'Shop':
+ return None
+ offset = 66000
+ if data[0] in ['Item', 'BossKey', 'Compass', 'Map', 'SmallKey', 'Token', 'GanonBossKey', 'FortressSmallKey', 'Song']:
+ return offset + data[2]
+ else:
+ raise Exception(f'Unexpected OOT item type found: {data[0]}')
+
+def ap_id_to_oot_data(ap_id):
+ offset = 66000
+ val = ap_id - offset
+ try:
+ return list(filter(lambda d: d[1][0] == 'Item' and d[1][2] == val, item_table.items()))[0]
+ except IndexError:
+ raise Exception(f'Could not find desired item ID: {ap_id}')
+
+class OOTItem(Item):
+ game: str = "Ocarina of Time"
+
+ def __init__(self, name, player, data, event):
+ (type, advancement, index, special) = data
+ adv = True if advancement else False # this looks silly but the table uses True, False, and None
+ super(OOTItem, self).__init__(name, adv, oot_data_to_ap_id(data, event), player)
+ self.type = type
+ self.index = index
+ self.special = special or {}
+ self.looks_like_item = None
+ self.price = special.get('price', None) if special else None
+ self.internal = False
+
+ # The playthrough calculation calls a function that uses "sweep_for_events(key_only=True)"
+ # This checks if the item it's looking for is a small key, using the small key property.
+ # Because of overlapping item fields, this means that OoT small keys are technically counted, unless we do this.
+ # This causes them to be double-collected during playthrough and generation.
+ @property
+ def smallkey(self) -> bool:
+ return False
+
+ @property
+ def bigkey(self) -> bool:
+ return False
+
+ @property
+ def dungeonitem(self) -> bool:
+ return self.type in ['SmallKey', 'FortressSmallKey', 'BossKey', 'GanonBossKey', 'Map', 'Compass']
+
+
+
+# Progressive: True -> Advancement
+# False -> Priority
+# None -> Normal
+# Item: (type, Progressive, GetItemID, special),
+item_table = {
+ 'Bombs (5)': ('Item', None, 0x01, None),
+ 'Deku Nuts (5)': ('Item', None, 0x02, None),
+ 'Bombchus (10)': ('Item', True, 0x03, None),
+ 'Boomerang': ('Item', True, 0x06, None),
+ 'Deku Stick (1)': ('Item', None, 0x07, None),
+ 'Lens of Truth': ('Item', True, 0x0A, None),
+ 'Megaton Hammer': ('Item', True, 0x0D, None),
+ 'Cojiro': ('Item', True, 0x0E, None),
+ 'Bottle': ('Item', True, 0x0F, {'bottle': float('Inf')}),
+ 'Bottle with Milk': ('Item', True, 0x14, {'bottle': float('Inf')}),
+ 'Rutos Letter': ('Item', True, 0x15, None),
+ 'Deliver Letter': ('Item', True, None, {'bottle': float('Inf')}),
+ 'Sell Big Poe': ('Item', True, None, {'bottle': float('Inf')}),
+ 'Magic Bean': ('Item', True, 0x16, None),
+ 'Skull Mask': ('Item', True, 0x17, None),
+ 'Spooky Mask': ('Item', None, 0x18, None),
+ 'Keaton Mask': ('Item', None, 0x1A, None),
+ 'Bunny Hood': ('Item', None, 0x1B, None),
+ 'Mask of Truth': ('Item', True, 0x1C, None),
+ 'Pocket Egg': ('Item', True, 0x1D, None),
+ 'Pocket Cucco': ('Item', True, 0x1E, None),
+ 'Odd Mushroom': ('Item', True, 0x1F, None),
+ 'Odd Potion': ('Item', True, 0x20, None),
+ 'Poachers Saw': ('Item', True, 0x21, None),
+ 'Broken Sword': ('Item', True, 0x22, None),
+ 'Prescription': ('Item', True, 0x23, None),
+ 'Eyeball Frog': ('Item', True, 0x24, None),
+ 'Eyedrops': ('Item', True, 0x25, None),
+ 'Claim Check': ('Item', True, 0x26, None),
+ 'Kokiri Sword': ('Item', True, 0x27, None),
+ 'Giants Knife': ('Item', True, 0x28, None),
+ 'Deku Shield': ('Item', None, 0x29, None),
+ 'Hylian Shield': ('Item', None, 0x2A, None),
+ 'Mirror Shield': ('Item', True, 0x2B, None),
+ 'Goron Tunic': ('Item', True, 0x2C, None),
+ 'Zora Tunic': ('Item', True, 0x2D, None),
+ 'Iron Boots': ('Item', True, 0x2E, None),
+ 'Hover Boots': ('Item', True, 0x2F, None),
+ 'Stone of Agony': ('Item', True, 0x39, None),
+ 'Gerudo Membership Card': ('Item', True, 0x3A, None),
+ 'Heart Container': ('Item', None, 0x3D, None),
+ 'Piece of Heart': ('Item', None, 0x3E, None),
+ 'Boss Key': ('BossKey', True, 0x3F, None),
+ 'Compass': ('Compass', None, 0x40, None),
+ 'Map': ('Map', None, 0x41, None),
+ 'Small Key': ('SmallKey', True, 0x42, {'progressive': float('Inf')}),
+ 'Weird Egg': ('Item', True, 0x47, None),
+ 'Recovery Heart': ('Item', None, 0x48, None),
+ 'Arrows (5)': ('Item', None, 0x49, None),
+ 'Arrows (10)': ('Item', None, 0x4A, None),
+ 'Arrows (30)': ('Item', None, 0x4B, None),
+ 'Rupee (1)': ('Item', None, 0x4C, None),
+ 'Rupees (5)': ('Item', None, 0x4D, None),
+ 'Rupees (20)': ('Item', None, 0x4E, None),
+ 'Heart Container (Boss)': ('Item', None, 0x4F, None),
+ 'Milk': ('Item', None, 0x50, None),
+ 'Goron Mask': ('Item', None, 0x51, None),
+ 'Zora Mask': ('Item', None, 0x52, None),
+ 'Gerudo Mask': ('Item', None, 0x53, None),
+ 'Rupees (50)': ('Item', None, 0x55, None),
+ 'Rupees (200)': ('Item', None, 0x56, None),
+ 'Biggoron Sword': ('Item', True, 0x57, None),
+ 'Fire Arrows': ('Item', True, 0x58, None),
+ 'Ice Arrows': ('Item', True, 0x59, None),
+ 'Light Arrows': ('Item', True, 0x5A, None),
+ 'Gold Skulltula Token': ('Token', True, 0x5B, {'progressive': float('Inf')}),
+ 'Dins Fire': ('Item', True, 0x5C, None),
+ 'Nayrus Love': ('Item', True, 0x5E, None),
+ 'Farores Wind': ('Item', True, 0x5D, None),
+ 'Deku Nuts (10)': ('Item', None, 0x64, None),
+ 'Bombs (10)': ('Item', None, 0x66, None),
+ 'Bombs (20)': ('Item', None, 0x67, None),
+ 'Deku Seeds (30)': ('Item', None, 0x69, None),
+ 'Bombchus (5)': ('Item', True, 0x6A, None),
+ 'Bombchus (20)': ('Item', True, 0x6B, None),
+ 'Rupee (Treasure Chest Game)': ('Item', None, 0x72, None),
+ 'Piece of Heart (Treasure Chest Game)': ('Item', None, 0x76, None),
+ 'Ice Trap': ('Item', None, 0x7C, None),
+ 'Progressive Hookshot': ('Item', True, 0x80, {'progressive': 2}),
+ 'Progressive Strength Upgrade': ('Item', True, 0x81, {'progressive': 3}),
+ 'Bomb Bag': ('Item', True, 0x82, None),
+ 'Bow': ('Item', True, 0x83, None),
+ 'Slingshot': ('Item', True, 0x84, None),
+ 'Progressive Wallet': ('Item', True, 0x85, {'progressive': 3}),
+ 'Progressive Scale': ('Item', True, 0x86, {'progressive': 2}),
+ 'Deku Nut Capacity': ('Item', None, 0x87, None),
+ 'Deku Stick Capacity': ('Item', None, 0x88, None),
+ 'Bombchus': ('Item', True, 0x89, None),
+ 'Magic Meter': ('Item', True, 0x8A, None),
+ 'Ocarina': ('Item', True, 0x8B, None),
+ 'Bottle with Red Potion': ('Item', True, 0x8C, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Green Potion': ('Item', True, 0x8D, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Blue Potion': ('Item', True, 0x8E, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Fairy': ('Item', True, 0x8F, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Fish': ('Item', True, 0x90, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Blue Fire': ('Item', True, 0x91, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Bugs': ('Item', True, 0x92, {'bottle': True, 'shop_object': 0x0F}),
+ 'Bottle with Big Poe': ('Item', True, 0x93, {'shop_object': 0x0F}),
+ 'Bottle with Poe': ('Item', True, 0x94, {'bottle': True, 'shop_object': 0x0F}),
+ 'Boss Key (Forest Temple)': ('BossKey', True, 0x95, None),
+ 'Boss Key (Fire Temple)': ('BossKey', True, 0x96, None),
+ 'Boss Key (Water Temple)': ('BossKey', True, 0x97, None),
+ 'Boss Key (Spirit Temple)': ('BossKey', True, 0x98, None),
+ 'Boss Key (Shadow Temple)': ('BossKey', True, 0x99, None),
+ 'Boss Key (Ganons Castle)': ('GanonBossKey',True,0x9A,None),
+ 'Compass (Deku Tree)': ('Compass', None, 0x9B, None),
+ 'Compass (Dodongos Cavern)': ('Compass', None, 0x9C, None),
+ 'Compass (Jabu Jabus Belly)': ('Compass', None, 0x9D, None),
+ 'Compass (Forest Temple)': ('Compass', None, 0x9E, None),
+ 'Compass (Fire Temple)': ('Compass', None, 0x9F, None),
+ 'Compass (Water Temple)': ('Compass', None, 0xA0, None),
+ 'Compass (Spirit Temple)': ('Compass', None, 0xA1, None),
+ 'Compass (Shadow Temple)': ('Compass', None, 0xA2, None),
+ 'Compass (Bottom of the Well)': ('Compass', None, 0xA3, None),
+ 'Compass (Ice Cavern)': ('Compass', None, 0xA4, None),
+ 'Map (Deku Tree)': ('Map', None, 0xA5, None),
+ 'Map (Dodongos Cavern)': ('Map', None, 0xA6, None),
+ 'Map (Jabu Jabus Belly)': ('Map', None, 0xA7, None),
+ 'Map (Forest Temple)': ('Map', None, 0xA8, None),
+ 'Map (Fire Temple)': ('Map', None, 0xA9, None),
+ 'Map (Water Temple)': ('Map', None, 0xAA, None),
+ 'Map (Spirit Temple)': ('Map', None, 0xAB, None),
+ 'Map (Shadow Temple)': ('Map', None, 0xAC, None),
+ 'Map (Bottom of the Well)': ('Map', None, 0xAD, None),
+ 'Map (Ice Cavern)': ('Map', None, 0xAE, None),
+ 'Small Key (Forest Temple)': ('SmallKey', True, 0xAF, {'progressive': float('Inf')}),
+ 'Small Key (Fire Temple)': ('SmallKey', True, 0xB0, {'progressive': float('Inf')}),
+ 'Small Key (Water Temple)': ('SmallKey', True, 0xB1, {'progressive': float('Inf')}),
+ 'Small Key (Spirit Temple)': ('SmallKey', True, 0xB2, {'progressive': float('Inf')}),
+ 'Small Key (Shadow Temple)': ('SmallKey', True, 0xB3, {'progressive': float('Inf')}),
+ 'Small Key (Bottom of the Well)': ('SmallKey', True, 0xB4, {'progressive': float('Inf')}),
+ 'Small Key (Gerudo Training Grounds)': ('SmallKey',True, 0xB5, {'progressive': float('Inf')}),
+ 'Small Key (Gerudo Fortress)': ('FortressSmallKey',True, 0xB6, {'progressive': float('Inf')}),
+ 'Small Key (Ganons Castle)': ('SmallKey', True, 0xB7, {'progressive': float('Inf')}),
+ 'Double Defense': ('Item', True, 0xB8, None),
+ 'Magic Bean Pack': ('Item', True, 0xC9, None),
+ 'Triforce Piece': ('Item', True, 0xCA, {'progressive': float('Inf')}),
+ 'Zeldas Letter': ('Item', True, 0x0B, None),
+ 'Time Travel': ('Event', True, None, None),
+ 'Scarecrow Song': ('Event', True, None, None),
+ 'Triforce': ('Event', True, None, None),
+
+ # Event items otherwise generated by generic event logic
+ # can be defined here to enforce their appearance in playthroughs.
+ 'Water Temple Clear': ('Event', True, None, None),
+ 'Forest Trial Clear': ('Event', True, None, None),
+ 'Fire Trial Clear': ('Event', True, None, None),
+ 'Water Trial Clear': ('Event', True, None, None),
+ 'Shadow Trial Clear': ('Event', True, None, None),
+ 'Spirit Trial Clear': ('Event', True, None, None),
+ 'Light Trial Clear': ('Event', True, None, None),
+
+ 'Deku Stick Drop': ('Drop', True, None, None),
+ 'Deku Nut Drop': ('Drop', True, None, None),
+ 'Blue Fire': ('Drop', True, None, None),
+ 'Fairy': ('Drop', True, None, None),
+ 'Fish': ('Drop', True, None, None),
+ 'Bugs': ('Drop', True, None, None),
+ 'Big Poe': ('Drop', True, None, None),
+ 'Bombchu Drop': ('Drop', True, None, None),
+
+ # Consumable refills defined mostly to placate 'starting with' options
+ 'Arrows': ('Refill', None, None, None),
+ 'Bombs': ('Refill', None, None, None),
+ 'Deku Seeds': ('Refill', None, None, None),
+ 'Deku Sticks': ('Refill', None, None, None),
+ 'Deku Nuts': ('Refill', None, None, None),
+ 'Rupees': ('Refill', None, None, None),
+
+ 'Minuet of Forest': ('Song', True, 0xBB,
+ {
+ 'text_id': 0x73,
+ 'song_id': 0x02,
+ 'item_id': 0x5A,
+ }),
+ 'Bolero of Fire': ('Song', True, 0xBC,
+ {
+ 'text_id': 0x74,
+ 'song_id': 0x03,
+ 'item_id': 0x5B,
+ }),
+ 'Serenade of Water': ('Song', True, 0xBD,
+ {
+ 'text_id': 0x75,
+ 'song_id': 0x04,
+ 'item_id': 0x5C,
+ }),
+ 'Requiem of Spirit': ('Song', True, 0xBE,
+ {
+ 'text_id': 0x76,
+ 'song_id': 0x05,
+ 'item_id': 0x5D,
+ }),
+ 'Nocturne of Shadow': ('Song', True, 0xBF,
+ {
+ 'text_id': 0x77,
+ 'song_id': 0x06,
+ 'item_id': 0x5E,
+ }),
+ 'Prelude of Light': ('Song', True, 0xC0,
+ {
+ 'text_id': 0x78,
+ 'song_id': 0x07,
+ 'item_id': 0x5F,
+ }),
+ 'Zeldas Lullaby': ('Song', True, 0xC1,
+ {
+ 'text_id': 0xD4,
+ 'song_id': 0x0A,
+ 'item_id': 0x60,
+ }),
+ 'Eponas Song': ('Song', True, 0xC2,
+ {
+ 'text_id': 0xD2,
+ 'song_id': 0x09,
+ 'item_id': 0x61,
+ }),
+ 'Sarias Song': ('Song', True, 0xC3,
+ {
+ 'text_id': 0xD1,
+ 'song_id': 0x08,
+ 'item_id': 0x62,
+ }),
+ 'Suns Song': ('Song', True, 0xC4,
+ {
+ 'text_id': 0xD3,
+ 'song_id': 0x0B,
+ 'item_id': 0x63,
+ }),
+ 'Song of Time': ('Song', True, 0xC5,
+ {
+ 'text_id': 0xD5,
+ 'song_id': 0x0C,
+ 'item_id': 0x64,
+ }),
+ 'Song of Storms': ('Song', True, 0xC6,
+ {
+ 'text_id': 0xD6,
+ 'song_id': 0x0D,
+ 'item_id': 0x65,
+ }),
+
+ 'Buy Deku Nut (5)': ('Shop', True, 0x00, {'object': 0x00BB, 'price': 15}),
+ 'Buy Arrows (30)': ('Shop', False, 0x01, {'object': 0x00D8, 'price': 60}),
+ 'Buy Arrows (50)': ('Shop', False, 0x02, {'object': 0x00D8, 'price': 90}),
+ 'Buy Bombs (5) [25]': ('Shop', False, 0x03, {'object': 0x00CE, 'price': 25}),
+ 'Buy Deku Nut (10)': ('Shop', True, 0x04, {'object': 0x00BB, 'price': 30}),
+ 'Buy Deku Stick (1)': ('Shop', True, 0x05, {'object': 0x00C7, 'price': 10}),
+ 'Buy Bombs (10)': ('Shop', False, 0x06, {'object': 0x00CE, 'price': 50}),
+ 'Buy Fish': ('Shop', True, 0x07, {'object': 0x00F4, 'price': 200}),
+ 'Buy Red Potion [30]': ('Shop', False, 0x08, {'object': 0x00EB, 'price': 30}),
+ 'Buy Green Potion': ('Shop', False, 0x09, {'object': 0x00EB, 'price': 30}),
+ 'Buy Blue Potion': ('Shop', False, 0x0A, {'object': 0x00EB, 'price': 100}),
+ 'Buy Hylian Shield': ('Shop', True, 0x0C, {'object': 0x00DC, 'price': 80}),
+ 'Buy Deku Shield': ('Shop', True, 0x0D, {'object': 0x00CB, 'price': 40}),
+ 'Buy Goron Tunic': ('Shop', True, 0x0E, {'object': 0x00F2, 'price': 200}),
+ 'Buy Zora Tunic': ('Shop', True, 0x0F, {'object': 0x00F2, 'price': 300}),
+ 'Buy Heart': ('Shop', False, 0x10, {'object': 0x00B7, 'price': 10}),
+ 'Buy Bombchu (10)': ('Shop', True, 0x15, {'object': 0x00D9, 'price': 99}),
+ 'Buy Bombchu (20)': ('Shop', True, 0x16, {'object': 0x00D9, 'price': 180}),
+ 'Buy Bombchu (5)': ('Shop', True, 0x18, {'object': 0x00D9, 'price': 60}),
+ 'Buy Deku Seeds (30)': ('Shop', False, 0x1D, {'object': 0x0119, 'price': 30}),
+ 'Sold Out': ('Shop', False, 0x26, {'object': 0x0148}),
+ 'Buy Blue Fire': ('Shop', True, 0x27, {'object': 0x0173, 'price': 300}),
+ 'Buy Bottle Bug': ('Shop', True, 0x28, {'object': 0x0174, 'price': 50}),
+ 'Buy Poe': ('Shop', False, 0x2A, {'object': 0x0176, 'price': 30}),
+ 'Buy Fairy\'s Spirit': ('Shop', True, 0x2B, {'object': 0x0177, 'price': 50}),
+ 'Buy Arrows (10)': ('Shop', False, 0x2C, {'object': 0x00D8, 'price': 20}),
+ 'Buy Bombs (20)': ('Shop', False, 0x2D, {'object': 0x00CE, 'price': 80}),
+ 'Buy Bombs (30)': ('Shop', False, 0x2E, {'object': 0x00CE, 'price': 120}),
+ 'Buy Bombs (5) [35]': ('Shop', False, 0x2F, {'object': 0x00CE, 'price': 35}),
+ 'Buy Red Potion [40]': ('Shop', False, 0x30, {'object': 0x00EB, 'price': 40}),
+ 'Buy Red Potion [50]': ('Shop', False, 0x31, {'object': 0x00EB, 'price': 50}),
+
+ 'Kokiri Emerald': ('DungeonReward', True, None,
+ {
+ 'stone': True,
+ 'addr2_data': 0x80,
+ 'bit_mask': 0x00040000,
+ 'item_id': 0x6C,
+ 'actor_type': 0x13,
+ 'object_id': 0x00AD,
+ }),
+ 'Goron Ruby': ('DungeonReward', True, None,
+ {
+ 'stone': True,
+ 'addr2_data': 0x81,
+ 'bit_mask': 0x00080000,
+ 'item_id': 0x6D,
+ 'actor_type': 0x14,
+ 'object_id': 0x00AD,
+ }),
+ 'Zora Sapphire': ('DungeonReward', True, None,
+ {
+ 'stone': True,
+ 'addr2_data': 0x82,
+ 'bit_mask': 0x00100000,
+ 'item_id': 0x6E,
+ 'actor_type': 0x15,
+ 'object_id': 0x00AD,
+ }),
+ 'Forest Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x3E,
+ 'bit_mask': 0x00000001,
+ 'item_id': 0x66,
+ 'actor_type': 0x0B,
+ 'object_id': 0x00BA,
+ }),
+ 'Fire Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x3C,
+ 'bit_mask': 0x00000002,
+ 'item_id': 0x67,
+ 'actor_type': 0x09,
+ 'object_id': 0x00BA,
+ }),
+ 'Water Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x3D,
+ 'bit_mask': 0x00000004,
+ 'item_id': 0x68,
+ 'actor_type': 0x0A,
+ 'object_id': 0x00BA,
+ }),
+ 'Spirit Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x3F,
+ 'bit_mask': 0x00000008,
+ 'item_id': 0x69,
+ 'actor_type': 0x0C,
+ 'object_id': 0x00BA,
+ }),
+ 'Shadow Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x41,
+ 'bit_mask': 0x00000010,
+ 'item_id': 0x6A,
+ 'actor_type': 0x0D,
+ 'object_id': 0x00BA,
+ }),
+ 'Light Medallion': ('DungeonReward', True, None,
+ {
+ 'medallion': True,
+ 'addr2_data': 0x40,
+ 'bit_mask': 0x00000020,
+ 'item_id': 0x6B,
+ 'actor_type': 0x0E,
+ 'object_id': 0x00BA,
+ }),
+}
diff --git a/worlds/oot/JSONDump.py b/worlds/oot/JSONDump.py
new file mode 100644
index 00000000..d653a3fd
--- /dev/null
+++ b/worlds/oot/JSONDump.py
@@ -0,0 +1,122 @@
+import json
+
+from functools import reduce
+
+INDENT = ' '
+
+class CollapseList(list):
+ pass
+class CollapseDict(dict):
+ pass
+class AlignedDict(dict):
+ def __init__(self, src_dict, depth):
+ self.depth = depth - 1
+ super().__init__(src_dict)
+class SortedDict(dict):
+ pass
+
+
+def is_scalar(value):
+ return not is_list(value) and not is_dict(value)
+
+
+def is_list(value):
+ return isinstance(value, list) or isinstance(value, tuple)
+
+
+def is_dict(value):
+ return isinstance(value, dict)
+
+
+def dump_scalar(obj, ensure_ascii=False):
+ return json.dumps(obj, ensure_ascii=ensure_ascii)
+
+
+def dump_list(obj, current_indent='', ensure_ascii=False):
+ entries = [dump_obj(value, current_indent + INDENT, ensure_ascii=ensure_ascii) for value in obj]
+
+ if len(entries) == 0:
+ return '[]'
+
+ if isinstance(obj, CollapseList):
+ values_format = '{value}'
+ output_format = '[{values}]'
+ join_format = ', '
+ else:
+ values_format = '{indent}{value}'
+ output_format = '[\n{values}\n{indent}]'
+ join_format = ',\n'
+
+ output = output_format.format(
+ indent=current_indent,
+ values=join_format.join([values_format.format(
+ value=entry,
+ indent=current_indent + INDENT
+ ) for entry in entries])
+ )
+
+ return output
+
+
+def get_keys(obj, depth):
+ if depth == 0:
+ yield from obj.keys()
+ else:
+ for value in obj.values():
+ yield from get_keys(value, depth - 1)
+
+
+def dump_dict(obj, current_indent='', sub_width=None, ensure_ascii=False):
+ entries = []
+
+ key_width = None
+ if sub_width is not None:
+ sub_width = (sub_width[0]-1, sub_width[1])
+ if sub_width[0] == 0:
+ key_width = sub_width[1]
+
+ if isinstance(obj, AlignedDict):
+ sub_keys = get_keys(obj, obj.depth)
+ sub_width = (obj.depth, reduce(lambda acc, entry: max(acc, len(entry)), sub_keys, 0))
+
+ for key, value in obj.items():
+ entries.append((dump_scalar(str(key), ensure_ascii), dump_obj(value, current_indent + INDENT, sub_width, ensure_ascii)))
+
+ if key_width is None:
+ key_width = reduce(lambda acc, entry: max(acc, len(entry[0])), entries, 0)
+
+ if len(entries) == 0:
+ return '{}'
+
+ if isinstance(obj, SortedDict):
+ entries.sort(key=lambda item: item[0])
+
+ if isinstance(obj, CollapseDict):
+ values_format = '{key} {value}'
+ output_format = '{{{values}}}'
+ join_format = ', '
+ else:
+ values_format = '{indent}{key:{padding}}{value}'
+ output_format = '{{\n{values}\n{indent}}}'
+ join_format = ',\n'
+
+ output = output_format.format(
+ indent=current_indent,
+ values=join_format.join([values_format.format(
+ key='{key}:'.format(key=key),
+ value=value,
+ indent=current_indent + INDENT,
+ padding=key_width + 2,
+ ) for (key, value) in entries])
+ )
+
+ return output
+
+
+def dump_obj(obj, current_indent='', sub_width=None, ensure_ascii=False):
+ if is_list(obj):
+ return dump_list(obj, current_indent, ensure_ascii)
+ elif is_dict(obj):
+ return dump_dict(obj, current_indent, sub_width, ensure_ascii)
+ else:
+ return dump_scalar(obj, ensure_ascii)
diff --git a/worlds/oot/LICENSE b/worlds/oot/LICENSE
new file mode 100644
index 00000000..c10701c6
--- /dev/null
+++ b/worlds/oot/LICENSE
@@ -0,0 +1,26 @@
+MIT License
+
+Copyright (c) 2017 Amazing Ampharos
+Copyright (c) 2021 espeon65536
+
+Credit for contributions to Junglechief87 on this and to LLCoolDave and
+KevinCathcart for their work on the Zelda Lttp Entrance Randomizer which
+was the code base for this project.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/worlds/oot/Location.py b/worlds/oot/Location.py
new file mode 100644
index 00000000..a8b659e7
--- /dev/null
+++ b/worlds/oot/Location.py
@@ -0,0 +1,53 @@
+from .LocationList import location_table
+from BaseClasses import Location
+
+location_id_offset = 67000
+location_name_to_id = {name: (location_id_offset + index) for (index, name) in enumerate(location_table)
+ if location_table[name][0] not in ['Boss', 'Event', 'Drop', 'HintStone', 'Hint']}
+
+class OOTLocation(Location):
+ game: str = 'Ocarina of Time'
+
+ def __init__(self, player, name='', code=None, address1=None, address2=None, default=None, type='Chest', scene=None, parent=None, filter_tags=None, internal=False):
+ super(OOTLocation, self).__init__(player, name, code, parent)
+ self.address1 = address1
+ self.address2 = address2
+ self.default = default
+ self.type = type
+ self.scene = scene
+ self.internal = internal
+ if filter_tags is None:
+ self.filter_tags = None
+ else:
+ self.filter_tags = list(filter_tags)
+ self.never = False # no idea what this does
+
+ if type == 'Event':
+ self.event = True
+
+
+def LocationFactory(locations, player: int):
+ ret = []
+ singleton = False
+ if isinstance(locations, str):
+ locations = [locations]
+ singleton = True
+ for location in locations:
+ if location in location_table:
+ match_location = location
+ else:
+ match_location = next(filter(lambda k: k.lower() == location.lower(), location_table), None)
+ if match_location:
+ type, scene, default, addresses, vanilla_item, filter_tags = location_table[match_location]
+ if addresses is None:
+ addresses = (None, None)
+ address1, address2 = addresses
+ ret.append(OOTLocation(player, match_location, location_name_to_id.get(match_location, None), address1, address2, default, type, scene, filter_tags=filter_tags))
+ else:
+ raise KeyError('Unknown Location: %s', location)
+
+ if singleton:
+ return ret[0]
+ return ret
+
+
diff --git a/worlds/oot/LocationList.py b/worlds/oot/LocationList.py
new file mode 100644
index 00000000..e487522f
--- /dev/null
+++ b/worlds/oot/LocationList.py
@@ -0,0 +1,932 @@
+from collections import OrderedDict
+
+
+def shop_address(shop_id, shelf_id):
+ return 0xC71ED0 + (0x40 * shop_id) + (0x08 * shelf_id)
+
+# Abbreviations
+# DMC Death Mountain Crater
+# DMT Death Mountain Trail
+# GC Goron City
+# GF Gerudo Fortress
+# GS Gold Skulltula
+# GV Gerudo Valley
+# HC Hyrule Castle
+# HF Hyrule Field
+# KF Kokiri Forest
+# LH Lake Hylia
+# LLR Lon Lon Ranch
+# LW Lost Woods
+# OGC Outside Ganon's Castle
+# SFM Sacred Forest Meadow
+# ToT Temple of Time
+# ZD Zora's Domain
+# ZF Zora's Fountain
+# ZR Zora's River
+
+# The order of this table is reflected in the spoiler's list of locations (except Hints aren't included).
+# Within a section, the order of types is: gifts/freestanding/chests, Deku Scrubs, Cows, Gold Skulltulas, Shops.
+
+# NPC Scrubs are on the overworld, while GrottoNPC is a special handler for Grottos
+# Grottos scrubs are the same scene and actor, so we use a unique grotto ID for the scene
+
+# Note that the scene for skulltulas is not the actual scene the token appears in
+# Rather, it is the index of the grouping used when storing skulltula collection
+# For example, zora river, zora's domain, and zora fountain are all a single 'scene' for skulltulas
+
+# Location: Type Scene Default Addresses Vanilla Item Categories
+location_table = OrderedDict([
+ ## Dungeon Rewards
+ ("Links Pocket", ("Boss", None, None, None, 'Light Medallion', None)),
+ ("Queen Gohma", ("Boss", None, 0x6C, (0x0CA315F, 0x2079571), 'Kokiri Emerald', None)),
+ ("King Dodongo", ("Boss", None, 0x6D, (0x0CA30DF, 0x2223309), 'Goron Ruby', None)),
+ ("Barinade", ("Boss", None, 0x6E, (0x0CA36EB, 0x2113C19), 'Zora Sapphire', None)),
+ ("Phantom Ganon", ("Boss", None, 0x66, (0x0CA3D07, 0x0D4ED79), 'Forest Medallion', None)),
+ ("Volvagia", ("Boss", None, 0x67, (0x0CA3D93, 0x0D10135), 'Fire Medallion', None)),
+ ("Morpha", ("Boss", None, 0x68, (0x0CA3E1F, 0x0D5A3A9), 'Water Medallion', None)),
+ ("Bongo Bongo", ("Boss", None, 0x6A, (0x0CA3F43, 0x0D13E19), 'Shadow Medallion', None)),
+ ("Twinrova", ("Boss", None, 0x69, (0x0CA3EB3, 0x0D39FF1), 'Spirit Medallion', None)),
+ ("Ganon", ("Event", None, None, None, 'Triforce', None)),
+
+ ## Songs
+ ("Song from Impa", ("Song", 0xFF, 0x26, (0x2E8E925, 0x2E8E925), 'Zeldas Lullaby', ("Hyrule Castle", "Market", "Songs"))),
+ ("Song from Malon", ("Song", 0xFF, 0x27, (0x0D7EB53, 0x0D7EBCF), 'Eponas Song', ("Lon Lon Ranch", "Songs",))),
+ ("Song from Saria", ("Song", 0xFF, 0x28, (0x20B1DB1, 0x20B1DB1), 'Sarias Song', ("Sacred Forest Meadow", "Forest", "Songs"))),
+ ("Song from Composers Grave", ("Song", 0xFF, 0x29, (0x332A871, 0x332A871), 'Suns Song', ("the Graveyard", "Kakariko", "Songs"))),
+ ("Song from Ocarina of Time", ("Song", 0xFF, 0x2A, (0x252FC89, 0x252FC89), 'Song of Time', ("Hyrule Field", "Songs", "Need Spiritual Stones"))),
+ ("Song from Windmill", ("Song", 0xFF, 0x2B, (0x0E42C07, 0x0E42B8B), 'Song of Storms', ("Kakariko Village", "Kakariko", "Songs"))),
+ ("Sheik in Forest", ("Song", 0xFF, 0x20, (0x20B0809, 0x20B0809), 'Minuet of Forest', ("Sacred Forest Meadow", "Forest", "Songs"))),
+ ("Sheik in Crater", ("Song", 0xFF, 0x21, (0x224D7F1, 0x224D7F1), 'Bolero of Fire', ("Death Mountain Crater", "Death Mountain", "Songs"))),
+ ("Sheik in Ice Cavern", ("Song", 0xFF, 0x22, (0x2BEC889, 0x2BEC889), 'Serenade of Water', ("Ice Cavern", "Songs",))),
+ ("Sheik at Colossus", ("Song", 0xFF, 0x23, (0x218C57D, 0x218C57D), 'Requiem of Spirit', ("Desert Colossus", "Songs",))),
+ ("Sheik in Kakariko", ("Song", 0xFF, 0x24, (0x2000FE1, 0x2000FE1), 'Nocturne of Shadow', ("Kakariko Village", "Kakariko", "Songs"))),
+ ("Sheik at Temple", ("Song", 0xFF, 0x25, (0x2531329, 0x2531329), 'Prelude of Light', ("Temple of Time", "Market", "Songs"))),
+
+ ## Overworld
+ # Kokiri Forest
+ ("KF Midos Top Left Chest", ("Chest", 0x28, 0x00, None, 'Rupees (5)', ("Kokiri Forest", "Forest",))),
+ ("KF Midos Top Right Chest", ("Chest", 0x28, 0x01, None, 'Rupees (5)', ("Kokiri Forest", "Forest",))),
+ ("KF Midos Bottom Left Chest", ("Chest", 0x28, 0x02, None, 'Rupee (1)', ("Kokiri Forest", "Forest",))),
+ ("KF Midos Bottom Right Chest", ("Chest", 0x28, 0x03, None, 'Recovery Heart', ("Kokiri Forest", "Forest",))),
+ ("KF Kokiri Sword Chest", ("Chest", 0x55, 0x00, None, 'Kokiri Sword', ("Kokiri Forest", "Forest",))),
+ ("KF Storms Grotto Chest", ("Chest", 0x3E, 0x0C, None, 'Rupees (20)', ("Kokiri Forest", "Forest", "Grottos"))),
+ ("KF Links House Cow", ("NPC", 0x34, 0x15, None, 'Milk', ("KF Links House", "Forest", "Cow", "Minigames"))),
+ ("KF GS Know It All House", ("GS Token", 0x0C, 0x02, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
+ ("KF GS Bean Patch", ("GS Token", 0x0C, 0x01, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
+ ("KF GS House of Twins", ("GS Token", 0x0C, 0x04, None, 'Gold Skulltula Token', ("Kokiri Forest", "Skulltulas",))),
+ ("KF Shop Item 1", ("Shop", 0x2D, 0x30, (shop_address(0, 0), None), 'Buy Deku Shield', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 2", ("Shop", 0x2D, 0x31, (shop_address(0, 1), None), 'Buy Deku Nut (5)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 3", ("Shop", 0x2D, 0x32, (shop_address(0, 2), None), 'Buy Deku Nut (10)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 4", ("Shop", 0x2D, 0x33, (shop_address(0, 3), None), 'Buy Deku Stick (1)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 5", ("Shop", 0x2D, 0x34, (shop_address(0, 4), None), 'Buy Deku Seeds (30)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 6", ("Shop", 0x2D, 0x35, (shop_address(0, 5), None), 'Buy Arrows (10)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 7", ("Shop", 0x2D, 0x36, (shop_address(0, 6), None), 'Buy Arrows (30)', ("Kokiri Forest", "Forest", "Shops"))),
+ ("KF Shop Item 8", ("Shop", 0x2D, 0x37, (shop_address(0, 7), None), 'Buy Heart', ("Kokiri Forest", "Forest", "Shops"))),
+
+ # Lost Woods
+ ("LW Gift from Saria", ("Cutscene", 0xFF, 0x02, None, 'Ocarina', ("the Lost Woods", "Forest",))),
+ ("LW Ocarina Memory Game", ("NPC", 0x5B, 0x76, None, 'Piece of Heart', ("the Lost Woods", "Forest", "Minigames"))),
+ ("LW Target in Woods", ("NPC", 0x5B, 0x60, None, 'Slingshot', ("the Lost Woods", "Forest",))),
+ ("LW Near Shortcuts Grotto Chest", ("Chest", 0x3E, 0x14, None, 'Rupees (5)', ("the Lost Woods", "Forest", "Grottos"))),
+ ("Deku Theater Skull Mask", ("NPC", 0x3E, 0x77, None, 'Deku Stick Capacity', ("the Lost Woods", "Forest", "Grottos"))),
+ ("Deku Theater Mask of Truth", ("NPC", 0x3E, 0x7A, None, 'Deku Nut Capacity', ("the Lost Woods", "Forest", "Need Spiritual Stones", "Grottos"))),
+ ("LW Skull Kid", ("NPC", 0x5B, 0x3E, None, 'Piece of Heart', ("the Lost Woods", "Forest",))),
+ ("LW Deku Scrub Near Bridge", ("NPC", 0x5B, 0x77, None, 'Deku Stick Capacity', ("the Lost Woods", "Forest", "Deku Scrub", "Deku Scrub Upgrades"))),
+ ("LW Deku Scrub Near Deku Theater Left", ("NPC", 0x5B, 0x31, None, 'Buy Deku Stick (1)', ("the Lost Woods", "Forest", "Deku Scrub"))),
+ ("LW Deku Scrub Near Deku Theater Right", ("NPC", 0x5B, 0x30, None, 'Buy Deku Nut (5)', ("the Lost Woods", "Forest", "Deku Scrub"))),
+ ("LW Deku Scrub Grotto Front", ("GrottoNPC", 0xF5, 0x79, None, 'Deku Nut Capacity', ("the Lost Woods", "Forest", "Deku Scrub", "Deku Scrub Upgrades", "Grottos"))),
+ ("LW Deku Scrub Grotto Rear", ("GrottoNPC", 0xF5, 0x33, None, 'Buy Deku Seeds (30)', ("the Lost Woods", "Forest", "Deku Scrub", "Grottos"))),
+ ("LW GS Bean Patch Near Bridge", ("GS Token", 0x0D, 0x01, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
+ ("LW GS Bean Patch Near Theater", ("GS Token", 0x0D, 0x02, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
+ ("LW GS Above Theater", ("GS Token", 0x0D, 0x04, None, 'Gold Skulltula Token', ("the Lost Woods", "Skulltulas",))),
+
+ # Sacred Forest Meadow
+ ("SFM Wolfos Grotto Chest", ("Chest", 0x3E, 0x11, None, 'Rupees (50)', ("Sacred Forest Meadow", "Forest", "Grottos"))),
+ ("SFM Deku Scrub Grotto Front", ("GrottoNPC", 0xEE, 0x3A, None, 'Buy Green Potion', ("Sacred Forest Meadow", "Forest", "Deku Scrub", "Grottos"))),
+ ("SFM Deku Scrub Grotto Rear", ("GrottoNPC", 0xEE, 0x39, None, 'Buy Red Potion [30]', ("Sacred Forest Meadow", "Forest", "Deku Scrub", "Grottos"))),
+ ("SFM GS", ("GS Token", 0x0D, 0x08, None, 'Gold Skulltula Token', ("Sacred Forest Meadow", "Skulltulas",))),
+
+ # Hyrule Field
+ ("HF Ocarina of Time Item", ("NPC", 0x51, 0x0C, None, 'Ocarina', ("Hyrule Field", "Need Spiritual Stones",))),
+ ("HF Near Market Grotto Chest", ("Chest", 0x3E, 0x00, None, 'Rupees (5)', ("Hyrule Field", "Grottos",))),
+ ("HF Tektite Grotto Freestanding PoH", ("Collectable", 0x3E, 0x01, None, 'Piece of Heart', ("Hyrule Field", "Grottos",))),
+ ("HF Southeast Grotto Chest", ("Chest", 0x3E, 0x02, None, 'Rupees (20)', ("Hyrule Field", "Grottos",))),
+ ("HF Open Grotto Chest", ("Chest", 0x3E, 0x03, None, 'Rupees (5)', ("Hyrule Field", "Grottos",))),
+ ("HF Deku Scrub Grotto", ("GrottoNPC", 0xE6, 0x3E, None, 'Piece of Heart', ("Hyrule Field", "Deku Scrub", "Deku Scrub Upgrades", "Grottos"))),
+ ("HF Cow Grotto Cow", ("NPC", 0x3E, 0x16, None, 'Milk', ("Hyrule Field", "Cow", "Grottos"))),
+ ("HF GS Cow Grotto", ("GS Token", 0x0A, 0x01, None, 'Gold Skulltula Token', ("Hyrule Field", "Skulltulas", "Grottos"))),
+ ("HF GS Near Kak Grotto", ("GS Token", 0x0A, 0x02, None, 'Gold Skulltula Token', ("Hyrule Field", "Skulltulas", "Grottos"))),
+
+ # Market
+ ("Market Shooting Gallery Reward", ("NPC", 0x42, 0x60, None, 'Slingshot', ("the Market", "Market", "Minigames"))),
+ ("Market Bombchu Bowling First Prize", ("NPC", 0x4B, 0x34, None, 'Bomb Bag', ("the Market", "Market", "Minigames"))),
+ ("Market Bombchu Bowling Second Prize", ("NPC", 0x4B, 0x3E, None, 'Piece of Heart', ("the Market", "Market", "Minigames"))),
+ ("Market Bombchu Bowling Bombchus", ("Event", 0x4B, None, None, 'Bombchu Drop', ("the Market", "Market", "Minigames"))),
+ ("Market Lost Dog", ("NPC", 0x35, 0x3E, None, 'Piece of Heart', ("the Market", "Market",))),
+ ("Market Treasure Chest Game Reward", ("Chest", 0x10, 0x0A, None, 'Piece of Heart (Treasure Chest Game)', ("the Market", "Market", "Minigames"))),
+ ("Market 10 Big Poes", ("NPC", 0x4D, 0x0F, None, 'Bottle', ("the Market", "Hyrule Castle",))),
+ ("Market GS Guard House", ("GS Token", 0x0E, 0x08, None, 'Gold Skulltula Token', ("the Market", "Skulltulas",))),
+ ("Market Bazaar Item 1", ("Shop", 0x2C, 0x30, (shop_address(4, 0), None), 'Buy Hylian Shield', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 2", ("Shop", 0x2C, 0x31, (shop_address(4, 1), None), 'Buy Bombs (5) [35]', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 3", ("Shop", 0x2C, 0x32, (shop_address(4, 2), None), 'Buy Deku Nut (5)', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 4", ("Shop", 0x2C, 0x33, (shop_address(4, 3), None), 'Buy Heart', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 5", ("Shop", 0x2C, 0x34, (shop_address(4, 4), None), 'Buy Arrows (10)', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 6", ("Shop", 0x2C, 0x35, (shop_address(4, 5), None), 'Buy Arrows (50)', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 7", ("Shop", 0x2C, 0x36, (shop_address(4, 6), None), 'Buy Deku Stick (1)', ("the Market", "Market", "Shops"))),
+ ("Market Bazaar Item 8", ("Shop", 0x2C, 0x37, (shop_address(4, 7), None), 'Buy Arrows (30)', ("the Market", "Market", "Shops"))),
+
+ ("Market Potion Shop Item 1", ("Shop", 0x31, 0x30, (shop_address(3, 0), None), 'Buy Green Potion', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 2", ("Shop", 0x31, 0x31, (shop_address(3, 1), None), 'Buy Blue Fire', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 3", ("Shop", 0x31, 0x32, (shop_address(3, 2), None), 'Buy Red Potion [30]', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 4", ("Shop", 0x31, 0x33, (shop_address(3, 3), None), 'Buy Fairy\'s Spirit', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 5", ("Shop", 0x31, 0x34, (shop_address(3, 4), None), 'Buy Deku Nut (5)', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 6", ("Shop", 0x31, 0x35, (shop_address(3, 5), None), 'Buy Bottle Bug', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 7", ("Shop", 0x31, 0x36, (shop_address(3, 6), None), 'Buy Poe', ("the Market", "Market", "Shops"))),
+ ("Market Potion Shop Item 8", ("Shop", 0x31, 0x37, (shop_address(3, 7), None), 'Buy Fish', ("the Market", "Market", "Shops"))),
+
+ ("Market Bombchu Shop Item 1", ("Shop", 0x32, 0x30, (shop_address(2, 0), None), 'Buy Bombchu (5)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 2", ("Shop", 0x32, 0x31, (shop_address(2, 1), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 3", ("Shop", 0x32, 0x32, (shop_address(2, 2), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 4", ("Shop", 0x32, 0x33, (shop_address(2, 3), None), 'Buy Bombchu (10)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 5", ("Shop", 0x32, 0x34, (shop_address(2, 4), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 6", ("Shop", 0x32, 0x35, (shop_address(2, 5), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 7", ("Shop", 0x32, 0x36, (shop_address(2, 6), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
+ ("Market Bombchu Shop Item 8", ("Shop", 0x32, 0x37, (shop_address(2, 7), None), 'Buy Bombchu (20)', ("the Market", "Market", "Shops"))),
+
+ ("ToT Light Arrows Cutscene", ("Cutscene", 0xFF, 0x01, None, 'Light Arrows', ("Temple of Time", "Market",))),
+
+ # Hyrule Castle
+ ("HC Malon Egg", ("NPC", 0x5F, 0x47, None, 'Weird Egg', ("Hyrule Castle", "Market",))),
+ ("HC Zeldas Letter", ("NPC", 0x4A, 0x0B, None, 'Zeldas Letter', ("Hyrule Castle", "Market",))),
+ ("HC Great Fairy Reward", ("Cutscene", 0xFF, 0x11, None, 'Dins Fire', ("Hyrule Castle", "Market", "Fairies"))),
+ ("HC GS Tree", ("GS Token", 0x0E, 0x04, None, 'Gold Skulltula Token', ("Hyrule Castle", "Skulltulas",))),
+ ("HC GS Storms Grotto", ("GS Token", 0x0E, 0x02, None, 'Gold Skulltula Token', ("Hyrule Castle", "Skulltulas", "Grottos"))),
+
+ # Lon Lon Ranch
+ ("LLR Talons Chickens", ("NPC", 0x4C, 0x14, None, 'Bottle with Milk', ("Lon Lon Ranch", "Kakariko", "Minigames"))),
+ ("LLR Freestanding PoH", ("Collectable", 0x4C, 0x01, None, 'Piece of Heart', ("Lon Lon Ranch",))),
+ ("LLR Deku Scrub Grotto Left", ("GrottoNPC", 0xFC, 0x30, None, 'Buy Deku Nut (5)', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
+ ("LLR Deku Scrub Grotto Center", ("GrottoNPC", 0xFC, 0x33, None, 'Buy Deku Seeds (30)', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
+ ("LLR Deku Scrub Grotto Right", ("GrottoNPC", 0xFC, 0x37, None, 'Buy Bombs (5) [35]', ("Lon Lon Ranch", "Deku Scrub", "Grottos"))),
+ ("LLR Stables Left Cow", ("NPC", 0x36, 0x15, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
+ ("LLR Stables Right Cow", ("NPC", 0x36, 0x16, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
+ ("LLR Tower Left Cow", ("NPC", 0x4C, 0x16, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
+ ("LLR Tower Right Cow", ("NPC", 0x4C, 0x15, None, 'Milk', ("Lon Lon Ranch", "Cow",))),
+ ("LLR GS House Window", ("GS Token", 0x0B, 0x04, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
+ ("LLR GS Tree", ("GS Token", 0x0B, 0x08, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
+ ("LLR GS Rain Shed", ("GS Token", 0x0B, 0x02, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
+ ("LLR GS Back Wall", ("GS Token", 0x0B, 0x01, None, 'Gold Skulltula Token', ("Lon Lon Ranch", "Skulltulas",))),
+
+ # Kakariko
+ ("Kak Anju as Child", ("NPC", 0x52, 0x0F, None, 'Bottle', ("Kakariko Village", "Kakariko", "Minigames"))),
+ ("Kak Anju as Adult", ("NPC", 0x52, 0x1D, None, 'Pocket Egg', ("Kakariko Village", "Kakariko",))),
+ ("Kak Impas House Freestanding PoH", ("Collectable", 0x37, 0x01, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
+ ("Kak Windmill Freestanding PoH", ("Collectable", 0x48, 0x01, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
+ ("Kak Man on Roof", ("NPC", 0x52, 0x3E, None, 'Piece of Heart', ("Kakariko Village", "Kakariko",))),
+ ("Kak Open Grotto Chest", ("Chest", 0x3E, 0x08, None, 'Rupees (20)', ("Kakariko Village", "Kakariko", "Grottos"))),
+ ("Kak Redead Grotto Chest", ("Chest", 0x3E, 0x0A, None, 'Rupees (200)', ("Kakariko Village", "Kakariko", "Grottos"))),
+ ("Kak Shooting Gallery Reward", ("NPC", 0x42, 0x30, None, 'Bow', ("Kakariko Village", "Kakariko", "Minigames"))),
+ ("Kak 10 Gold Skulltula Reward", ("NPC", 0x50, 0x45, None, 'Progressive Wallet', ("Kakariko Village", "Kakariko", "Skulltula House"))),
+ ("Kak 20 Gold Skulltula Reward", ("NPC", 0x50, 0x39, None, 'Stone of Agony', ("Kakariko Village", "Kakariko", "Skulltula House"))),
+ ("Kak 30 Gold Skulltula Reward", ("NPC", 0x50, 0x46, None, 'Progressive Wallet', ("Kakariko Village", "Kakariko", "Skulltula House"))),
+ ("Kak 40 Gold Skulltula Reward", ("NPC", 0x50, 0x03, None, 'Bombchus (10)', ("Kakariko Village", "Kakariko", "Skulltula House"))),
+ ("Kak 50 Gold Skulltula Reward", ("NPC", 0x50, 0x3E, None, 'Piece of Heart', ("Kakariko Village", "Kakariko", "Skulltula House"))),
+ ("Kak Impas House Cow", ("NPC", 0x37, 0x15, None, 'Milk', ("Kakariko Village", "Kakariko", "Cow"))),
+ ("Kak GS Tree", ("GS Token", 0x10, 0x20, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak GS Guards House", ("GS Token", 0x10, 0x02, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak GS Watchtower", ("GS Token", 0x10, 0x04, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak GS Skulltula House", ("GS Token", 0x10, 0x10, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak GS House Under Construction", ("GS Token", 0x10, 0x08, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak GS Above Impas House", ("GS Token", 0x10, 0x40, None, 'Gold Skulltula Token', ("Kakariko Village", "Skulltulas",))),
+ ("Kak Bazaar Item 1", ("Shop", 0x2C, 0x38, (shop_address(5, 0), None), 'Buy Hylian Shield', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 2", ("Shop", 0x2C, 0x39, (shop_address(5, 1), None), 'Buy Bombs (5) [35]', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 3", ("Shop", 0x2C, 0x3A, (shop_address(5, 2), None), 'Buy Deku Nut (5)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 4", ("Shop", 0x2C, 0x3B, (shop_address(5, 3), None), 'Buy Heart', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 5", ("Shop", 0x2C, 0x3D, (shop_address(5, 4), None), 'Buy Arrows (10)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 6", ("Shop", 0x2C, 0x3E, (shop_address(5, 5), None), 'Buy Arrows (50)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 7", ("Shop", 0x2C, 0x3F, (shop_address(5, 6), None), 'Buy Deku Stick (1)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Bazaar Item 8", ("Shop", 0x2C, 0x40, (shop_address(5, 7), None), 'Buy Arrows (30)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 1", ("Shop", 0x30, 0x30, (shop_address(1, 0), None), 'Buy Deku Nut (5)', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 2", ("Shop", 0x30, 0x31, (shop_address(1, 1), None), 'Buy Fish', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 3", ("Shop", 0x30, 0x32, (shop_address(1, 2), None), 'Buy Red Potion [30]', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 4", ("Shop", 0x30, 0x33, (shop_address(1, 3), None), 'Buy Green Potion', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 5", ("Shop", 0x30, 0x34, (shop_address(1, 4), None), 'Buy Blue Fire', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 6", ("Shop", 0x30, 0x35, (shop_address(1, 5), None), 'Buy Bottle Bug', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 7", ("Shop", 0x30, 0x36, (shop_address(1, 6), None), 'Buy Poe', ("Kakariko Village", "Kakariko", "Shops"))),
+ ("Kak Potion Shop Item 8", ("Shop", 0x30, 0x37, (shop_address(1, 7), None), 'Buy Fairy\'s Spirit', ("Kakariko Village", "Kakariko", "Shops"))),
+
+ # Graveyard
+ ("Graveyard Shield Grave Chest", ("Chest", 0x40, 0x00, None, 'Hylian Shield', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Heart Piece Grave Chest", ("Chest", 0x3F, 0x00, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Composers Grave Chest", ("Chest", 0x41, 0x00, None, 'Bombs (5)', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Freestanding PoH", ("Collectable", 0x53, 0x04, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Dampe Gravedigging Tour", ("Collectable", 0x53, 0x08, None, 'Piece of Heart', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Hookshot Chest", ("Chest", 0x48, 0x00, None, 'Progressive Hookshot', ("the Graveyard", "Kakariko",))),
+ ("Graveyard Dampe Race Freestanding PoH", ("Collectable", 0x48, 0x07, None, 'Piece of Heart', ("the Graveyard", "Kakariko", "Minigames"))),
+ ("Graveyard GS Bean Patch", ("GS Token", 0x10, 0x01, None, 'Gold Skulltula Token', ("the Graveyard", "Skulltulas",))),
+ ("Graveyard GS Wall", ("GS Token", 0x10, 0x80, None, 'Gold Skulltula Token', ("the Graveyard", "Skulltulas",))),
+
+ # Death Mountain Trail
+ ("DMT Freestanding PoH", ("Collectable", 0x60, 0x1E, None, 'Piece of Heart', ("Death Mountain Trail", "Death Mountain",))),
+ ("DMT Chest", ("Chest", 0x60, 0x01, None, 'Rupees (50)', ("Death Mountain Trail", "Death Mountain",))),
+ ("DMT Storms Grotto Chest", ("Chest", 0x3E, 0x17, None, 'Rupees (200)', ("Death Mountain Trail", "Death Mountain", "Grottos"))),
+ ("DMT Great Fairy Reward", ("Cutscene", 0xFF, 0x13, None, 'Magic Meter', ("Death Mountain Trail", "Death Mountain", "Fairies"))),
+ ("DMT Biggoron", ("NPC", 0x60, 0x57, None, 'Biggoron Sword', ("Death Mountain Trail", "Death Mountain",))),
+ ("DMT Cow Grotto Cow", ("NPC", 0x3E, 0x15, None, 'Milk', ("Death Mountain Trail", "Death Mountain", "Cow", "Grottos"))),
+ ("DMT GS Near Kak", ("GS Token", 0x0F, 0x04, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
+ ("DMT GS Bean Patch", ("GS Token", 0x0F, 0x02, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
+ ("DMT GS Above Dodongos Cavern", ("GS Token", 0x0F, 0x08, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
+ ("DMT GS Falling Rocks Path", ("GS Token", 0x0F, 0x10, None, 'Gold Skulltula Token', ("Death Mountain Trail", "Skulltulas",))),
+
+ # Goron City
+ ("GC Darunias Joy", ("NPC", 0x62, 0x54, None, 'Progressive Strength Upgrade', ("Goron City",))),
+ ("GC Pot Freestanding PoH", ("Collectable", 0x62, 0x1F, None, 'Piece of Heart', ("Goron City", "Goron City",))),
+ ("GC Rolling Goron as Child", ("NPC", 0x62, 0x34, None, 'Bomb Bag', ("Goron City",))),
+ ("GC Rolling Goron as Adult", ("NPC", 0x62, 0x2C, None, 'Goron Tunic', ("Goron City",))),
+ ("GC Medigoron", ("NPC", 0x62, 0x28, None, 'Giants Knife', ("Goron City",))),
+ ("GC Maze Left Chest", ("Chest", 0x62, 0x00, None, 'Rupees (200)', ("Goron City",))),
+ ("GC Maze Right Chest", ("Chest", 0x62, 0x01, None, 'Rupees (50)', ("Goron City",))),
+ ("GC Maze Center Chest", ("Chest", 0x62, 0x02, None, 'Rupees (50)', ("Goron City",))),
+ ("GC Deku Scrub Grotto Left", ("GrottoNPC", 0xFB, 0x30, None, 'Buy Deku Nut (5)', ("Goron City", "Deku Scrub", "Grottos"))),
+ ("GC Deku Scrub Grotto Center", ("GrottoNPC", 0xFB, 0x33, None, 'Buy Arrows (30)', ("Goron City", "Deku Scrub", "Grottos"))),
+ ("GC Deku Scrub Grotto Right", ("GrottoNPC", 0xFB, 0x37, None, 'Buy Bombs (5) [35]', ("Goron City", "Deku Scrub", "Grottos"))),
+ ("GC GS Center Platform", ("GS Token", 0x0F, 0x20, None, 'Gold Skulltula Token', ("Goron City", "Skulltulas",))),
+ ("GC GS Boulder Maze", ("GS Token", 0x0F, 0x40, None, 'Gold Skulltula Token', ("Goron City", "Skulltulas",))),
+ ("GC Shop Item 1", ("Shop", 0x2E, 0x30, (shop_address(8, 0), None), 'Buy Bombs (5) [25]', ("Goron City", "Shops",))),
+ ("GC Shop Item 2", ("Shop", 0x2E, 0x31, (shop_address(8, 1), None), 'Buy Bombs (10)', ("Goron City", "Shops",))),
+ ("GC Shop Item 3", ("Shop", 0x2E, 0x32, (shop_address(8, 2), None), 'Buy Bombs (20)', ("Goron City", "Shops",))),
+ ("GC Shop Item 4", ("Shop", 0x2E, 0x33, (shop_address(8, 3), None), 'Buy Bombs (30)', ("Goron City", "Shops",))),
+ ("GC Shop Item 5", ("Shop", 0x2E, 0x34, (shop_address(8, 4), None), 'Buy Goron Tunic', ("Goron City", "Shops",))),
+ ("GC Shop Item 6", ("Shop", 0x2E, 0x35, (shop_address(8, 5), None), 'Buy Heart', ("Goron City", "Shops",))),
+ ("GC Shop Item 7", ("Shop", 0x2E, 0x36, (shop_address(8, 6), None), 'Buy Red Potion [40]', ("Goron City", "Shops",))),
+ ("GC Shop Item 8", ("Shop", 0x2E, 0x37, (shop_address(8, 7), None), 'Buy Heart', ("Goron City", "Shops",))),
+
+ # Death Mountain Crater
+ ("DMC Volcano Freestanding PoH", ("Collectable", 0x61, 0x08, None, 'Piece of Heart', ("Death Mountain Crater", "Death Mountain",))),
+ ("DMC Wall Freestanding PoH", ("Collectable", 0x61, 0x02, None, 'Piece of Heart', ("Death Mountain Crater", "Death Mountain",))),
+ ("DMC Upper Grotto Chest", ("Chest", 0x3E, 0x1A, None, 'Bombs (20)', ("Death Mountain Crater", "Death Mountain", "Grottos"))),
+ ("DMC Great Fairy Reward", ("Cutscene", 0xFF, 0x14, None, 'Magic Meter', ("Death Mountain Crater", "Death Mountain", "Fairies",))),
+ ("DMC Deku Scrub", ("NPC", 0x61, 0x37, None, 'Buy Bombs (5) [35]', ("Death Mountain Crater", "Death Mountain", "Deku Scrub"))),
+ ("DMC Deku Scrub Grotto Left", ("GrottoNPC", 0xF9, 0x30, None, 'Buy Deku Nut (5)', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
+ ("DMC Deku Scrub Grotto Center", ("GrottoNPC", 0xF9, 0x33, None, 'Buy Arrows (30)', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
+ ("DMC Deku Scrub Grotto Right", ("GrottoNPC", 0xF9, 0x37, None, 'Buy Bombs (5) [35]', ("Death Mountain Crater", "Death Mountain", "Deku Scrub", "Grottos"))),
+ ("DMC GS Crate", ("GS Token", 0x0F, 0x80, None, 'Gold Skulltula Token', ("Death Mountain Crater", "Skulltulas",))),
+ ("DMC GS Bean Patch", ("GS Token", 0x0F, 0x01, None, 'Gold Skulltula Token', ("Death Mountain Crater", "Skulltulas",))),
+
+ # Zora's River
+ ("ZR Magic Bean Salesman", ("NPC", 0x54, 0x16, None, 'Magic Bean', ("Zora's River",))),
+ ("ZR Open Grotto Chest", ("Chest", 0x3E, 0x09, None, 'Rupees (20)', ("Zora's River", "Grottos",))),
+ ("ZR Frogs in the Rain", ("NPC", 0x54, 0x3E, None, 'Piece of Heart', ("Zora's River", "Minigames",))),
+ ("ZR Frogs Ocarina Game", ("NPC", 0x54, 0x76, None, 'Piece of Heart', ("Zora's River",))),
+ ("ZR Near Open Grotto Freestanding PoH", ("Collectable", 0x54, 0x04, None, 'Piece of Heart', ("Zora's River",))),
+ ("ZR Near Domain Freestanding PoH", ("Collectable", 0x54, 0x0B, None, 'Piece of Heart', ("Zora's River",))),
+ ("ZR Deku Scrub Grotto Front", ("GrottoNPC", 0xEB, 0x3A, None, 'Buy Green Potion', ("Zora's River", "Deku Scrub", "Grottos"))),
+ ("ZR Deku Scrub Grotto Rear", ("GrottoNPC", 0xEB, 0x39, None, 'Buy Red Potion [30]', ("Zora's River", "Deku Scrub", "Grottos"))),
+ ("ZR GS Tree", ("GS Token", 0x11, 0x02, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
+ ("ZR GS Ladder", ("GS Token", 0x11, 0x01, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
+ ("ZR GS Near Raised Grottos", ("GS Token", 0x11, 0x10, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
+ ("ZR GS Above Bridge", ("GS Token", 0x11, 0x08, None, 'Gold Skulltula Token', ("Zora's River", "Skulltulas",))),
+
+ # Zora's Domain
+ ("ZD Diving Minigame", ("NPC", 0x58, 0x37, None, 'Progressive Scale', ("Zora's Domain", "Minigames",))),
+ ("ZD Chest", ("Chest", 0x58, 0x00, None, 'Piece of Heart', ("Zora's Domain", ))),
+ ("ZD King Zora Thawed", ("NPC", 0x58, 0x2D, None, 'Zora Tunic', ("Zora's Domain",))),
+ ("ZD GS Frozen Waterfall", ("GS Token", 0x11, 0x40, None, 'Gold Skulltula Token', ("Zora's Domain", "Skulltulas",))),
+ ("ZD Shop Item 1", ("Shop", 0x2F, 0x30, (shop_address(7, 0), None), 'Buy Zora Tunic', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 2", ("Shop", 0x2F, 0x31, (shop_address(7, 1), None), 'Buy Arrows (10)', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 3", ("Shop", 0x2F, 0x32, (shop_address(7, 2), None), 'Buy Heart', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 4", ("Shop", 0x2F, 0x33, (shop_address(7, 3), None), 'Buy Arrows (30)', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 5", ("Shop", 0x2F, 0x34, (shop_address(7, 4), None), 'Buy Deku Nut (5)', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 6", ("Shop", 0x2F, 0x35, (shop_address(7, 5), None), 'Buy Arrows (50)', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 7", ("Shop", 0x2F, 0x36, (shop_address(7, 6), None), 'Buy Fish', ("Zora's Domain", "Shops",))),
+ ("ZD Shop Item 8", ("Shop", 0x2F, 0x37, (shop_address(7, 7), None), 'Buy Red Potion [50]', ("Zora's Domain", "Shops",))),
+
+ # Zora's Fountain
+ ("ZF Great Fairy Reward", ("Cutscene", 0xFF, 0x10, None, 'Farores Wind', ("Zora's Fountain", "Fairies",))),
+ ("ZF Iceberg Freestanding PoH", ("Collectable", 0x59, 0x01, None, 'Piece of Heart', ("Zora's Fountain",))),
+ ("ZF Bottom Freestanding PoH", ("Collectable", 0x59, 0x14, None, 'Piece of Heart', ("Zora's Fountain",))),
+ ("ZF GS Above the Log", ("GS Token", 0x11, 0x04, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
+ ("ZF GS Tree", ("GS Token", 0x11, 0x80, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
+ ("ZF GS Hidden Cave", ("GS Token", 0x11, 0x20, None, 'Gold Skulltula Token', ("Zora's Fountain", "Skulltulas",))),
+
+ # Lake Hylia
+ ("LH Underwater Item", ("NPC", 0x57, 0x15, None, 'Rutos Letter', ("Lake Hylia",))),
+ ("LH Child Fishing", ("NPC", 0x49, 0x3E, None, 'Piece of Heart', ("Lake Hylia", "Minigames",))),
+ ("LH Adult Fishing", ("NPC", 0x49, 0x38, None, 'Progressive Scale', ("Lake Hylia", "Minigames",))),
+ ("LH Lab Dive", ("NPC", 0x38, 0x3E, None, 'Piece of Heart', ("Lake Hylia",))),
+ ("LH Freestanding PoH", ("Collectable", 0x57, 0x1E, None, 'Piece of Heart', ("Lake Hylia",))),
+ ("LH Sun", ("NPC", 0x57, 0x58, None, 'Fire Arrows', ("Lake Hylia",))),
+ ("LH Deku Scrub Grotto Left", ("GrottoNPC", 0xEF, 0x30, None, 'Buy Deku Nut (5)', ("Lake Hylia", "Deku Scrub", "Grottos"))),
+ ("LH Deku Scrub Grotto Center", ("GrottoNPC", 0xEF, 0x33, None, 'Buy Deku Seeds (30)', ("Lake Hylia", "Deku Scrub", "Grottos"))),
+ ("LH Deku Scrub Grotto Right", ("GrottoNPC", 0xEF, 0x37, None, 'Buy Bombs (5) [35]', ("Lake Hylia", "Deku Scrub", "Grottos"))),
+ ("LH GS Bean Patch", ("GS Token", 0x12, 0x01, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
+ ("LH GS Lab Wall", ("GS Token", 0x12, 0x04, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
+ ("LH GS Small Island", ("GS Token", 0x12, 0x02, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
+ ("LH GS Lab Crate", ("GS Token", 0x12, 0x08, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
+ ("LH GS Tree", ("GS Token", 0x12, 0x10, None, 'Gold Skulltula Token', ("Lake Hylia", "Skulltulas",))),
+
+ # Gerudo Valley
+ ("GV Crate Freestanding PoH", ("Collectable", 0x5A, 0x02, None, 'Piece of Heart', ("Gerudo Valley", "Gerudo",))),
+ ("GV Waterfall Freestanding PoH", ("Collectable", 0x5A, 0x01, None, 'Piece of Heart', ("Gerudo Valley", "Gerudo",))),
+ ("GV Chest", ("Chest", 0x5A, 0x00, None, 'Rupees (50)', ("Gerudo Valley", "Gerudo",))),
+ ("GV Deku Scrub Grotto Front", ("GrottoNPC", 0xF0, 0x3A, None, 'Buy Green Potion', ("Gerudo Valley", "Gerudo", "Deku Scrub", "Grottos"))),
+ ("GV Deku Scrub Grotto Rear", ("GrottoNPC", 0xF0, 0x39, None, 'Buy Red Potion [30]', ("Gerudo Valley", "Gerudo", "Deku Scrub", "Grottos"))),
+ ("GV Cow", ("NPC", 0x5A, 0x15, None, 'Milk', ("Gerudo Valley", "Gerudo", "Cow"))),
+ ("GV GS Small Bridge", ("GS Token", 0x13, 0x02, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
+ ("GV GS Bean Patch", ("GS Token", 0x13, 0x01, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
+ ("GV GS Behind Tent", ("GS Token", 0x13, 0x08, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
+ ("GV GS Pillar", ("GS Token", 0x13, 0x04, None, 'Gold Skulltula Token', ("Gerudo Valley", "Skulltulas",))),
+
+ # Gerudo's Fortress
+ ("GF North F1 Carpenter", ("Collectable", 0x0C, 0x0C, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF North F2 Carpenter", ("Collectable", 0x0C, 0x0A, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF South F1 Carpenter", ("Collectable", 0x0C, 0x0E, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF South F2 Carpenter", ("Collectable", 0x0C, 0x0F, None, 'Small Key (Gerudo Fortress)', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF Gerudo Membership Card", ("NPC", 0x0C, 0x3A, None, 'Gerudo Membership Card', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF Chest", ("Chest", 0x5D, 0x00, None, 'Piece of Heart', ("Gerudo's Fortress", "Gerudo",))),
+ ("GF HBA 1000 Points", ("NPC", 0x5D, 0x3E, None, 'Piece of Heart', ("Gerudo's Fortress", "Gerudo", "Minigames"))),
+ ("GF HBA 1500 Points", ("NPC", 0x5D, 0x30, None, 'Bow', ("Gerudo's Fortress", "Gerudo", "Minigames"))),
+ ("GF GS Top Floor", ("GS Token", 0x14, 0x02, None, 'Gold Skulltula Token', ("Gerudo's Fortress", "Skulltulas",))),
+ ("GF GS Archery Range", ("GS Token", 0x14, 0x01, None, 'Gold Skulltula Token', ("Gerudo's Fortress", "Skulltulas",))),
+
+ # Wasteland
+ ("Wasteland Bombchu Salesman", ("NPC", 0x5E, 0x03, None, 'Bombchus (10)', ("Haunted Wasteland",))),
+ ("Wasteland Chest", ("Chest", 0x5E, 0x00, None, 'Rupees (50)', ("Haunted Wasteland",))),
+ ("Wasteland GS", ("GS Token", 0x15, 0x02, None, 'Gold Skulltula Token', ("Haunted Wasteland", "Skulltulas",))),
+
+ # Colossus
+ ("Colossus Great Fairy Reward", ("Cutscene", 0xFF, 0x12, None, 'Nayrus Love', ("Desert Colossus", "Fairies",))),
+ ("Colossus Freestanding PoH", ("Collectable", 0x5C, 0x0D, None, 'Piece of Heart', ("Desert Colossus",))),
+ ("Colossus Deku Scrub Grotto Front", ("GrottoNPC", 0xFD, 0x3A, None, 'Buy Green Potion', ("Desert Colossus", "Deku Scrub", "Grottos"))),
+ ("Colossus Deku Scrub Grotto Rear", ("GrottoNPC", 0xFD, 0x39, None, 'Buy Red Potion [30]', ("Desert Colossus", "Deku Scrub", "Grottos"))),
+ ("Colossus GS Bean Patch", ("GS Token", 0x15, 0x01, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
+ ("Colossus GS Tree", ("GS Token", 0x15, 0x08, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
+ ("Colossus GS Hill", ("GS Token", 0x15, 0x04, None, 'Gold Skulltula Token', ("Desert Colossus", "Skulltulas",))),
+
+ # Outside Ganon's Castle
+ ("OGC Great Fairy Reward", ("Cutscene", 0xFF, 0x15, None, 'Double Defense', ("outside Ganon's Castle", "Market", "Fairies"))),
+ ("OGC GS", ("GS Token", 0x0E, 0x01, None, 'Gold Skulltula Token', ("outside Ganon's Castle", "Skulltulas",))),
+
+ ## Dungeons
+ # Deku Tree vanilla
+ ("Deku Tree Map Chest", ("Chest", 0x00, 0x03, None, 'Map (Deku Tree)', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree Slingshot Room Side Chest", ("Chest", 0x00, 0x05, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree Slingshot Chest", ("Chest", 0x00, 0x01, None, 'Slingshot', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree Compass Chest", ("Chest", 0x00, 0x02, None, 'Compass (Deku Tree)', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree Compass Room Side Chest", ("Chest", 0x00, 0x06, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree Basement Chest", ("Chest", 0x00, 0x04, None, 'Recovery Heart', ("Deku Tree", "Vanilla",))),
+ ("Deku Tree GS Compass Room", ("GS Token", 0x00, 0x08, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
+ ("Deku Tree GS Basement Vines", ("GS Token", 0x00, 0x04, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
+ ("Deku Tree GS Basement Gate", ("GS Token", 0x00, 0x02, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
+ ("Deku Tree GS Basement Back Room", ("GS Token", 0x00, 0x01, None, 'Gold Skulltula Token', ("Deku Tree", "Vanilla", "Skulltulas",))),
+ # Deku Tree MQ
+ ("Deku Tree MQ Map Chest", ("Chest", 0x00, 0x03, None, 'Map (Deku Tree)', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Slingshot Chest", ("Chest", 0x00, 0x06, None, 'Slingshot', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Slingshot Room Back Chest", ("Chest", 0x00, 0x02, None, 'Deku Shield', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Compass Chest", ("Chest", 0x00, 0x01, None, 'Compass (Deku Tree)', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Basement Chest", ("Chest", 0x00, 0x04, None, 'Deku Shield', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Before Spinning Log Chest", ("Chest", 0x00, 0x05, None, 'Recovery Heart', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ After Spinning Log Chest", ("Chest", 0x00, 0x00, None, 'Rupees (50)', ("Deku Tree", "Master Quest",))),
+ ("Deku Tree MQ Deku Scrub", ("NPC", 0x00, 0x34, None, 'Buy Deku Shield', ("Deku Tree", "Master Quest", "Deku Scrub",))),
+ ("Deku Tree MQ GS Lobby", ("GS Token", 0x00, 0x02, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
+ ("Deku Tree MQ GS Compass Room", ("GS Token", 0x00, 0x08, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
+ ("Deku Tree MQ GS Basement Graves Room", ("GS Token", 0x00, 0x04, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
+ ("Deku Tree MQ GS Basement Back Room", ("GS Token", 0x00, 0x01, None, 'Gold Skulltula Token', ("Deku Tree", "Master Quest", "Skulltulas",))),
+ # Deku Tree shared
+ ("Deku Tree Queen Gohma Heart", ("BossHeart", 0x11, 0x4F, None, 'Heart Container', ("Deku Tree", "Vanilla", "Master Quest",))),
+
+ # Dodongo's Cavern vanilla
+ ("Dodongos Cavern Map Chest", ("Chest", 0x01, 0x08, None, 'Map (Dodongos Cavern)', ("Dodongo's Cavern", "Vanilla",))),
+ ("Dodongos Cavern Compass Chest", ("Chest", 0x01, 0x05, None, 'Compass (Dodongos Cavern)', ("Dodongo's Cavern", "Vanilla",))),
+ ("Dodongos Cavern Bomb Flower Platform Chest", ("Chest", 0x01, 0x06, None, 'Rupees (20)', ("Dodongo's Cavern", "Vanilla",))),
+ ("Dodongos Cavern Bomb Bag Chest", ("Chest", 0x01, 0x04, None, 'Bomb Bag', ("Dodongo's Cavern", "Vanilla",))),
+ ("Dodongos Cavern End of Bridge Chest", ("Chest", 0x01, 0x0A, None, 'Deku Shield', ("Dodongo's Cavern", "Vanilla",))),
+ ("Dodongos Cavern Deku Scrub Side Room Near Dodongos", ("NPC", 0x01, 0x31, None, 'Buy Deku Stick (1)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
+ ("Dodongos Cavern Deku Scrub Lobby", ("NPC", 0x01, 0x34, None, 'Buy Deku Shield', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
+ ("Dodongos Cavern Deku Scrub Near Bomb Bag Left", ("NPC", 0x01, 0x30, None, 'Buy Deku Nut (5)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
+ ("Dodongos Cavern Deku Scrub Near Bomb Bag Right", ("NPC", 0x01, 0x33, None, 'Buy Deku Seeds (30)', ("Dodongo's Cavern", "Vanilla", "Deku Scrub",))),
+ ("Dodongos Cavern GS Side Room Near Lower Lizalfos", ("GS Token", 0x01, 0x10, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
+ ("Dodongos Cavern GS Scarecrow", ("GS Token", 0x01, 0x02, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
+ ("Dodongos Cavern GS Alcove Above Stairs", ("GS Token", 0x01, 0x04, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
+ ("Dodongos Cavern GS Vines Above Stairs", ("GS Token", 0x01, 0x01, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
+ ("Dodongos Cavern GS Back Room", ("GS Token", 0x01, 0x08, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Vanilla", "Skulltulas",))),
+ # Dodongo's Cavern MQ
+ ("Dodongos Cavern MQ Map Chest", ("Chest", 0x01, 0x00, None, 'Map (Dodongos Cavern)', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Bomb Bag Chest", ("Chest", 0x01, 0x04, None, 'Bomb Bag', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Torch Puzzle Room Chest", ("Chest", 0x01, 0x03, None, 'Rupees (5)', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Larvae Room Chest", ("Chest", 0x01, 0x02, None, 'Deku Shield', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Compass Chest", ("Chest", 0x01, 0x05, None, 'Compass (Dodongos Cavern)', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Under Grave Chest", ("Chest", 0x01, 0x01, None, 'Hylian Shield', ("Dodongo's Cavern", "Master Quest",))),
+ ("Dodongos Cavern MQ Deku Scrub Lobby Front", ("NPC", 0x01, 0x33, None, 'Buy Deku Seeds (30)', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
+ ("Dodongos Cavern MQ Deku Scrub Lobby Rear", ("NPC", 0x01, 0x31, None, 'Buy Deku Stick (1)', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
+ ("Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos", ("NPC", 0x01, 0x39, None, 'Buy Red Potion [30]', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
+ ("Dodongos Cavern MQ Deku Scrub Staircase", ("NPC", 0x01, 0x34, None, 'Buy Deku Shield', ("Dodongo's Cavern", "Master Quest", "Deku Scrub",))),
+ ("Dodongos Cavern MQ GS Scrub Room", ("GS Token", 0x01, 0x02, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
+ ("Dodongos Cavern MQ GS Larvae Room", ("GS Token", 0x01, 0x10, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
+ ("Dodongos Cavern MQ GS Lizalfos Room", ("GS Token", 0x01, 0x04, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
+ ("Dodongos Cavern MQ GS Song of Time Block Room", ("GS Token", 0x01, 0x08, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
+ ("Dodongos Cavern MQ GS Back Area", ("GS Token", 0x01, 0x01, None, 'Gold Skulltula Token', ("Dodongo's Cavern", "Master Quest", "Skulltulas",))),
+ # Dodongo's Cavern shared
+ ("Dodongos Cavern Boss Room Chest", ("Chest", 0x12, 0x00, None, 'Bombs (5)', ("Dodongo's Cavern", "Vanilla", "Master Quest",))),
+ ("Dodongos Cavern King Dodongo Heart", ("BossHeart", 0x12, 0x4F, None, 'Heart Container', ("Dodongo's Cavern", "Vanilla", "Master Quest",))),
+
+ # Jabu Jabu's Belly vanilla
+ ("Jabu Jabus Belly Boomerang Chest", ("Chest", 0x02, 0x01, None, 'Boomerang', ("Jabu Jabu's Belly", "Vanilla",))),
+ ("Jabu Jabus Belly Map Chest", ("Chest", 0x02, 0x02, None, 'Map (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Vanilla",))),
+ ("Jabu Jabus Belly Compass Chest", ("Chest", 0x02, 0x04, None, 'Compass (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Vanilla",))),
+ ("Jabu Jabus Belly Deku Scrub", ("NPC", 0x02, 0x30, None, 'Buy Deku Nut (5)', ("Jabu Jabu's Belly", "Vanilla", "Deku Scrub",))),
+ ("Jabu Jabus Belly GS Water Switch Room", ("GS Token", 0x02, 0x08, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
+ ("Jabu Jabus Belly GS Lobby Basement Lower", ("GS Token", 0x02, 0x01, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
+ ("Jabu Jabus Belly GS Lobby Basement Upper", ("GS Token", 0x02, 0x02, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
+ ("Jabu Jabus Belly GS Near Boss", ("GS Token", 0x02, 0x04, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Vanilla", "Skulltulas",))),
+ # Jabu Jabu's Belly MQ
+ ("Jabu Jabus Belly MQ Map Chest", ("Chest", 0x02, 0x03, None, 'Map (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ First Room Side Chest", ("Chest", 0x02, 0x05, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Second Room Lower Chest", ("Chest", 0x02, 0x02, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Compass Chest", ("Chest", 0x02, 0x00, None, 'Compass (Jabu Jabus Belly)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Basement Near Switches Chest", ("Chest", 0x02, 0x08, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Basement Near Vines Chest", ("Chest", 0x02, 0x04, None, 'Bombchus (10)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Boomerang Room Small Chest", ("Chest", 0x02, 0x01, None, 'Deku Nuts (5)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Boomerang Chest", ("Chest", 0x02, 0x06, None, 'Boomerang', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Falling Like Like Room Chest", ("Chest", 0x02, 0x09, None, 'Deku Stick (1)', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Second Room Upper Chest", ("Chest", 0x02, 0x07, None, 'Recovery Heart', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Near Boss Chest", ("Chest", 0x02, 0x0A, None, 'Deku Shield', ("Jabu Jabu's Belly", "Master Quest",))),
+ ("Jabu Jabus Belly MQ Cow", ("NPC", 0x02, 0x15, None, 'Milk', ("Jabu Jabu's Belly", "Master Quest", "Cow",))),
+ ("Jabu Jabus Belly MQ GS Boomerang Chest Room", ("GS Token", 0x02, 0x01, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
+ ("Jabu Jabus Belly MQ GS Tailpasaran Room", ("GS Token", 0x02, 0x04, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
+ ("Jabu Jabus Belly MQ GS Invisible Enemies Room", ("GS Token", 0x02, 0x08, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
+ ("Jabu Jabus Belly MQ GS Near Boss", ("GS Token", 0x02, 0x02, None, 'Gold Skulltula Token', ("Jabu Jabu's Belly", "Master Quest", "Skulltulas",))),
+ # Jabu Jabu's Belly shared
+ ("Jabu Jabus Belly Barinade Heart", ("BossHeart", 0x13, 0x4F, None, 'Heart Container', ("Jabu Jabu's Belly", "Vanilla", "Master Quest",))),
+
+ # Bottom of the Well vanilla
+ ("Bottom of the Well Front Left Fake Wall Chest", ("Chest", 0x08, 0x08, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Front Center Bombable Chest", ("Chest", 0x08, 0x02, None, 'Bombchus (10)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Back Left Bombable Chest", ("Chest", 0x08, 0x04, None, 'Deku Nuts (10)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Underwater Left Chest", ("Chest", 0x08, 0x09, None, 'Recovery Heart', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Freestanding Key", ("Collectable", 0x08, 0x01, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Compass Chest", ("Chest", 0x08, 0x01, None, 'Compass (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Center Skulltula Chest", ("Chest", 0x08, 0x0E, None, 'Deku Nuts (5)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Right Bottom Fake Wall Chest", ("Chest", 0x08, 0x05, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Fire Keese Chest", ("Chest", 0x08, 0x0A, None, 'Deku Shield', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Like Like Chest", ("Chest", 0x08, 0x0C, None, 'Hylian Shield', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Map Chest", ("Chest", 0x08, 0x07, None, 'Map (Bottom of the Well)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Underwater Front Chest", ("Chest", 0x08, 0x10, None, 'Bombs (10)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Invisible Chest", ("Chest", 0x08, 0x14, None, 'Rupees (200)', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well Lens of Truth Chest", ("Chest", 0x08, 0x03, None, 'Lens of Truth', ("Bottom of the Well", "Vanilla",))),
+ ("Bottom of the Well GS West Inner Room", ("GS Token", 0x08, 0x04, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
+ ("Bottom of the Well GS East Inner Room", ("GS Token", 0x08, 0x02, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
+ ("Bottom of the Well GS Like Like Cage", ("GS Token", 0x08, 0x01, None, 'Gold Skulltula Token', ("Bottom of the Well", "Vanilla", "Skulltulas",))),
+ # Bottom of the Well MQ
+ ("Bottom of the Well MQ Map Chest", ("Chest", 0x08, 0x03, None, 'Map (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
+ ("Bottom of the Well MQ East Inner Room Freestanding Key", ("Collectable", 0x08, 0x01, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
+ ("Bottom of the Well MQ Compass Chest", ("Chest", 0x08, 0x02, None, 'Compass (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
+ ("Bottom of the Well MQ Dead Hand Freestanding Key", ("Collectable", 0x08, 0x02, None, 'Small Key (Bottom of the Well)', ("Bottom of the Well", "Master Quest",))),
+ ("Bottom of the Well MQ Lens of Truth Chest", ("Chest", 0x08, 0x01, None, 'Lens of Truth', ("Bottom of the Well", "Master Quest",))),
+ ("Bottom of the Well MQ GS Coffin Room", ("GS Token", 0x08, 0x04, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
+ ("Bottom of the Well MQ GS West Inner Room", ("GS Token", 0x08, 0x02, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
+ ("Bottom of the Well MQ GS Basement", ("GS Token", 0x08, 0x01, None, 'Gold Skulltula Token', ("Bottom of the Well", "Master Quest", "Skulltulas",))),
+
+ # Forest Temple vanilla
+ ("Forest Temple First Room Chest", ("Chest", 0x03, 0x03, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple First Stalfos Chest", ("Chest", 0x03, 0x00, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Raised Island Courtyard Chest", ("Chest", 0x03, 0x05, None, 'Recovery Heart', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Map Chest", ("Chest", 0x03, 0x01, None, 'Map (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Well Chest", ("Chest", 0x03, 0x09, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Eye Switch Chest", ("Chest", 0x03, 0x04, None, 'Arrows (30)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Boss Key Chest", ("Chest", 0x03, 0x0E, None, 'Boss Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Floormaster Chest", ("Chest", 0x03, 0x02, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Red Poe Chest", ("Chest", 0x03, 0x0D, None, 'Small Key (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Bow Chest", ("Chest", 0x03, 0x0C, None, 'Bow', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Blue Poe Chest", ("Chest", 0x03, 0x0F, None, 'Compass (Forest Temple)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Falling Ceiling Room Chest", ("Chest", 0x03, 0x07, None, 'Arrows (10)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple Basement Chest", ("Chest", 0x03, 0x0B, None, 'Arrows (5)', ("Forest Temple", "Vanilla",))),
+ ("Forest Temple GS First Room", ("GS Token", 0x03, 0x02, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
+ ("Forest Temple GS Lobby", ("GS Token", 0x03, 0x08, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
+ ("Forest Temple GS Raised Island Courtyard", ("GS Token", 0x03, 0x01, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
+ ("Forest Temple GS Level Island Courtyard", ("GS Token", 0x03, 0x04, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
+ ("Forest Temple GS Basement", ("GS Token", 0x03, 0x10, None, 'Gold Skulltula Token', ("Forest Temple", "Vanilla", "Skulltulas",))),
+ # Forest Temple MQ
+ ("Forest Temple MQ First Room Chest", ("Chest", 0x03, 0x03, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Wolfos Chest", ("Chest", 0x03, 0x00, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Well Chest", ("Chest", 0x03, 0x09, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Raised Island Courtyard Lower Chest", ("Chest", 0x03, 0x01, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Raised Island Courtyard Upper Chest", ("Chest", 0x03, 0x05, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Boss Key Chest", ("Chest", 0x03, 0x0E, None, 'Boss Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Redead Chest", ("Chest", 0x03, 0x02, None, 'Small Key (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Map Chest", ("Chest", 0x03, 0x0D, None, 'Map (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Bow Chest", ("Chest", 0x03, 0x0C, None, 'Bow', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Compass Chest", ("Chest", 0x03, 0x0F, None, 'Compass (Forest Temple)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Falling Ceiling Room Chest", ("Chest", 0x03, 0x06, None, 'Arrows (5)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ Basement Chest", ("Chest", 0x03, 0x0B, None, 'Arrows (5)', ("Forest Temple", "Master Quest",))),
+ ("Forest Temple MQ GS First Hallway", ("GS Token", 0x03, 0x02, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
+ ("Forest Temple MQ GS Raised Island Courtyard", ("GS Token", 0x03, 0x01, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
+ ("Forest Temple MQ GS Level Island Courtyard", ("GS Token", 0x03, 0x04, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
+ ("Forest Temple MQ GS Well", ("GS Token", 0x03, 0x08, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
+ ("Forest Temple MQ GS Block Push Room", ("GS Token", 0x03, 0x10, None, 'Gold Skulltula Token', ("Forest Temple", "Master Quest", "Skulltulas",))),
+ # Forest Temple shared
+ ("Forest Temple Phantom Ganon Heart", ("BossHeart", 0x14, 0x4F, None, 'Heart Container', ("Forest Temple", "Vanilla", "Master Quest",))),
+
+ # Fire Temple vanilla
+ ("Fire Temple Near Boss Chest", ("Chest", 0x04, 0x01, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Flare Dancer Chest", ("Chest", 0x04, 0x00, None, 'Bombs (10)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Boss Key Chest", ("Chest", 0x04, 0x0C, None, 'Boss Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Big Lava Room Lower Open Door Chest", ("Chest", 0x04, 0x04, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Big Lava Room Blocked Door Chest", ("Chest", 0x04, 0x02, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Boulder Maze Lower Chest", ("Chest", 0x04, 0x03, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Boulder Maze Side Room Chest", ("Chest", 0x04, 0x08, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Map Chest", ("Chest", 0x04, 0x0A, None, 'Map (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Boulder Maze Shortcut Chest", ("Chest", 0x04, 0x0B, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Boulder Maze Upper Chest", ("Chest", 0x04, 0x06, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Scarecrow Chest", ("Chest", 0x04, 0x0D, None, 'Rupees (200)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Compass Chest", ("Chest", 0x04, 0x07, None, 'Compass (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Megaton Hammer Chest", ("Chest", 0x04, 0x05, None, 'Megaton Hammer', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple Highest Goron Chest", ("Chest", 0x04, 0x09, None, 'Small Key (Fire Temple)', ("Fire Temple", "Vanilla",))),
+ ("Fire Temple GS Boss Key Loop", ("GS Token", 0x04, 0x02, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
+ ("Fire Temple GS Song of Time Room", ("GS Token", 0x04, 0x01, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
+ ("Fire Temple GS Boulder Maze", ("GS Token", 0x04, 0x04, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
+ ("Fire Temple GS Scarecrow Climb", ("GS Token", 0x04, 0x10, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
+ ("Fire Temple GS Scarecrow Top", ("GS Token", 0x04, 0x08, None, 'Gold Skulltula Token', ("Fire Temple", "Vanilla", "Skulltulas",))),
+ # Fire Temple MQ
+ ("Fire Temple MQ Map Room Side Chest", ("Chest", 0x04, 0x02, None, 'Hylian Shield', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Megaton Hammer Chest", ("Chest", 0x04, 0x00, None, 'Megaton Hammer', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Map Chest", ("Chest", 0x04, 0x0C, None, 'Map (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Near Boss Chest", ("Chest", 0x04, 0x07, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Big Lava Room Blocked Door Chest", ("Chest", 0x04, 0x01, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Boss Key Chest", ("Chest", 0x04, 0x04, None, 'Boss Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Lizalfos Maze Side Room Chest", ("Chest", 0x04, 0x08, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Compass Chest", ("Chest", 0x04, 0x0B, None, 'Compass (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Lizalfos Maze Upper Chest", ("Chest", 0x04, 0x06, None, 'Bombs (10)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Lizalfos Maze Lower Chest", ("Chest", 0x04, 0x03, None, 'Bombs (10)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Freestanding Key", ("Collectable", 0x04, 0x1C, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ Chest On Fire", ("Chest", 0x04, 0x05, None, 'Small Key (Fire Temple)', ("Fire Temple", "Master Quest",))),
+ ("Fire Temple MQ GS Big Lava Room Open Door", ("GS Token", 0x04, 0x01, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
+ ("Fire Temple MQ GS Skull On Fire", ("GS Token", 0x04, 0x04, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
+ ("Fire Temple MQ GS Fire Wall Maze Center", ("GS Token", 0x04, 0x08, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
+ ("Fire Temple MQ GS Fire Wall Maze Side Room", ("GS Token", 0x04, 0x10, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
+ ("Fire Temple MQ GS Above Fire Wall Maze", ("GS Token", 0x04, 0x02, None, 'Gold Skulltula Token', ("Fire Temple", "Master Quest", "Skulltulas",))),
+ # Fire Temple shared
+ ("Fire Temple Volvagia Heart", ("BossHeart", 0x15, 0x4F, None, 'Heart Container', ("Fire Temple", "Vanilla", "Master Quest",))),
+
+ # Water Temple vanilla
+ ("Water Temple Compass Chest", ("Chest", 0x05, 0x09, None, 'Compass (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Map Chest", ("Chest", 0x05, 0x02, None, 'Map (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Cracked Wall Chest", ("Chest", 0x05, 0x00, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Torches Chest", ("Chest", 0x05, 0x01, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Boss Key Chest", ("Chest", 0x05, 0x05, None, 'Boss Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Central Pillar Chest", ("Chest", 0x05, 0x06, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Central Bow Target Chest", ("Chest", 0x05, 0x08, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Longshot Chest", ("Chest", 0x05, 0x07, None, 'Progressive Hookshot', ("Water Temple", "Vanilla",))),
+ ("Water Temple River Chest", ("Chest", 0x05, 0x03, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple Dragon Chest", ("Chest", 0x05, 0x0A, None, 'Small Key (Water Temple)', ("Water Temple", "Vanilla",))),
+ ("Water Temple GS Behind Gate", ("GS Token", 0x05, 0x01, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
+ ("Water Temple GS Near Boss Key Chest", ("GS Token", 0x05, 0x08, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
+ ("Water Temple GS Central Pillar", ("GS Token", 0x05, 0x04, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
+ ("Water Temple GS Falling Platform Room", ("GS Token", 0x05, 0x02, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
+ ("Water Temple GS River", ("GS Token", 0x05, 0x10, None, 'Gold Skulltula Token', ("Water Temple", "Vanilla", "Skulltulas",))),
+ # Water Temple MQ
+ ("Water Temple MQ Longshot Chest", ("Chest", 0x05, 0x00, None, 'Progressive Hookshot', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ Map Chest", ("Chest", 0x05, 0x02, None, 'Map (Water Temple)', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ Compass Chest", ("Chest", 0x05, 0x01, None, 'Compass (Water Temple)', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ Central Pillar Chest", ("Chest", 0x05, 0x06, None, 'Small Key (Water Temple)', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ Boss Key Chest", ("Chest", 0x05, 0x05, None, 'Boss Key (Water Temple)', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ Freestanding Key", ("Collectable", 0x05, 0x01, None, 'Small Key (Water Temple)', ("Water Temple", "Master Quest",))),
+ ("Water Temple MQ GS Lizalfos Hallway", ("GS Token", 0x05, 0x01, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
+ ("Water Temple MQ GS Before Upper Water Switch", ("GS Token", 0x05, 0x04, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
+ ("Water Temple MQ GS River", ("GS Token", 0x05, 0x02, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
+ ("Water Temple MQ GS Freestanding Key Area", ("GS Token", 0x05, 0x08, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
+ ("Water Temple MQ GS Triple Wall Torch", ("GS Token", 0x05, 0x10, None, 'Gold Skulltula Token', ("Water Temple", "Master Quest", "Skulltulas",))),
+ # Water Temple shared
+ ("Water Temple Morpha Heart", ("BossHeart", 0x16, 0x4F, None, 'Heart Container', ("Water Temple", "Vanilla", "Master Quest",))),
+
+ # Shadow Temple vanilla
+ ("Shadow Temple Map Chest", ("Chest", 0x07, 0x01, None, 'Map (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Hover Boots Chest", ("Chest", 0x07, 0x07, None, 'Hover Boots', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Compass Chest", ("Chest", 0x07, 0x03, None, 'Compass (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Early Silver Rupee Chest", ("Chest", 0x07, 0x02, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Invisible Blades Visible Chest", ("Chest", 0x07, 0x0C, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Invisible Blades Invisible Chest", ("Chest", 0x07, 0x16, None, 'Arrows (30)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Falling Spikes Lower Chest", ("Chest", 0x07, 0x05, None, 'Arrows (10)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Falling Spikes Upper Chest", ("Chest", 0x07, 0x06, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Falling Spikes Switch Chest", ("Chest", 0x07, 0x04, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Invisible Spikes Chest", ("Chest", 0x07, 0x09, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Freestanding Key", ("Collectable", 0x07, 0x01, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Wind Hint Chest", ("Chest", 0x07, 0x15, None, 'Arrows (10)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple After Wind Enemy Chest", ("Chest", 0x07, 0x08, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple After Wind Hidden Chest", ("Chest", 0x07, 0x14, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Spike Walls Left Chest", ("Chest", 0x07, 0x0A, None, 'Rupees (5)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Boss Key Chest", ("Chest", 0x07, 0x0B, None, 'Boss Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple Invisible Floormaster Chest", ("Chest", 0x07, 0x0D, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Vanilla",))),
+ ("Shadow Temple GS Like Like Room", ("GS Token", 0x07, 0x08, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
+ ("Shadow Temple GS Falling Spikes Room", ("GS Token", 0x07, 0x02, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
+ ("Shadow Temple GS Single Giant Pot", ("GS Token", 0x07, 0x01, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
+ ("Shadow Temple GS Near Ship", ("GS Token", 0x07, 0x10, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
+ ("Shadow Temple GS Triple Giant Pot", ("GS Token", 0x07, 0x04, None, 'Gold Skulltula Token', ("Shadow Temple", "Vanilla", "Skulltulas",))),
+ # Shadow Temple MQ
+ ("Shadow Temple MQ Early Gibdos Chest", ("Chest", 0x07, 0x03, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Map Chest", ("Chest", 0x07, 0x02, None, 'Map (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Near Ship Invisible Chest", ("Chest", 0x07, 0x0E, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Compass Chest", ("Chest", 0x07, 0x01, None, 'Compass (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Hover Boots Chest", ("Chest", 0x07, 0x07, None, 'Hover Boots', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Invisible Blades Invisible Chest", ("Chest", 0x07, 0x16, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Invisible Blades Visible Chest", ("Chest", 0x07, 0x0C, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Beamos Silver Rupees Chest", ("Chest", 0x07, 0x0F, None, 'Arrows (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Falling Spikes Lower Chest", ("Chest", 0x07, 0x05, None, 'Arrows (10)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Falling Spikes Upper Chest", ("Chest", 0x07, 0x06, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Falling Spikes Switch Chest", ("Chest", 0x07, 0x04, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Invisible Spikes Chest", ("Chest", 0x07, 0x09, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Stalfos Room Chest", ("Chest", 0x07, 0x10, None, 'Rupees (20)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Wind Hint Chest", ("Chest", 0x07, 0x15, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ After Wind Hidden Chest", ("Chest", 0x07, 0x14, None, 'Arrows (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ After Wind Enemy Chest", ("Chest", 0x07, 0x08, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Boss Key Chest", ("Chest", 0x07, 0x0B, None, 'Boss Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Spike Walls Left Chest", ("Chest", 0x07, 0x0A, None, 'Rupees (5)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Freestanding Key", ("Collectable", 0x07, 0x06, None, 'Small Key (Shadow Temple)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ Bomb Flower Chest", ("Chest", 0x07, 0x0D, None, 'Arrows (10)', ("Shadow Temple", "Master Quest",))),
+ ("Shadow Temple MQ GS Falling Spikes Room", ("GS Token", 0x07, 0x02, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
+ ("Shadow Temple MQ GS Wind Hint Room", ("GS Token", 0x07, 0x01, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
+ ("Shadow Temple MQ GS After Wind", ("GS Token", 0x07, 0x08, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
+ ("Shadow Temple MQ GS After Ship", ("GS Token", 0x07, 0x10, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
+ ("Shadow Temple MQ GS Near Boss", ("GS Token", 0x07, 0x04, None, 'Gold Skulltula Token', ("Shadow Temple", "Master Quest", "Skulltulas",))),
+ # Shadow Temple shared
+ ("Shadow Temple Bongo Bongo Heart", ("BossHeart", 0x18, 0x4F, None, 'Heart Container', ("Shadow Temple", "Vanilla", "Master Quest",))),
+
+ # Spirit Temple shared
+ # Vanilla and MQ locations are mixed to ensure the positions of Silver Gauntlets/Mirror Shield chests are correct for both versions
+ ("Spirit Temple Child Bridge Chest", ("Chest", 0x06, 0x08, None, 'Deku Shield', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Child Early Torches Chest", ("Chest", 0x06, 0x00, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Child Climb North Chest", ("Chest", 0x06, 0x06, None, 'Bombchus (10)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Child Climb East Chest", ("Chest", 0x06, 0x0C, None, 'Deku Shield', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Map Chest", ("Chest", 0x06, 0x03, None, 'Map (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Sun Block Room Chest", ("Chest", 0x06, 0x01, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple MQ Entrance Front Left Chest", ("Chest", 0x06, 0x1A, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Entrance Back Right Chest", ("Chest", 0x06, 0x1F, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Entrance Front Right Chest", ("Chest", 0x06, 0x1B, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Entrance Back Left Chest", ("Chest", 0x06, 0x1E, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Map Chest", ("Chest", 0x06, 0x00, None, 'Map (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Map Room Enemy Chest", ("Chest", 0x06, 0x08, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Child Climb North Chest", ("Chest", 0x06, 0x06, None, 'Bombchus (10)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Child Climb South Chest", ("Chest", 0x06, 0x0C, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Compass Chest", ("Chest", 0x06, 0x03, None, 'Compass (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Silver Block Hallway Chest", ("Chest", 0x06, 0x1C, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Sun Block Room Chest", ("Chest", 0x06, 0x01, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple Silver Gauntlets Chest", ("Chest", 0x5C, 0x0B, None, 'Progressive Strength Upgrade', ("Spirit Temple", "Vanilla", "Master Quest", "Desert Colossus"))),
+
+ ("Spirit Temple Compass Chest", ("Chest", 0x06, 0x04, None, 'Compass (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Early Adult Right Chest", ("Chest", 0x06, 0x07, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple First Mirror Left Chest", ("Chest", 0x06, 0x0D, None, 'Ice Trap', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple First Mirror Right Chest", ("Chest", 0x06, 0x0E, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Statue Room Northeast Chest", ("Chest", 0x06, 0x0F, None, 'Rupees (5)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Statue Room Hand Chest", ("Chest", 0x06, 0x02, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Near Four Armos Chest", ("Chest", 0x06, 0x05, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Hallway Right Invisible Chest", ("Chest", 0x06, 0x14, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Hallway Left Invisible Chest", ("Chest", 0x06, 0x15, None, 'Recovery Heart', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple MQ Child Hammer Switch Chest", ("Chest", 0x06, 0x1D, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Statue Room Lullaby Chest", ("Chest", 0x06, 0x0F, None, 'Rupees (5)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Statue Room Invisible Chest", ("Chest", 0x06, 0x02, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Leever Room Chest", ("Chest", 0x06, 0x04, None, 'Rupees (50)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Symphony Room Chest", ("Chest", 0x06, 0x07, None, 'Rupees (50)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Beamos Room Chest", ("Chest", 0x06, 0x19, None, 'Recovery Heart', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Chest Switch Chest", ("Chest", 0x06, 0x18, None, 'Ice Trap', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple MQ Boss Key Chest", ("Chest", 0x06, 0x05, None, 'Boss Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+ ("Spirit Temple Mirror Shield Chest", ("Chest", 0x5C, 0x09, None, 'Mirror Shield', ("Spirit Temple", "Vanilla", "Master Quest", "Desert Colossus"))),
+
+ ("Spirit Temple Boss Key Chest", ("Chest", 0x06, 0x0A, None, 'Boss Key (Spirit Temple)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple Topmost Chest", ("Chest", 0x06, 0x12, None, 'Bombs (20)', ("Spirit Temple", "Vanilla",))),
+ ("Spirit Temple MQ Mirror Puzzle Invisible Chest", ("Chest", 0x06, 0x12, None, 'Small Key (Spirit Temple)', ("Spirit Temple", "Master Quest",))),
+
+ ("Spirit Temple GS Metal Fence", ("GS Token", 0x06, 0x10, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
+ ("Spirit Temple GS Sun on Floor Room", ("GS Token", 0x06, 0x08, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
+ ("Spirit Temple GS Hall After Sun Block Room", ("GS Token", 0x06, 0x01, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
+ ("Spirit Temple GS Lobby", ("GS Token", 0x06, 0x04, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
+ ("Spirit Temple GS Boulder Room", ("GS Token", 0x06, 0x02, None, 'Gold Skulltula Token', ("Spirit Temple", "Vanilla", "Skulltulas",))),
+ ("Spirit Temple MQ GS Sun Block Room", ("GS Token", 0x06, 0x01, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
+ ("Spirit Temple MQ GS Leever Room", ("GS Token", 0x06, 0x02, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
+ ("Spirit Temple MQ GS Symphony Room", ("GS Token", 0x06, 0x08, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
+ ("Spirit Temple MQ GS Nine Thrones Room West", ("GS Token", 0x06, 0x04, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
+ ("Spirit Temple MQ GS Nine Thrones Room North", ("GS Token", 0x06, 0x10, None, 'Gold Skulltula Token', ("Spirit Temple", "Master Quest", "Skulltulas",))),
+
+ ("Spirit Temple Twinrova Heart", ("BossHeart", 0x17, 0x4F, None, 'Heart Container', ("Spirit Temple", "Vanilla", "Master Quest",))),
+
+ # Ice Cavern vanilla
+ ("Ice Cavern Map Chest", ("Chest", 0x09, 0x00, None, 'Map (Ice Cavern)', ("Ice Cavern", "Vanilla",))),
+ ("Ice Cavern Compass Chest", ("Chest", 0x09, 0x01, None, 'Compass (Ice Cavern)', ("Ice Cavern", "Vanilla",))),
+ ("Ice Cavern Freestanding PoH", ("Collectable", 0x09, 0x01, None, 'Piece of Heart', ("Ice Cavern", "Vanilla",))),
+ ("Ice Cavern Iron Boots Chest", ("Chest", 0x09, 0x02, None, 'Iron Boots', ("Ice Cavern", "Vanilla",))),
+ ("Ice Cavern GS Spinning Scythe Room", ("GS Token", 0x09, 0x02, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
+ ("Ice Cavern GS Heart Piece Room", ("GS Token", 0x09, 0x04, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
+ ("Ice Cavern GS Push Block Room", ("GS Token", 0x09, 0x01, None, 'Gold Skulltula Token', ("Ice Cavern", "Vanilla", "Skulltulas",))),
+ # Ice Cavern MQ
+ ("Ice Cavern MQ Map Chest", ("Chest", 0x09, 0x01, None, 'Map (Ice Cavern)', ("Ice Cavern", "Master Quest",))),
+ ("Ice Cavern MQ Compass Chest", ("Chest", 0x09, 0x00, None, 'Compass (Ice Cavern)', ("Ice Cavern", "Master Quest",))),
+ ("Ice Cavern MQ Freestanding PoH", ("Collectable", 0x09, 0x01, None, 'Piece of Heart', ("Ice Cavern", "Master Quest",))),
+ ("Ice Cavern MQ Iron Boots Chest", ("Chest", 0x09, 0x02, None, 'Iron Boots', ("Ice Cavern", "Master Quest",))),
+ ("Ice Cavern MQ GS Red Ice", ("GS Token", 0x09, 0x02, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
+ ("Ice Cavern MQ GS Ice Block", ("GS Token", 0x09, 0x04, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
+ ("Ice Cavern MQ GS Scarecrow", ("GS Token", 0x09, 0x01, None, 'Gold Skulltula Token', ("Ice Cavern", "Master Quest", "Skulltulas",))),
+
+ # Gerudo Training Grounds vanilla
+ ("Gerudo Training Grounds Lobby Left Chest", ("Chest", 0x0B, 0x13, None, 'Rupees (5)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Lobby Right Chest", ("Chest", 0x0B, 0x07, None, 'Arrows (10)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Stalfos Chest", ("Chest", 0x0B, 0x00, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Before Heavy Block Chest", ("Chest", 0x0B, 0x11, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Heavy Block First Chest", ("Chest", 0x0B, 0x0F, None, 'Rupees (200)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Heavy Block Second Chest", ("Chest", 0x0B, 0x0E, None, 'Rupees (5)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Heavy Block Third Chest", ("Chest", 0x0B, 0x14, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Heavy Block Fourth Chest", ("Chest", 0x0B, 0x02, None, 'Ice Trap', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Eye Statue Chest", ("Chest", 0x0B, 0x03, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Near Scarecrow Chest", ("Chest", 0x0B, 0x04, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Hammer Room Clear Chest", ("Chest", 0x0B, 0x12, None, 'Arrows (10)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Hammer Room Switch Chest", ("Chest", 0x0B, 0x10, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Freestanding Key", ("Collectable", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Right Central Chest", ("Chest", 0x0B, 0x05, None, 'Bombchus (5)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Right Side Chest", ("Chest", 0x0B, 0x08, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Underwater Silver Rupee Chest", ("Chest", 0x0B, 0x0D, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Beamos Chest", ("Chest", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Hidden Ceiling Chest", ("Chest", 0x0B, 0x0B, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Path First Chest", ("Chest", 0x0B, 0x06, None, 'Rupees (50)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Path Second Chest", ("Chest", 0x0B, 0x0A, None, 'Rupees (20)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Path Third Chest", ("Chest", 0x0B, 0x09, None, 'Arrows (30)', ("Gerudo Training Grounds", "Vanilla",))),
+ ("Gerudo Training Grounds Maze Path Final Chest", ("Chest", 0x0B, 0x0C, None, 'Ice Arrows', ("Gerudo Training Grounds", "Vanilla",))),
+ # Gerudo Training Grounds MQ
+ ("Gerudo Training Grounds MQ Lobby Left Chest", ("Chest", 0x0B, 0x13, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Lobby Right Chest", ("Chest", 0x0B, 0x07, None, 'Bombchus (5)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ First Iron Knuckle Chest", ("Chest", 0x0B, 0x00, None, 'Rupees (5)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Before Heavy Block Chest", ("Chest", 0x0B, 0x11, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Heavy Block Chest", ("Chest", 0x0B, 0x02, None, 'Rupees (50)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Eye Statue Chest", ("Chest", 0x0B, 0x03, None, 'Bombchus (10)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Ice Arrows Chest", ("Chest", 0x0B, 0x04, None, 'Ice Arrows', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Second Iron Knuckle Chest", ("Chest", 0x0B, 0x12, None, 'Arrows (10)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Flame Circle Chest", ("Chest", 0x0B, 0x0E, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Maze Right Central Chest", ("Chest", 0x0B, 0x05, None, 'Rupees (5)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Maze Right Side Chest", ("Chest", 0x0B, 0x08, None, 'Rupee (Treasure Chest Game)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Underwater Silver Rupee Chest", ("Chest", 0x0B, 0x0D, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Dinolfos Chest", ("Chest", 0x0B, 0x01, None, 'Small Key (Gerudo Training Grounds)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Hidden Ceiling Chest", ("Chest", 0x0B, 0x0B, None, 'Rupees (50)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Maze Path First Chest", ("Chest", 0x0B, 0x06, None, 'Rupee (1)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Maze Path Third Chest", ("Chest", 0x0B, 0x09, None, 'Rupee (Treasure Chest Game)', ("Gerudo Training Grounds", "Master Quest",))),
+ ("Gerudo Training Grounds MQ Maze Path Second Chest", ("Chest", 0x0B, 0x0A, None, 'Rupees (20)', ("Gerudo Training Grounds", "Master Quest",))),
+
+ # Ganon's Castle vanilla
+ ("Ganons Castle Forest Trial Chest", ("Chest", 0x0D, 0x09, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Water Trial Left Chest", ("Chest", 0x0D, 0x07, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Water Trial Right Chest", ("Chest", 0x0D, 0x06, None, 'Recovery Heart', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Shadow Trial Front Chest", ("Chest", 0x0D, 0x08, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Shadow Trial Golden Gauntlets Chest", ("Chest", 0x0D, 0x05, None, 'Progressive Strength Upgrade', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial First Left Chest", ("Chest", 0x0D, 0x0C, None, 'Rupees (5)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Second Left Chest", ("Chest", 0x0D, 0x0B, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Third Left Chest", ("Chest", 0x0D, 0x0D, None, 'Recovery Heart', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial First Right Chest", ("Chest", 0x0D, 0x0E, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Second Right Chest", ("Chest", 0x0D, 0x0A, None, 'Arrows (30)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Third Right Chest", ("Chest", 0x0D, 0x0F, None, 'Ice Trap', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Invisible Enemies Chest", ("Chest", 0x0D, 0x10, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Light Trial Lullaby Chest", ("Chest", 0x0D, 0x11, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Spirit Trial Crystal Switch Chest", ("Chest", 0x0D, 0x12, None, 'Bombchus (20)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Spirit Trial Invisible Chest", ("Chest", 0x0D, 0x14, None, 'Arrows (10)', ("Ganon's Castle", "Vanilla",))),
+ ("Ganons Castle Deku Scrub Left", ("NPC", 0x0D, 0x3A, None, 'Buy Green Potion', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
+ ("Ganons Castle Deku Scrub Center-Left", ("NPC", 0x0D, 0x37, None, 'Buy Bombs (5) [35]', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
+ ("Ganons Castle Deku Scrub Center-Right", ("NPC", 0x0D, 0x33, None, 'Buy Arrows (30)', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
+ ("Ganons Castle Deku Scrub Right", ("NPC", 0x0D, 0x39, None, 'Buy Red Potion [30]', ("Ganon's Castle", "Vanilla", "Deku Scrub",))),
+ # Ganon's Castle MQ
+ ("Ganons Castle MQ Forest Trial Freestanding Key", ("Collectable", 0x0D, 0x01, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Forest Trial Eye Switch Chest", ("Chest", 0x0D, 0x02, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Forest Trial Frozen Eye Switch Chest", ("Chest", 0x0D, 0x03, None, 'Bombs (5)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Water Trial Chest", ("Chest", 0x0D, 0x01, None, 'Rupees (20)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Shadow Trial Bomb Flower Chest", ("Chest", 0x0D, 0x00, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Shadow Trial Eye Switch Chest", ("Chest", 0x0D, 0x05, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Light Trial Lullaby Chest", ("Chest", 0x0D, 0x04, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial First Chest", ("Chest", 0x0D, 0x0A, None, 'Bombchus (10)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial Invisible Chest", ("Chest", 0x0D, 0x14, None, 'Arrows (10)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial Sun Front Left Chest", ("Chest", 0x0D, 0x09, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial Sun Back Left Chest", ("Chest", 0x0D, 0x08, None, 'Small Key (Ganons Castle)', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial Sun Back Right Chest", ("Chest", 0x0D, 0x07, None, 'Recovery Heart', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Spirit Trial Golden Gauntlets Chest", ("Chest", 0x0D, 0x06, None, 'Progressive Strength Upgrade', ("Ganon's Castle", "Master Quest",))),
+ ("Ganons Castle MQ Deku Scrub Left", ("NPC", 0x0D, 0x3A, None, 'Buy Green Potion', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
+ ("Ganons Castle MQ Deku Scrub Center-Left", ("NPC", 0x0D, 0x37, None, 'Buy Bombs (5) [35]', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
+ ("Ganons Castle MQ Deku Scrub Center", ("NPC", 0x0D, 0x33, None, 'Buy Arrows (30)', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
+ ("Ganons Castle MQ Deku Scrub Center-Right", ("NPC", 0x0D, 0x39, None, 'Buy Red Potion [30]', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
+ ("Ganons Castle MQ Deku Scrub Right", ("NPC", 0x0D, 0x30, None, 'Buy Deku Nut (5)', ("Ganon's Castle", "Master Quest", "Deku Scrub",))),
+ # Ganon's Castle shared
+ ("Ganons Tower Boss Key Chest", ("Chest", 0x0A, 0x0B, None, 'Boss Key (Ganons Castle)', ("Ganon's Castle", "Vanilla", "Master Quest",))),
+
+ ## Events and Drops
+ ("Pierre", ("Event", None, None, None, 'Scarecrow Song', None)),
+ ("Deliver Rutos Letter", ("Event", None, None, None, 'Deliver Letter', None)),
+ ("Master Sword Pedestal", ("Event", None, None, None, 'Time Travel', None)),
+
+ ("Deku Baba Sticks", ("Drop", None, None, None, 'Deku Stick Drop', None)),
+ ("Deku Baba Nuts", ("Drop", None, None, None, 'Deku Nut Drop', None)),
+ ("Stick Pot", ("Drop", None, None, None, 'Deku Stick Drop', None)),
+ ("Nut Pot", ("Drop", None, None, None, 'Deku Nut Drop', None)),
+ ("Nut Crate", ("Drop", None, None, None, 'Deku Nut Drop', None)),
+ ("Blue Fire", ("Drop", None, None, None, 'Blue Fire', None)),
+ ("Lone Fish", ("Drop", None, None, None, 'Fish', None)),
+ ("Fish Group", ("Drop", None, None, None, 'Fish', None)),
+ ("Bug Rock", ("Drop", None, None, None, 'Bugs', None)),
+ ("Bug Shrub", ("Drop", None, None, None, 'Bugs', None)),
+ ("Wandering Bugs", ("Drop", None, None, None, 'Bugs', None)),
+ ("Fairy Pot", ("Drop", None, None, None, 'Fairy', None)),
+ ("Free Fairies", ("Drop", None, None, None, 'Fairy', None)),
+ ("Wall Fairy", ("Drop", None, None, None, 'Fairy', None)),
+ ("Butterfly Fairy", ("Drop", None, None, None, 'Fairy', None)),
+ ("Gossip Stone Fairy", ("Drop", None, None, None, 'Fairy', None)),
+ ("Bean Plant Fairy", ("Drop", None, None, None, 'Fairy', None)),
+ ("Fairy Pond", ("Drop", None, None, None, 'Fairy', None)),
+ ("Big Poe Kill", ("Drop", None, None, None, 'Big Poe', None)),
+
+ ## Hints
+ # These are not actual locations, but are filler spots used for hint reachability.
+ # Hint location types must start with 'Hint'.
+ ("DMC Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("DMT Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("Colossus Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("Dodongos Cavern Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("GV Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("GC Maze Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("GC Medigoron Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("Graveyard Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HC Malon Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HC Rock Wall Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HC Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HF Cow Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("KF Deku Tree Gossip Stone (Left)", ("HintStone", None, None, None, None, None)),
+ ("KF Deku Tree Gossip Stone (Right)", ("HintStone", None, None, None, None, None)),
+ ("KF Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("LH Lab Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("LH Gossip Stone (Southeast)", ("HintStone", None, None, None, None, None)),
+ ("LH Gossip Stone (Southwest)", ("HintStone", None, None, None, None, None)),
+ ("LW Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("SFM Maze Gossip Stone (Lower)", ("HintStone", None, None, None, None, None)),
+ ("SFM Maze Gossip Stone (Upper)", ("HintStone", None, None, None, None, None)),
+ ("SFM Saria Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ToT Gossip Stone (Left)", ("HintStone", None, None, None, None, None)),
+ ("ToT Gossip Stone (Left-Center)", ("HintStone", None, None, None, None, None)),
+ ("ToT Gossip Stone (Right)", ("HintStone", None, None, None, None, None)),
+ ("ToT Gossip Stone (Right-Center)", ("HintStone", None, None, None, None, None)),
+ ("ZD Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ZF Fairy Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ZF Jabu Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ZR Near Grottos Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ZR Near Domain Gossip Stone", ("HintStone", None, None, None, None, None)),
+
+ ("HF Near Market Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HF Southeast Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("HF Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("Kak Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("ZR Open Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("KF Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("LW Near Shortcuts Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("DMT Storms Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+ ("DMC Upper Grotto Gossip Stone", ("HintStone", None, None, None, None, None)),
+
+ ("Ganondorf Hint", ("Hint", None, None, None, None, None)),
+])
+
+location_sort_order = {
+ loc: i for i, loc in enumerate(location_table.keys())
+}
+
+# Business Scrub Details
+business_scrubs = [
+ # id price text text replacement
+ (0x30, 20, 0x10A0, ["Deku Nuts", "a \x05\x42mysterious item\x05\x40"]),
+ (0x31, 15, 0x10A1, ["Deku Sticks", "a \x05\x42mysterious item\x05\x40"]),
+ (0x3E, 10, 0x10A2, ["Piece of Heart", "\x05\x42mysterious item\x05\x40"]),
+ (0x33, 40, 0x10CA, ["\x05\x41Deku Seeds", "a \x05\x42mysterious item"]),
+ (0x34, 50, 0x10CB, ["\x41Deku Shield", "\x42mysterious item"]),
+ (0x37, 40, 0x10CC, ["\x05\x41Bombs", "a \x05\x42mysterious item"]),
+ (0x38, 00, 0x10CD, ["\x05\x41Arrows", "a \x05\x42mysterious item"]), # unused
+ (0x39, 40, 0x10CE, ["\x05\x41Red Potion", "\x05\x42mysterious item"]),
+ (0x3A, 40, 0x10CF, ["Green Potion", "mysterious item"]),
+ (0x77, 40, 0x10DC, ["enable you to pick up more\x01\x05\x41Deku Sticks", "sell you a \x05\x42mysterious item"]),
+ (0x79, 40, 0x10DD, ["enable you to pick up more \x05\x41Deku\x01Nuts", "sell you a \x05\x42mysterious item"]),
+]
+
+dungeons = ('Deku Tree', 'Dodongo\'s Cavern', 'Jabu Jabu\'s Belly', 'Forest Temple', 'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple', 'Ice Cavern', 'Bottom of the Well', 'Gerudo Training Grounds', 'Ganon\'s Castle')
+location_groups = {
+ 'Song': [name for (name, data) in location_table.items() if data[0] == 'Song'],
+ 'Chest': [name for (name, data) in location_table.items() if data[0] == 'Chest'],
+ 'Collectable': [name for (name, data) in location_table.items() if data[0] == 'Collectable'],
+ 'BossHeart': [name for (name, data) in location_table.items() if data[0] == 'BossHeart'],
+ 'CollectableLike': [name for (name, data) in location_table.items() if data[0] in ('Collectable', 'BossHeart', 'GS Token')],
+ 'CanSee': [name for (name, data) in location_table.items()
+ if data[0] in ('Collectable', 'BossHeart', 'GS Token', 'Shop')
+ # Treasure Box Shop, Bombchu Bowling, Hyrule Field (OoT), Lake Hylia (RL/FA)
+ or data[0:2] in [('Chest', 0x10), ('NPC', 0x4B), ('NPC', 0x51), ('NPC', 0x57)]],
+ 'Dungeon': [name for (name, data) in location_table.items() if data[5] is not None and any(dungeon in data[5] for dungeon in dungeons)],
+}
+
+
+def location_is_viewable(loc_name, correct_chest_sizes):
+ return correct_chest_sizes and loc_name in location_groups['Chest'] or loc_name in location_groups['CanSee']
+
+
+# Function to run exactly once after after placing items in drop locations for each world
+# Sets all Drop locations to a unique name in order to avoid name issues and to identify locations in the spoiler
+# Also cause them to not be shown in the list of locations, only in playthrough
+def set_drop_location_names(ootworld):
+ for region in ootworld.regions:
+ for location in region.locations:
+ if location.type == 'Drop':
+ location.name = location.parent_region.name + " " + location.name
+ location.show_in_spoiler = False
diff --git a/worlds/oot/LogicTricks.py b/worlds/oot/LogicTricks.py
new file mode 100644
index 00000000..90abc13a
--- /dev/null
+++ b/worlds/oot/LogicTricks.py
@@ -0,0 +1,1359 @@
+known_logic_tricks = {
+ 'Fewer Tunic Requirements': {
+ 'name' : 'logic_fewer_tunic_requirements',
+ 'tags' : ("General", "Fire Temple", "Water Temple", "Gerudo Training Grounds", "Zora's Fountain",),
+ 'tooltip' : '''\
+ Allows the following possible without Tunics:
+ - Enter Water Temple. The key below the center
+ pillar still requires Zora Tunic.
+ - Enter Fire Temple. Only the first floor is
+ accessible, and not Volvagia.
+ - Zora's Fountain Bottom Freestanding PoH.
+ Might not have enough health to resurface.
+ - Gerudo Training Grounds Underwater
+ Silver Rupee Chest. May need to make multiple
+ trips.
+ '''},
+ 'Hidden Grottos without Stone of Agony': {
+ 'name' : 'logic_grottos_without_agony',
+ 'tags' : ("General", "Entrance",),
+ 'tooltip' : '''\
+ Allows entering hidden grottos without the
+ Stone of Agony.
+ '''},
+ 'Pass Through Visible One-Way Collisions': {
+ 'name' : 'logic_visible_collisions',
+ 'tags' : ("Entrance", "Kakariko Village",),
+ 'tooltip' : '''\
+ Allows climbing through the platform to reach
+ Impa's House Back as adult with no items and
+ going through the Kakariko Village Gate as child
+ when coming from the Mountain Trail side.
+ '''},
+ 'Child Deadhand without Kokiri Sword': {
+ 'name' : 'logic_child_deadhand',
+ 'tags' : ("Bottom of the Well",),
+ 'tooltip' : '''\
+ Requires 9 sticks or 5 jump slashes.
+ '''},
+ 'Second Dampe Race as Child': {
+ 'name' : 'logic_child_dampe_race_poh',
+ 'tags' : ("the Graveyard", "Entrance",),
+ 'tooltip' : '''\
+ It is possible to complete the second dampe
+ race as child in under a minute, but it is
+ a strict time limit.
+ '''},
+ 'Man on Roof without Hookshot': {
+ 'name' : 'logic_man_on_roof',
+ 'tags' : ("Kakariko Village",),
+ 'tooltip' : '''\
+ Can be reached by side-hopping off
+ the watchtower.
+ '''},
+ 'Dodongo\'s Cavern Staircase with Bow': {
+ 'name' : 'logic_dc_staircase',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ The Bow can be used to knock down the stairs
+ with two well-timed shots.
+ '''},
+ 'Dodongo\'s Cavern Spike Trap Room Jump without Hover Boots': {
+ 'name' : 'logic_dc_jump',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ Jump is adult only.
+ '''},
+ 'Dodongo\'s Cavern Vines GS from Below with Longshot': {
+ 'name' : 'logic_dc_vines_gs',
+ 'tags' : ("Dodongo's Cavern", "Skulltulas",),
+ 'tooltip' : '''\
+ The vines upon which this Skulltula rests are one-
+ sided collision. You can use the Longshot to get it
+ from below, by shooting it through the vines,
+ bypassing the need to lower the staircase.
+ '''},
+ 'Gerudo Fortress "Kitchen" with No Additional Items': {
+ 'name' : 'logic_gerudo_kitchen',
+ 'tags' : ("Gerudo's Fortress",),
+ 'tooltip' : '''\
+ The logic normally guarantees one of Bow, Hookshot,
+ or Hover Boots.
+ '''},
+ 'Deku Tree Basement Vines GS with Jump Slash': {
+ 'name' : 'logic_deku_basement_gs',
+ 'tags' : ("Deku Tree", "Skulltulas",),
+ 'tooltip' : '''\
+ Can be defeated by doing a precise jump slash.
+ '''},
+ 'Deku Tree Basement Web to Gohma with Bow': {
+ 'name' : 'logic_deku_b1_webs_with_bow',
+ 'tags' : ("Deku Tree", "Entrance",),
+ 'tooltip' : '''\
+ All spider web walls in the Deku Tree basement can be burnt
+ as adult with just a bow by shooting through torches. This
+ trick only applies to the circular web leading to Gohma;
+ the two vertical webs are always in logic.
+
+ Backflip onto the chest near the torch at the bottom of
+ the vine wall. With precise positioning you can shoot
+ through the torch to the right edge of the circular web.
+
+ This allows completion of adult Deku Tree with no fire source.
+ '''},
+ 'Deku Tree MQ Roll Under the Spiked Log': {
+ 'name' : 'logic_deku_mq_log',
+ 'tags' : ("Deku Tree",),
+ 'tooltip' : '''\
+ You can get past the spiked log by rolling
+ to briefly shrink your hitbox. As adult,
+ the timing is a bit more precise.
+ '''},
+ 'Hammer Rusted Switches Through Walls': {
+ 'name' : 'logic_rusted_switches',
+ 'tags' : ("Fire Temple", "Ganon's Castle",),
+ 'tooltip' : '''\
+ Applies to:
+ - Fire Temple Highest Goron Chest.
+ - MQ Fire Temple Lizalfos Maze.
+ - MQ Spirit Trial.
+ '''},
+ 'Bottom of the Well Map Chest with Strength & Sticks': {
+ 'name' : 'logic_botw_basement',
+ 'tags' : ("Bottom of the Well",),
+ 'tooltip' : '''\
+ The chest in the basement can be reached with
+ strength by doing a jump slash with a lit
+ stick to access the bomb flowers.
+ '''},
+ 'Bottom of the Well MQ Jump Over the Pits': {
+ 'name' : 'logic_botw_mq_pits',
+ 'tags' : ("Bottom of the Well",),
+ 'tooltip' : '''\
+ While the pits in Bottom of the Well don't allow you to
+ jump just by running straight at them, you can still get
+ over them by side-hopping or backflipping across. With
+ explosives, this allows you to access the central areas
+ without Zelda's Lullaby. With Zelda's Lullaby, it allows
+ you to access the west inner room without explosives.
+ '''},
+ 'Skip Forest Temple MQ Block Puzzle with Bombchu': {
+ 'name' : 'logic_forest_mq_block_puzzle',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ Send the Bombchu straight up the center of the
+ wall directly to the left upon entering the room.
+ '''},
+ 'Spirit Temple Child Side Bridge with Bombchu': {
+ 'name' : 'logic_spirit_child_bombchu',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ A carefully-timed Bombchu can hit the switch.
+ '''},
+ 'Windmill PoH as Adult with Nothing': {
+ 'name' : 'logic_windmill_poh',
+ 'tags' : ("Kakariko Village",),
+ 'tooltip' : '''\
+ Can jump up to the spinning platform from
+ below as adult.
+ '''},
+ 'Crater\'s Bean PoH with Hover Boots': {
+ 'name' : 'logic_crater_bean_poh_with_hovers',
+ 'tags' : ("Death Mountain Crater",),
+ 'tooltip' : '''\
+ Hover from the base of the bridge
+ near Goron City and walk up the
+ very steep slope.
+ '''},
+ 'Zora\'s Domain Entry with Cucco': {
+ 'name' : 'logic_zora_with_cucco',
+ 'tags' : ("Zora's River",),
+ 'tooltip' : '''\
+ Can fly behind the waterfall with
+ a cucco as child.
+ '''},
+ 'Water Temple MQ Central Pillar with Fire Arrows': {
+ 'name' : 'logic_water_mq_central_pillar',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ Slanted torches have misleading hitboxes. Whenever
+ you see a slanted torch jutting out of the wall,
+ you can expect most or all of its hitbox is actually
+ on the other side that wall. This can make slanted
+ torches very finicky to light when using arrows. The
+ torches in the central pillar of MQ Water Temple are
+ a particularly egregious example. Logic normally
+ expects Din's Fire and Song of Time.
+ '''},
+ 'Gerudo Training Grounds MQ Left Side Silver Rupees with Hookshot': {
+ 'name' : 'logic_gtg_mq_with_hookshot',
+ 'tags' : ("Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ The highest silver rupee can be obtained by
+ hookshotting the target and then immediately jump
+ slashing toward the rupee.
+ '''},
+ 'Forest Temple East Courtyard Vines with Hookshot': {
+ 'name' : 'logic_forest_vines',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ The vines in Forest Temple leading to where the well
+ drain switch is in the standard form can be barely
+ reached with just the Hookshot.
+ '''},
+ 'Forest Temple East Courtyard GS with Boomerang': {
+ 'name' : 'logic_forest_outdoor_east_gs',
+ 'tags' : ("Forest Temple", "Entrance", "Skulltulas",),
+ 'tooltip' : '''\
+ Precise Boomerang throws can allow child to
+ kill the Skulltula and collect the token.
+ '''},
+ 'Forest Temple First Room GS with Difficult-to-Use Weapons': {
+ 'name' : 'logic_forest_first_gs',
+ 'tags' : ("Forest Temple", "Entrance", "Skulltulas",),
+ 'tooltip' : '''\
+ Allows killing this Skulltula with Sword or Sticks by
+ jump slashing it as you let go from the vines. You will
+ take fall damage.
+ Also allows killing it as Child with a Bomb throw. It's
+ much more difficult to use a Bomb as child due to
+ Child Link's shorter height.
+ '''},
+ 'Swim Through Forest Temple MQ Well with Hookshot': {
+ 'name' : 'logic_forest_well_swim',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ Shoot the vines in the well as low and as far to
+ the right as possible, and then immediately swim
+ under the ceiling to the right. This can only be
+ required if Forest Temple is in its Master Quest
+ form.
+ '''},
+ 'Forest Temple MQ Twisted Hallway Switch with Jump Slash': {
+ 'name' : 'logic_forest_mq_hallway_switch_jumpslash',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ The switch to twist the hallway can be hit with
+ a jump slash through the glass block. To get in
+ front of the switch, either use the Hover Boots
+ or hit the shortcut switch at the top of the
+ room and jump from the glass blocks that spawn.
+ '''},
+ 'Forest Temple MQ Twisted Hallway Switch with Hookshot': {
+ 'name' : 'logic_forest_mq_hallway_switch_hookshot',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ There's a very small gap between the glass block
+ and the wall. Through that gap you can hookshot
+ the target on the ceiling.
+ '''},
+ 'Death Mountain Trail Chest with Strength': {
+ 'name' : 'logic_dmt_bombable',
+ 'tags' : ("Death Mountain Trail",),
+ 'tooltip' : '''\
+ Child Link can blow up the wall using a nearby bomb
+ flower. You must backwalk with the flower and then
+ quickly throw it toward the wall.
+ '''},
+ 'Goron City Spinning Pot PoH with Strength': {
+ 'name' : 'logic_goron_city_pot_with_strength',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ Allows for stopping the Goron City Spinning
+ Pot using a bomb flower alone, requiring
+ strength in lieu of inventory explosives.
+ '''},
+ 'Adult Kokiri Forest GS with Hover Boots': {
+ 'name' : 'logic_adult_kokiri_gs',
+ 'tags' : ("Kokiri Forest", "Skulltulas",),
+ 'tooltip' : '''\
+ Can be obtained without Hookshot by using the Hover
+ Boots off of one of the roots.
+ '''},
+ 'Spirit Temple MQ Frozen Eye Switch without Fire': {
+ 'name' : 'logic_spirit_mq_frozen_eye',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ You can melt the ice by shooting an arrow through a
+ torch. The only way to find a line of sight for this
+ shot is to first spawn a Song of Time block, and then
+ stand on the very edge of it.
+ '''},
+ 'Spirit Temple Shifting Wall with No Additional Items': {
+ 'name' : 'logic_spirit_wall',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ The logic normally guarantees a way of dealing with both
+ the Beamos and the Walltula before climbing the wall.
+ '''},
+ 'Spirit Temple Main Room GS with Boomerang': {
+ 'name' : 'logic_spirit_lobby_gs',
+ 'tags' : ("Spirit Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ Standing on the highest part of the arm of the statue, a
+ precise Boomerang throw can kill and obtain this Gold
+ Skulltula. You must throw the Boomerang slightly off to
+ the side so that it curves into the Skulltula, as aiming
+ directly at it will clank off of the wall in front.
+ '''},
+ 'Spirit Temple Main Room Jump from Hands to Upper Ledges': {
+ 'name' : 'logic_spirit_lobby_jump',
+ 'tags' : ("Spirit Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ A precise jump to obtain the following as adult
+ without needing one of Hookshot or Hover Boots:
+ - Spirit Temple Statue Room Northeast Chest
+ - Spirit Temple GS Lobby
+ '''},
+ 'Spirit Temple MQ Sun Block Room GS with Boomerang': {
+ 'name' : 'logic_spirit_mq_sun_block_gs',
+ 'tags' : ("Spirit Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ Throw the Boomerang in such a way that it
+ curves through the side of the glass block
+ to hit the Gold Skulltula.
+ '''},
+ 'Jabu Scrub as Adult with Jump Dive': {
+ 'name' : 'logic_jabu_scrub_jump_dive',
+ 'tags' : ("Jabu Jabu's Belly", "Entrance",),
+ 'tooltip' : '''\
+ Standing above the underwater tunnel leading to the scrub,
+ jump down and swim through the tunnel. This allows adult to
+ access the scrub with no Scale or Iron Boots.
+ '''},
+ 'Jabu MQ Song of Time Block GS with Boomerang': {
+ 'name' : 'logic_jabu_mq_sot_gs',
+ 'tags' : ("Jabu Jabu's Belly", "Skulltulas",),
+ 'tooltip' : '''\
+ Allow the Boomerang to return to you through
+ the Song of Time block to grab the token.
+ '''},
+ 'Bottom of the Well MQ Dead Hand Freestanding Key with Boomerang': {
+ 'name' : 'logic_botw_mq_dead_hand_key',
+ 'tags' : ("Bottom of the Well",),
+ 'tooltip' : '''\
+ Boomerang can fish the item out of the rubble without
+ needing explosives to blow it up.
+ '''},
+ 'Fire Temple Flame Wall Maze Skip': {
+ 'name' : 'logic_fire_flame_maze',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ If you move quickly you can sneak past the edge of
+ a flame wall before it can rise up to block you.
+ To do it without taking damage is more precise.
+ Allows you to progress without needing either a
+ Small Key or Hover Boots.
+ '''},
+ 'Fire Temple MQ Flame Wall Maze Skip': {
+ 'name' : 'logic_fire_mq_flame_maze',
+ 'tags' : ("Fire Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ If you move quickly you can sneak past the edge of
+ a flame wall before it can rise up to block you.
+ To do it without taking damage is more precise.
+ Allows you to reach the side room GS without needing
+ Song of Time or Hover Boots. If either of "Fire Temple
+ MQ Lower to Upper Lizalfos Maze with Hover Boots" or
+ "with Precise Jump" are enabled, this also allows you
+ to progress deeper into the dungeon without Hookshot.
+ '''},
+ 'Fire Temple MQ Climb without Fire Source': {
+ 'name' : 'logic_fire_mq_climb',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ You can use the Hover Boots to hover around to
+ the climbable wall, skipping the need to use a
+ fire source and spawn a Hookshot target.
+ '''},
+ 'Fire Temple MQ Lower to Upper Lizalfos Maze with Hover Boots': {
+ 'name' : 'logic_fire_mq_maze_hovers',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ Use the Hover Boots off of a crate to
+ climb to the upper maze without needing
+ to spawn and use the Hookshot targets.
+ '''},
+ 'Fire Temple MQ Chest Near Boss without Breaking Crate': {
+ 'name' : 'logic_fire_mq_near_boss',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ The hitbox for the torch extends a bit outside of the crate.
+ Shoot a flaming arrow at the side of the crate to light the
+ torch without needing to get over there and break the crate.
+ '''},
+ 'Fire Temple MQ Lizalfos Maze Side Room without Box': {
+ 'name' : 'logic_fire_mq_maze_side_room',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ You can walk from the blue switch to the door and
+ quickly open the door before the bars reclose. This
+ skips needing to reach the upper sections of the
+ maze to get a box to place on the switch.
+ '''},
+ 'Fire Temple MQ Boss Key Chest without Bow': {
+ 'name' : 'logic_fire_mq_bk_chest',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ It is possible to light both of the timed torches
+ to unbar the door to the boss key chest's room
+ with just Din's Fire if you move very quickly
+ between the two torches. It is also possible to
+ unbar the door with just Din's by abusing an
+ oversight in the way the game counts how many
+ torches have been lit.
+ '''},
+ 'Fire Temple MQ Above Flame Wall Maze GS from Below with Longshot': {
+ 'name' : 'logic_fire_mq_above_maze_gs',
+ 'tags' : ("Fire Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ The floor of the room that contains this Skulltula
+ is only solid from above. From the maze below, the
+ Longshot can be shot through the ceiling to obtain
+ the token with two fewer small keys than normal.
+ '''},
+ 'Zora\'s River Lower Freestanding PoH as Adult with Nothing': {
+ 'name' : 'logic_zora_river_lower',
+ 'tags' : ("Zora's River",),
+ 'tooltip' : '''\
+ Adult can reach this PoH with a precise jump,
+ no Hover Boots required.
+ '''},
+ 'Water Temple Cracked Wall with Hover Boots': {
+ 'name' : 'logic_water_cracked_wall_hovers',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ With a midair side-hop while wearing the Hover
+ Boots, you can reach the cracked wall without
+ needing to raise the water up to the middle level.
+ '''},
+ 'Shadow Temple Freestanding Key with Bombchu': {
+ 'name' : 'logic_shadow_freestanding_key',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ Release the Bombchu with good timing so that
+ it explodes near the bottom of the pot.
+ '''},
+ 'Shadow Temple MQ Invisible Blades Silver Rupees without Song of Time': {
+ 'name' : 'logic_shadow_mq_invisible_blades',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ The Like Like can be used to boost you into the
+ silver rupee that normally requires Song of Time.
+ This cannot be performed on OHKO since the Like
+ Like does not boost you high enough if you die.
+ '''},
+ 'Shadow Temple MQ Lower Huge Pit without Fire Source': {
+ 'name' : 'logic_shadow_mq_huge_pit',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ Normally a frozen eye switch spawns some platforms
+ that you can use to climb down, but there's actually
+ a small piece of ground that you can stand on that
+ you can just jump down to.
+ '''},
+ 'Backflip over Mido as Adult': {
+ 'name' : 'logic_mido_backflip',
+ 'tags' : ("the Lost Woods",),
+ 'tooltip' : '''\
+ With a specific position and angle, you can
+ backflip over Mido.
+ '''},
+ 'Fire Temple Boss Door without Hover Boots or Pillar': {
+ 'name' : 'logic_fire_boss_door_jump',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ The Fire Temple Boss Door can be reached with a precise
+ jump. You must be touching the side wall of the room so
+ that Link will grab the ledge from farther away than
+ is normally possible.
+ '''},
+ 'Lake Hylia Lab Dive without Gold Scale': {
+ 'name' : 'logic_lab_diving',
+ 'tags' : ("Lake Hylia",),
+ 'tooltip' : '''\
+ Remove the Iron Boots in the midst of
+ Hookshotting the underwater crate.
+ '''},
+ 'Deliver Eye Drops with Bolero of Fire': {
+ 'name' : 'logic_biggoron_bolero',
+ 'tags' : ("Death Mountain Trail",),
+ 'tooltip' : '''\
+ Playing a warp song normally causes a trade item to
+ spoil immediately, however, it is possible use Bolero
+ to reach Biggoron and still deliver the Eye Drops
+ before they spoil. If you do not wear the Goron Tunic,
+ the heat timer inside the crater will override the trade
+ item's timer. When you exit to Death Mountain Trail you
+ will have one second to show the Eye Drops before they
+ expire. You can get extra time to show the Eye Drops if
+ you warp immediately upon receiving them. If you don't
+ have many hearts, you may have to reset the heat timer
+ by quickly dipping in and out of Darunia's chamber.
+ This trick does not apply if "Randomize Warp Song
+ Destinations" is enabled, or if the settings are such
+ that trade items do not need to be delivered within a
+ time limit.
+ '''},
+ 'Wasteland Crossing without Hover Boots or Longshot': {
+ 'name' : 'logic_wasteland_crossing',
+ 'tags' : ("Haunted Wasteland",),
+ 'tooltip' : '''\
+ You can beat the quicksand by backwalking across it
+ in a specific way.
+ '''},
+ 'Colossus Hill GS with Hookshot': {
+ 'name' : 'logic_colossus_gs',
+ 'tags' : ("Desert Colossus", "Skulltulas",),
+ 'tooltip' : '''\
+ Somewhat precise. If you kill enough Leevers
+ you can get enough of a break to take some time
+ to aim more carefully.
+ '''},
+ 'Dodongo\'s Cavern Scarecrow GS with Armos Statue': {
+ 'name' : 'logic_dc_scarecrow_gs',
+ 'tags' : ("Dodongo's Cavern", "Skulltulas",),
+ 'tooltip' : '''\
+ You can jump off an Armos Statue to reach the
+ alcove with the Gold Skulltula. It takes quite
+ a long time to pull the statue the entire way.
+ The jump to the alcove can be a bit picky when
+ done as child.
+ '''},
+ 'Kakariko Tower GS with Jump Slash': {
+ 'name' : 'logic_kakariko_tower_gs',
+ 'tags' : ("Kakariko Village", "Skulltulas",),
+ 'tooltip' : '''\
+ Climb the tower as high as you can without
+ touching the Gold Skulltula, then let go and
+ jump slash immediately. You will take fall
+ damage.
+ '''},
+ 'Deku Tree MQ Compass Room GS Boulders with Just Hammer': {
+ 'name' : 'logic_deku_mq_compass_gs',
+ 'tags' : ("Deku Tree", "Skulltulas",),
+ 'tooltip' : '''\
+ Climb to the top of the vines, then let go
+ and jump slash immediately to destroy the
+ boulders using the Hammer, without needing
+ to spawn a Song of Time block.
+ '''},
+ 'Lake Hylia Lab Wall GS with Jump Slash': {
+ 'name' : 'logic_lab_wall_gs',
+ 'tags' : ("Lake Hylia", "Skulltulas",),
+ 'tooltip' : '''\
+ The jump slash to actually collect the
+ token is somewhat precise.
+ '''},
+ 'Spirit Temple MQ Lower Adult without Fire Arrows': {
+ 'name' : 'logic_spirit_mq_lower_adult',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ It can be done with Din\'s Fire and Bow.
+ Whenever an arrow passes through a lit torch, it
+ resets the timer. It's finicky but it's also
+ possible to stand on the pillar next to the center
+ torch, which makes it easier.
+ '''},
+ 'Spirit Temple Map Chest with Bow': {
+ 'name' : 'logic_spirit_map_chest',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ To get a line of sight from the upper torch to
+ the map chest torches, you must pull an Armos
+ statue all the way up the stairs.
+ '''},
+ 'Spirit Temple Sun Block Room Chest with Bow': {
+ 'name' : 'logic_spirit_sun_chest',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ Using the blocks in the room as platforms you can
+ get lines of sight to all three torches. The timer
+ on the torches is quite short so you must move
+ quickly in order to light all three.
+ '''},
+ 'Spirit Temple MQ Sun Block Room as Child without Song of Time': {
+ 'name' : 'logic_spirit_mq_sun_block_sot',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ While adult can easily jump directly to the switch that
+ unbars the door to the sun block room, child Link cannot
+ make the jump without spawning a Song of Time block to
+ jump from. You can skip this by throwing the crate down
+ onto the switch from above, which does unbar the door,
+ however the crate immediately breaks, so you must move
+ quickly to get through the door before it closes back up.
+ '''},
+ 'Shadow Trial MQ Torch with Bow': {
+ 'name' : 'logic_shadow_trial_mq',
+ 'tags' : ("Ganon's Castle",),
+ 'tooltip' : '''\
+ You can light the torch in this room without a fire
+ source by shooting an arrow through the lit torch
+ at the beginning of the room. Because the room is
+ so dark and the unlit torch is so far away, it can
+ be difficult to aim the shot correctly.
+ '''},
+ 'Forest Temple NE Outdoors Ledge with Hover Boots': {
+ 'name' : 'logic_forest_outdoors_ledge',
+ 'tags' : ("Forest Temple", "Entrance",),
+ 'tooltip' : '''\
+ With precise Hover Boots movement you can fall down
+ to this ledge from upper balconies. If done precisely
+ enough, it is not necessary to take fall damage.
+ In MQ, this skips a Longshot requirement.
+ In Vanilla, this can skip a Hookshot requirement in
+ entrance randomizer.
+ '''},
+ 'Water Temple Boss Key Region with Hover Boots': {
+ 'name' : 'logic_water_boss_key_region',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ With precise Hover Boots movement it is possible
+ to reach the boss key chest's region without
+ needing the Longshot. It is not necessary to take
+ damage from the spikes. The Gold Skulltula Token
+ in the following room can also be obtained with
+ just the Hover Boots.
+ '''},
+ 'Water Temple MQ North Basement GS without Small Key': {
+ 'name' : 'logic_water_mq_locked_gs',
+ 'tags' : ("Water Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ There is an invisible Hookshot target that can be used
+ to get over the gate that blocks you from going to this
+ Skulltula early. This avoids going through some rooms
+ that normally require a Small Key to access. If "Water
+ Temple North Basement Ledge with Precise Jump" is not
+ enabled, this also skips needing Hover Boots or
+ Scarecrow's Song to reach the locked door.
+ '''},
+ 'Water Temple Falling Platform Room GS with Hookshot': {
+ 'name' : 'logic_water_falling_platform_gs_hookshot',
+ 'tags' : ("Water Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ If you stand on the very edge of the platform, this
+ Gold Skulltula can be obtained with only the Hookshot.
+ '''},
+ 'Water Temple Falling Platform Room GS with Boomerang': {
+ 'name' : 'logic_water_falling_platform_gs_boomerang',
+ 'tags' : ("Water Temple", "Skulltulas", "Entrance",),
+ 'tooltip' : '''\
+ If you stand on the very edge of the platform, this
+ Gold Skulltula can be obtained with only the Boomerang.
+ '''},
+ 'Water Temple River GS without Iron Boots': {
+ 'name' : 'logic_water_river_gs',
+ 'tags' : ("Water Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ Standing on the exposed ground toward the end of
+ the river, a precise Longshot use can obtain the
+ token. The Longshot cannot normally reach far
+ enough to kill the Skulltula, however. You'll
+ first have to find some other way of killing it.
+ '''},
+ 'Water Temple Entry without Iron Boots using Hookshot': {
+ 'name' : 'logic_water_hookshot_entry',
+ 'tags' : ("Lake Hylia",),
+ 'tooltip' : '''\
+ When entering Water Temple using Gold Scale instead
+ of Iron Boots, the Longshot is usually used to be
+ able to hit the switch and open the gate. But, by
+ standing in a particular spot, the switch can be hit
+ with only the reach of the Hookshot.
+ '''},
+ 'Death Mountain Trail Climb with Hover Boots': {
+ 'name' : 'logic_dmt_climb_hovers',
+ 'tags' : ("Death Mountain Trail",),
+ 'tooltip' : '''\
+ It is possible to use the Hover Boots to bypass
+ needing to destroy the boulders blocking the path
+ to the top of Death Mountain.
+ '''},
+ 'Death Mountain Trail Upper Red Rock GS without Hammer': {
+ 'name' : 'logic_trail_gs_upper',
+ 'tags' : ("Death Mountain Trail", "Skulltulas",),
+ 'tooltip' : '''\
+ After killing the Skulltula, the token can be collected
+ by backflipping into the rock at the correct angle.
+ '''},
+ 'Death Mountain Trail Lower Red Rock GS with Hookshot': {
+ 'name' : 'logic_trail_gs_lower_hookshot',
+ 'tags' : ("Death Mountain Trail", "Skulltulas",),
+ 'tooltip' : '''\
+ After killing the Skulltula, the token can be fished
+ out of the rock without needing to destroy it, by
+ using the Hookshot in the correct way.
+ '''},
+ 'Death Mountain Trail Lower Red Rock GS with Hover Boots': {
+ 'name' : 'logic_trail_gs_lower_hovers',
+ 'tags' : ("Death Mountain Trail", "Skulltulas",),
+ 'tooltip' : '''\
+ After killing the Skulltula, the token can be
+ collected without needing to destroy the rock by
+ backflipping down onto it with the Hover Boots.
+ First use the Hover Boots to stand on a nearby
+ fence, and go for the Skulltula Token from there.
+ '''},
+ 'Death Mountain Trail Lower Red Rock GS with Magic Bean': {
+ 'name' : 'logic_trail_gs_lower_bean',
+ 'tags' : ("Death Mountain Trail", "Skulltulas",),
+ 'tooltip' : '''\
+ After killing the Skulltula, the token can be
+ collected without needing to destroy the rock by
+ jumping down onto it from the bean plant,
+ midflight, with precise timing and positioning.
+ '''},
+ 'Death Mountain Crater Upper to Lower with Hammer': {
+ 'name' : 'logic_crater_upper_to_lower',
+ 'tags' : ("Death Mountain Crater",),
+ 'tooltip' : '''\
+ With the Hammer, you can jump slash the rock twice
+ in the same jump in order to destroy it before you
+ fall into the lava.
+ '''},
+ 'Zora\'s Domain Entry with Hover Boots': {
+ 'name' : 'logic_zora_with_hovers',
+ 'tags' : ("Zora's River",),
+ 'tooltip' : '''\
+ Can hover behind the waterfall as adult.
+ '''},
+ 'Zora\'s Domain GS with No Additional Items': {
+ 'name' : 'logic_domain_gs',
+ 'tags' : ("Zora's Domain", "Skulltulas",),
+ 'tooltip' : '''\
+ A precise jump slash can kill the Skulltula and
+ recoil back onto the top of the frozen waterfall.
+ To kill it, the logic normally guarantees one of
+ Hookshot, Bow, or Magic.
+ '''},
+ 'Shadow Temple River Statue with Bombchu': {
+ 'name' : 'logic_shadow_statue',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ By sending a Bombchu around the edge of the
+ gorge, you can knock down the statue without
+ needing a Bow.
+ Applies in both vanilla and MQ Shadow.
+ '''},
+ 'Stop Link the Goron with Din\'s Fire': {
+ 'name' : 'logic_link_goron_dins',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ The timing is quite awkward.
+ '''},
+ 'Fire Temple Song of Time Room GS without Song of Time': {
+ 'name' : 'logic_fire_song_of_time',
+ 'tags' : ("Fire Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ A precise jump can be used to reach this room.
+ '''},
+ 'Fire Temple Climb without Strength': {
+ 'name' : 'logic_fire_strength',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ A precise jump can be used to skip
+ pushing the block.
+ '''},
+ 'Fire Temple MQ Big Lava Room Blocked Door without Hookshot': {
+ 'name' : 'logic_fire_mq_blocked_chest',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ There is a gap between the hitboxes of the flame
+ wall in the big lava room. If you know where this
+ gap is located, you can jump through it and skip
+ needing to use the Hookshot. To do this without
+ taking damage is more precise.
+ '''},
+ 'Fire Temple MQ Lower to Upper Lizalfos Maze with Precise Jump': {
+ 'name' : 'logic_fire_mq_maze_jump',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ A precise jump off of a crate can be used to
+ climb to the upper maze without needing to spawn
+ and use the Hookshot targets. This trick
+ supersedes both "Fire Temple MQ Lower to Upper
+ Lizalfos Maze with Hover Boots" and "Fire Temple
+ MQ Lizalfos Maze Side Room without Box".
+ '''},
+ 'Light Trial MQ without Hookshot': {
+ 'name' : 'logic_light_trial_mq',
+ 'tags' : ("Ganon's Castle",),
+ 'tooltip' : '''\
+ If you move quickly you can sneak past the edge of
+ a flame wall before it can rise up to block you.
+ In this case to do it without taking damage is
+ especially precise.
+ '''},
+ 'Ice Cavern MQ Scarecrow GS with No Additional Items': {
+ 'name' : 'logic_ice_mq_scarecrow',
+ 'tags' : ("Ice Cavern", "Skulltulas",),
+ 'tooltip' : '''\
+ A precise jump can be used to reach this alcove.
+ '''},
+ 'Ice Cavern MQ Red Ice GS without Song of Time': {
+ 'name' : 'logic_ice_mq_red_ice_gs',
+ 'tags' : ("Ice Cavern", "Skulltulas",),
+ 'tooltip' : '''\
+ If you side-hop into the perfect position, you
+ can briefly stand on the platform with the red
+ ice just long enough to dump some blue fire.
+ '''},
+ 'Ice Cavern Block Room GS with Hover Boots': {
+ 'name' : 'logic_ice_block_gs',
+ 'tags' : ("Ice Cavern", "Skulltulas",),
+ 'tooltip' : '''\
+ The Hover Boots can be used to get in front of the
+ Skulltula to kill it with a jump slash. Then, the
+ Hover Boots can again be used to obtain the Token,
+ all without Hookshot or Boomerang.
+ '''},
+ 'Reverse Wasteland': {
+ 'name' : 'logic_reverse_wasteland',
+ 'tags' : ("Haunted Wasteland",),
+ 'tooltip' : '''\
+ By memorizing the path, you can travel through the
+ Wasteland in reverse.
+ Note that jumping to the carpet merchant as child
+ typically requires a fairly precise jump slash.
+ The equivalent trick for going forward through the
+ Wasteland is "Lensless Wasteland".
+ To cross the river of sand with no additional items,
+ be sure to also enable "Wasteland Crossing without
+ Hover Boots or Longshot".
+ Unless all overworld entrances are randomized, child
+ Link will not be expected to do anything at Gerudo's
+ Fortress.
+ '''},
+ 'Zora\'s River Upper Freestanding PoH as Adult with Nothing': {
+ 'name' : 'logic_zora_river_upper',
+ 'tags' : ("Zora's River",),
+ 'tooltip' : '''\
+ Adult can reach this PoH with a precise jump,
+ no Hover Boots required.
+ '''},
+ 'Shadow Temple MQ Truth Spinner Gap with Longshot': {
+ 'name' : 'logic_shadow_mq_gap',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ You can Longshot a torch and jump-slash recoil onto
+ the tongue. It works best if you Longshot the right
+ torch from the left side of the room.
+ '''},
+ 'Lost Woods Adult GS without Bean': {
+ 'name' : 'logic_lost_woods_gs_bean',
+ 'tags' : ("the Lost Woods", "Skulltulas",),
+ 'tooltip' : '''\
+ You can collect the token with a precise
+ Hookshot use, as long as you can kill the
+ Skulltula somehow first. It can be killed
+ using Longshot, Bow, Bombchus or Din's Fire.
+ '''},
+ 'Jabu Near Boss GS without Boomerang as Adult': {
+ 'name' : 'logic_jabu_boss_gs_adult',
+ 'tags' : ("Jabu Jabu's Belly", "Skulltulas", "Entrance",),
+ 'tooltip' : '''\
+ You can easily get over to the door to the
+ near boss area early with Hover Boots. The
+ tricky part is getting through the door
+ without being able to use a box to keep the
+ switch pressed. One way is to quickly roll
+ from the switch and open the door before it
+ closes.
+ '''},
+ 'Kakariko Rooftop GS with Hover Boots': {
+ 'name' : 'logic_kakariko_rooftop_gs',
+ 'tags' : ("Kakariko Village", "Skulltulas",),
+ 'tooltip' : '''\
+ Take the Hover Boots from the entrance to Impa's
+ House over to the rooftop of Skulltula House. From
+ there, a precise Hover Boots backwalk with backflip
+ can be used to get onto a hill above the side of
+ the village. And then from there you can Hover onto
+ Impa's rooftop to kill the Skulltula and backflip
+ into the token.
+ '''},
+ 'Graveyard Freestanding PoH with Boomerang': {
+ 'name' : 'logic_graveyard_poh',
+ 'tags' : ("the Graveyard",),
+ 'tooltip' : '''\
+ Using a precise moving setup you can obtain
+ the Piece of Heart by having the Boomerang
+ interact with it along the return path.
+ '''},
+ 'Hyrule Castle Storms Grotto GS with Just Boomerang': {
+ 'name' : 'logic_castle_storms_gs',
+ 'tags' : ("Hyrule Castle", "Skulltulas",),
+ 'tooltip' : '''\
+ With precise throws, the Boomerang alone can
+ kill the Skulltula and collect the token,
+ without first needing to blow up the wall.
+ '''},
+ 'Death Mountain Trail Soil GS without Destroying Boulder': {
+ 'name' : 'logic_dmt_soil_gs',
+ 'tags' : ("Death Mountain Trail", "Skulltulas",),
+ 'tooltip' : '''\
+ Bugs will go into the soft soil even while the boulder is
+ still blocking the entrance.
+ Then, using a precise moving setup you can kill the Gold
+ Skulltula and obtain the token by having the Boomerang
+ interact with it along the return path.
+ '''},
+ 'Gerudo Training Grounds Left Side Silver Rupees without Hookshot': {
+ 'name' : 'logic_gtg_without_hookshot',
+ 'tags' : ("Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ After collecting the rest of the silver rupees in the room,
+ you can reach the final silver rupee on the ceiling by being
+ pulled up into it after getting grabbed by the Wallmaster.
+ Then, you must also reach the exit of the room without the
+ use of the Hookshot. If you move quickly you can sneak past
+ the edge of a flame wall before it can rise up to block you.
+ To do so without taking damage is more precise.
+ '''},
+ 'Gerudo Training Grounds MQ Left Side Silver Rupees without Hookshot': {
+ 'name' : 'logic_gtg_mq_without_hookshot',
+ 'tags' : ("Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ After collecting the rest of the silver rupees in the room,
+ you can reach the final silver rupee on the ceiling by being
+ pulled up into it after getting grabbed by the Wallmaster.
+ The Wallmaster will not track you to directly underneath the
+ rupee. You should take the last step to be under the rupee
+ after the Wallmaster has begun its attempt to grab you.
+ Also included with this trick is that fact that the switch
+ that unbars the door to the final chest of GTG can be hit
+ without a projectile, using a precise jump slash.
+ This trick supersedes "Gerudo Training Grounds MQ Left Side
+ Silver Rupees with Hookshot".
+ '''},
+ 'Reach Gerudo Training Grounds Fake Wall Ledge with Hover Boots': {
+ 'name' : 'logic_gtg_fake_wall',
+ 'tags' : ("Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ A precise Hover Boots use from the top of the chest can allow
+ you to grab the ledge without needing the usual requirements.
+ In Master Quest, this always skips a Song of Time requirement.
+ In Vanilla, this skips a Hookshot requirement, but is only
+ relevant if "Gerudo Training Grounds Left Side Silver Rupees
+ without Hookshot" is enabled.
+ '''},
+ 'Water Temple Cracked Wall with No Additional Items': {
+ 'name' : 'logic_water_cracked_wall_nothing',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ A precise jump slash (among other methods) will
+ get you to the cracked wall without needing the
+ Hover Boots or to raise the water to the middle
+ level. This trick supersedes "Water Temple
+ Cracked Wall with Hover Boots".
+ '''},
+ 'Water Temple North Basement Ledge with Precise Jump': {
+ 'name' : 'logic_water_north_basement_ledge_jump',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ In the northern basement there's a ledge from where, in
+ vanilla Water Temple, boulders roll out into the room.
+ Normally to jump directly to this ledge logically
+ requires the Hover Boots, but with precise jump, it can
+ be done without them. This trick applies to both
+ Vanilla and Master Quest.
+ '''},
+ 'Water Temple Torch Longshot': {
+ 'name' : 'logic_water_temple_torch_longshot',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ Stand on the eastern side of the central pillar and longshot
+ the torches on the bottom level. Swim through the corridor
+ and float up to the top level. This allows access to this
+ area and lower water levels without Iron Boots.
+ The majority of the tricks that allow you to skip Iron Boots
+ in the Water Temple are not going to be relevant unless this
+ trick is first enabled.
+ '''},
+ 'Water Temple Central Pillar GS with Farore\'s Wind': {
+ 'name' : 'logic_water_central_gs_fw',
+ 'tags' : ("Water Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ If you set Farore's Wind inside the central pillar
+ and then return to that warp point after raising
+ the water to the highest level, you can obtain this
+ Skulltula Token with Hookshot or Boomerang.
+ '''},
+ 'Water Temple Central Pillar GS with Iron Boots': {
+ 'name' : 'logic_water_central_gs_irons',
+ 'tags' : ("Water Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ After opening the middle water level door into the
+ central pillar, the door will stay unbarred so long
+ as you do not leave the room -- even if you were to
+ raise the water up to the highest level. With the
+ Iron Boots to go through the door after the water has
+ been raised, you can obtain the Skulltula Token with
+ the Hookshot.
+ '''},
+ 'Water Temple Boss Key Jump Dive': {
+ 'name' : 'logic_water_bk_jump_dive',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ Stand on the very edge of the raised corridor leading from the
+ push block room to the rolling boulder corridor. Face the
+ gold skulltula on the waterfall and jump over the boulder
+ corridor floor into the pool of water, swimming right once
+ underwater. This allows access to the boss key room without
+ Iron boots.
+ '''},
+ 'Water Temple Dragon Statue Jump Dive': {
+ 'name' : 'logic_water_dragon_jump_dive',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ If you come into the dragon statue room from the
+ serpent river, you can jump down from above and get
+ into the tunnel without needing either Iron Boots
+ or a Scale. This trick applies to both Vanilla and
+ Master Quest. In Vanilla, you must shoot the switch
+ from above with the Bow, and then quickly get
+ through the tunnel before the gate closes.
+ '''},
+ 'Water Temple Dragon Statue Switch from Above the Water as Adult': {
+ 'name' : 'logic_water_dragon_adult',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ Normally you need both Hookshot and Iron Boots to hit the
+ switch and swim through the tunnel to get to the chest. But
+ by hitting the switch from dry land, using one of Bombchus,
+ Hookshot, or Bow, it is possible to skip one or both of
+ those requirements. After the gate has been opened, besides
+ just using the Iron Boots, a well-timed dive with at least
+ the Silver Scale could be used to swim through the tunnel. If
+ coming from the serpent river, a jump dive can also be used
+ to get into the tunnel.
+ '''},
+ 'Water Temple Dragon Statue Switch from Above the Water as Child': {
+ 'name' : 'logic_water_dragon_child',
+ 'tags' : ("Water Temple", "Entrance",),
+ 'tooltip' : '''\
+ It is possible for child to hit the switch from dry land
+ using one of Bombchus, Slingshot or Boomerang. Then, to
+ get to the chest, child can dive through the tunnel using
+ at least the Silver Scale. The timing and positioning of
+ this dive needs to be perfect to actually make it under the
+ gate, and it all needs to be done very quickly to be able to
+ get through before the gate closes. Be sure to enable "Water
+ Temple Dragon Statue Switch from Above the Water as Adult"
+ for adult's variant of this trick.
+ '''},
+ 'Goron City Maze Left Chest with Hover Boots': {
+ 'name' : 'logic_goron_city_leftmost',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ A precise backwalk starting from on top of the
+ crate and ending with a precisely-timed backflip
+ can reach this chest without needing either
+ the Hammer or Silver Gauntlets.
+ '''},
+ 'Goron City Grotto with Hookshot While Taking Damage': {
+ 'name' : 'logic_goron_grotto',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ It is possible to reach the Goron City Grotto by
+ quickly using the Hookshot while in the midst of
+ taking damage from the lava floor. This trick will
+ not be expected on OHKO or quadruple damage.
+ '''},
+ 'Deku Tree Basement without Slingshot': {
+ 'name' : 'logic_deku_b1_skip',
+ 'tags' : ("Deku Tree",),
+ 'tooltip' : '''\
+ A precise jump can be used to skip
+ needing to use the Slingshot to go
+ around B1 of the Deku Tree. If used
+ with the "Closed Forest" setting, a
+ Slingshot will not be guaranteed to
+ exist somewhere inside the Forest.
+ This trick applies to both Vanilla
+ and Master Quest.
+ '''},
+ 'Spirit Temple Lower Adult Switch with Bombs': {
+ 'name' : 'logic_spirit_lower_adult_switch',
+ 'tags' : ("Spirit Temple",),
+ 'tooltip' : '''\
+ A bomb can be used to hit the switch on the ceiling,
+ but it must be thrown from a particular distance
+ away and with precise timing.
+ '''},
+ 'Forest Temple Outside Backdoor with Jump Slash': {
+ 'name' : 'logic_forest_outside_backdoor',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ With a precise jump slash from above,
+ you can reach the backdoor to the west
+ courtyard without Hover Boots. Applies
+ to both Vanilla and Master Quest.
+ '''},
+ 'Forest Temple East Courtyard Door Frame with Hover Boots': {
+ 'name' : 'logic_forest_door_frame',
+ 'tags' : ("Forest Temple",),
+ 'tooltip' : '''\
+ A precise Hover Boots movement from the upper
+ balconies in this courtyard can be used to get on
+ top of the door frame. Applies to both Vanilla and
+ Master Quest. In Vanilla, from on top the door
+ frame you can summon Pierre, allowing you to access
+ the falling ceiling room early. In Master Quest,
+ this allows you to obtain the GS on the door frame
+ as adult without Hookshot or Song of Time.
+ '''},
+ 'Dodongo\'s Cavern MQ Early Bomb Bag Area as Child': {
+ 'name' : 'logic_dc_mq_child_bombs',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ With a precise jump slash from above, you
+ can reach the Bomb Bag area as only child
+ without needing a Slingshot. You will
+ take fall damage.
+ '''},
+ 'Dodongo\'s Cavern Two Scrub Room with Strength': {
+ 'name' : 'logic_dc_scrub_room',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ With help from a conveniently-positioned block,
+ Adult can quickly carry a bomb flower over to
+ destroy the mud wall blocking the room with two
+ Deku Scrubs.
+ '''},
+ 'Dodongo\'s Cavern Child Slingshot Skips': {
+ 'name' : 'logic_dc_slingshot_skip',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ With precise platforming, child can cross the
+ platforms while the flame circles are there.
+ When enabling this trick, it's recommended that
+ you also enable the Adult variant: "Dodongo's
+ Cavern Spike Trap Room Jump without Hover Boots".
+ '''},
+ 'Dodongo\'s Cavern MQ Light the Eyes with Strength': {
+ 'name' : 'logic_dc_mq_eyes',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ If you move very quickly, it is possible to use
+ the bomb flower at the top of the room to light
+ the eyes. To perform this trick as child is
+ significantly more difficult, but child will never
+ be expected to do so unless "Dodongo's Cavern MQ
+ Back Areas as Child without Explosives" is enabled.
+ Also, the bombable floor before King Dodongo can be
+ destroyed with Hammer if hit in the very center.
+ '''},
+ 'Dodongo\'s Cavern MQ Back Areas as Child without Explosives': {
+ 'name' : 'logic_dc_mq_child_back',
+ 'tags' : ("Dodongo's Cavern",),
+ 'tooltip' : '''\
+ Child can progress through the back areas without
+ explosives by throwing a pot at a switch to lower a
+ fire wall, and by defeating Armos to detonate bomb
+ flowers (among other methods). While these techniques
+ themselves are relatively simple, they're not
+ relevant unless "Dodongo's Cavern MQ Light the Eyes
+ with Strength" is enabled, which is a trick that
+ is particularly difficult for child to perform.
+ '''},
+ 'Rolling Goron (Hot Rodder Goron) as Child with Strength': {
+ 'name' : 'logic_child_rolling_with_strength',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ Use the bombflower on the stairs or near Medigoron.
+ Timing is tight, especially without backwalking.
+ '''},
+ 'Goron City Spinning Pot PoH with Bombchu': {
+ 'name' : 'logic_goron_city_pot',
+ 'tags' : ("Goron City",),
+ 'tooltip' : '''\
+ A Bombchu can be used to stop the spinning
+ pot, but it can be quite finicky to get it
+ to work.
+ '''},
+ 'Gerudo Valley Crate PoH as Adult with Hover Boots': {
+ 'name' : 'logic_valley_crate_hovers',
+ 'tags' : ("Gerudo Valley",),
+ 'tooltip' : '''\
+ From the far side of Gerudo Valley, a precise
+ Hover Boots movement and jump-slash recoil can
+ allow adult to reach the ledge with the crate
+ PoH without needing Longshot. You will take
+ fall damage.
+ '''},
+ 'Jump onto the Lost Woods Bridge as Adult with Nothing': {
+ 'name' : 'logic_lost_woods_bridge',
+ 'tags' : ("the Lost Woods", "Entrance",),
+ 'tooltip' : '''\
+ With very precise movement it's possible for
+ adult to jump onto the bridge without needing
+ Longshot, Hover Boots, or Bean.
+ '''},
+ 'Spirit Trial without Hookshot': {
+ 'name' : 'logic_spirit_trial_hookshot',
+ 'tags' : ("Ganon's Castle",),
+ 'tooltip' : '''\
+ A precise jump off of an Armos can
+ collect the highest rupee.
+ '''},
+ 'Shadow Temple Stone Umbrella Skip': {
+ 'name' : 'logic_shadow_umbrella',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ A very precise Hover Boots movement
+ from off of the lower chest can get you
+ on top of the crushing spikes without
+ needing to pull the block. Applies to
+ both Vanilla and Master Quest.
+ '''},
+ 'Shadow Temple Falling Spikes GS with Hover Boots': {
+ 'name' : 'logic_shadow_umbrella_gs',
+ 'tags' : ("Shadow Temple", "Skulltulas",),
+ 'tooltip' : '''\
+ After killing the Skulltula, a very precise Hover Boots
+ movement from off of the lower chest can get you on top
+ of the crushing spikes without needing to pull the block.
+ From there, another very precise Hover Boots movement can
+ be used to obtain the token without needing the Hookshot.
+ Applies to both Vanilla and Master Quest. For obtaining
+ the chests in this room with just Hover Boots, be sure to
+ enable "Shadow Temple Stone Umbrella Skip".
+ '''},
+ 'Water Temple Central Bow Target without Longshot or Hover Boots': {
+ 'name' : 'logic_water_central_bow',
+ 'tags' : ("Water Temple",),
+ 'tooltip' : '''\
+ A very precise Bow shot can hit the eye
+ switch from the floor above. Then, you
+ can jump down into the hallway and make
+ through it before the gate closes.
+ It can also be done as child, using the
+ Slingshot instead of the Bow.
+ '''},
+ 'Fire Temple East Tower without Scarecrow\'s Song': {
+ 'name' : 'logic_fire_scarecrow',
+ 'tags' : ("Fire Temple",),
+ 'tooltip' : '''\
+ Also known as "Pixelshot".
+ The Longshot can reach the target on the elevator
+ itself, allowing you to skip needing to spawn the
+ scarecrow.
+ '''},
+ 'Fire Trial MQ with Hookshot': {
+ 'name' : 'logic_fire_trial_mq',
+ 'tags' : ("Ganon's Castle",),
+ 'tooltip' : '''\
+ It's possible to hook the target at the end of
+ fire trial with just Hookshot, but it requires
+ precise aim and perfect positioning. The main
+ difficulty comes from getting on the very corner
+ of the obelisk without falling into the lava.
+ '''},
+ 'Shadow Temple Entry with Fire Arrows': {
+ 'name' : 'logic_shadow_fire_arrow_entry',
+ 'tags' : ("Shadow Temple",),
+ 'tooltip' : '''\
+ It is possible to light all of the torches to
+ open the Shadow Temple entrance with just Fire
+ Arrows, but you must be very quick, precise,
+ and strategic with how you take your shots.
+ '''},
+ 'Lensless Wasteland': {
+ 'name' : 'logic_lens_wasteland',
+ 'tags' : ("Lens of Truth","Haunted Wasteland",),
+ 'tooltip' : '''\
+ By memorizing the path, you can travel through the
+ Wasteland without using the Lens of Truth to see
+ the Poe.
+ The equivalent trick for going in reverse through
+ the Wasteland is "Reverse Wasteland".
+ '''},
+ 'Bottom of the Well without Lens of Truth': {
+ 'name' : 'logic_lens_botw',
+ 'tags' : ("Lens of Truth","Bottom of the Well",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Bottom of the Well.
+ '''},
+ 'Ganon\'s Castle MQ without Lens of Truth': {
+ 'name' : 'logic_lens_castle_mq',
+ 'tags' : ("Lens of Truth","Ganon's Castle",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Ganon's Castle MQ.
+ '''},
+ 'Ganon\'s Castle without Lens of Truth': {
+ 'name' : 'logic_lens_castle',
+ 'tags' : ("Lens of Truth","Ganon's Castle",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Ganon's Castle.
+ '''},
+ 'Gerudo Training Grounds MQ without Lens of Truth': {
+ 'name' : 'logic_lens_gtg_mq',
+ 'tags' : ("Lens of Truth","Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Gerudo Training Grounds MQ.
+ '''},
+ 'Gerudo Training Grounds without Lens of Truth': {
+ 'name' : 'logic_lens_gtg',
+ 'tags' : ("Lens of Truth","Gerudo Training Grounds",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Gerudo Training Grounds.
+ '''},
+ 'Jabu MQ without Lens of Truth': {
+ 'name' : 'logic_lens_jabu_mq',
+ 'tags' : ("Lens of Truth","Jabu Jabu's Belly",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Jabu MQ.
+ '''},
+ 'Shadow Temple MQ before Invisible Moving Platform without Lens of Truth': {
+ 'name' : 'logic_lens_shadow_mq',
+ 'tags' : ("Lens of Truth","Shadow Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Shadow Temple MQ before the invisible moving platform.
+ '''},
+ 'Shadow Temple MQ beyond Invisible Moving Platform without Lens of Truth': {
+ 'name' : 'logic_lens_shadow_mq_back',
+ 'tags' : ("Lens of Truth","Shadow Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Shadow Temple MQ beyond the invisible moving platform.
+ '''},
+ 'Shadow Temple before Invisible Moving Platform without Lens of Truth': {
+ 'name' : 'logic_lens_shadow',
+ 'tags' : ("Lens of Truth","Shadow Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Shadow Temple before the invisible moving platform.
+ '''},
+ 'Shadow Temple beyond Invisible Moving Platform without Lens of Truth': {
+ 'name' : 'logic_lens_shadow_back',
+ 'tags' : ("Lens of Truth","Shadow Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Shadow Temple beyond the invisible moving platform.
+ '''},
+ 'Spirit Temple MQ without Lens of Truth': {
+ 'name' : 'logic_lens_spirit_mq',
+ 'tags' : ("Lens of Truth","Spirit Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Spirit Temple MQ.
+ '''},
+ 'Spirit Temple without Lens of Truth': {
+ 'name' : 'logic_lens_spirit',
+ 'tags' : ("Lens of Truth","Spirit Temple",),
+ 'tooltip' : '''\
+ Removes the requirements for the Lens of Truth
+ in Spirit Temple.
+ '''},
+}
+
+normalized_name_tricks = {trick.casefold(): info for (trick, info) in known_logic_tricks.items()}
\ No newline at end of file
diff --git a/worlds/oot/MQ.py b/worlds/oot/MQ.py
new file mode 100644
index 00000000..3120891e
--- /dev/null
+++ b/worlds/oot/MQ.py
@@ -0,0 +1,732 @@
+# mzxrules 2018
+# In order to patch MQ to the existing data...
+#
+# Scenes:
+#
+# Ice Cavern (Scene 9) needs to have it's header altered to support MQ's path list. This
+# expansion will delete the otherwise unused alternate headers command
+#
+# Transition actors will be patched over the old data, as the number of records is the same
+# Path data will be appended to the end of the scene file.
+#
+# The size of a single path on file is NUM_POINTS * 6, rounded up to the nearest 4 byte boundary
+# The total size consumed by the path data is NUM_PATHS * 8, plus the sum of all path file sizes
+# padded to the nearest 0x10 bytes
+#
+# Collision:
+# OoT's collision data consists of these elements: vertices, surface types, water boxes,
+# camera behavior data, and polys. MQ's vertice and polygon geometry data is identical.
+# However, the surface types and the collision exclusion flags bound to the polys have changed
+# for some polygons, as well as the number of surface type records and camera type records.
+#
+# To patch collision, a flag denotes whether collision data cannot be written in place without
+# expanding the size of the scene file. If true, the camera data is relocated to the end
+# of the scene file, and the surface types are shifted down into where the camera types
+# were situated. If false, the camera data isn't moved, but rather the surface type list
+# will be shifted to the end of the camera data
+#
+# Rooms:
+#
+# Object file initialization data will be appended to the end of the room file.
+# The total size consumed by the object file data is NUM_OBJECTS * 0x02, aligned to
+# the nearest 0x04 bytes
+#
+# Actor spawn data will be appended to the end of the room file, after the objects.
+# The total size consumed by the actor spawn data is NUM_ACTORS * 0x10
+#
+# Finally:
+#
+# Scene and room files will be padded to the nearest 0x10 bytes
+#
+# Maps:
+# Jabu Jabu's B1 map contains no chests in the vanilla layout. Because of this,
+# the floor map data is missing a vertex pointer that would point within kaleido_scope.
+# As such, if the file moves, the patch will break.
+
+from .Utils import data_path
+from .Rom import Rom
+import json
+from struct import pack, unpack
+
+SCENE_TABLE = 0xB71440
+
+
+class File(object):
+ def __init__(self, file):
+ self.name = file['Name']
+ self.start = int(file['Start'], 16) if 'Start' in file else 0
+ self.end = int(file['End'], 16) if 'End' in file else self.start
+ self.remap = file['RemapStart'] if 'RemapStart' in file else None
+ self.from_file = self.start
+
+ # used to update the file's associated dmadata record
+ self.dma_key = self.start
+
+ if self.remap is not None:
+ self.remap = int(self.remap, 16)
+
+ def __repr__(self):
+ remap = "None"
+ if self.remap is not None:
+ remap = "{0:x}".format(self.remap)
+ return "{0}: {1:x} {2:x}, remap {3}".format(self.name, self.start, self.end, remap)
+
+ def relocate(self, rom:Rom):
+ if self.remap is None:
+ self.remap = rom.free_space()
+
+ new_start = self.remap
+
+ offset = new_start - self.start
+ new_end = self.end + offset
+
+ rom.buffer[new_start:new_end] = rom.buffer[self.start:self.end]
+ self.start = new_start
+ self.end = new_end
+ update_dmadata(rom, self)
+
+ # The file will now refer to the new copy of the file
+ def copy(self, rom:Rom):
+ self.dma_key = None
+ self.relocate(rom)
+
+
+class CollisionMesh(object):
+ def __init__(self, rom:Rom, start, offset):
+ self.offset = offset
+ self.poly_addr = rom.read_int32(start + offset + 0x18)
+ self.polytypes_addr = rom.read_int32(start + offset + 0x1C)
+ self.camera_data_addr = rom.read_int32(start + offset + 0x20)
+ self.polytypes = (self.poly_addr - self.polytypes_addr) // 8
+
+ def write_to_scene(self, rom:Rom, start):
+ addr = start + self.offset + 0x18
+ rom.write_int32s(addr, [self.poly_addr, self.polytypes_addr, self.camera_data_addr])
+
+
+class ColDelta(object):
+ def __init__(self, delta):
+ self.is_larger = delta['IsLarger']
+ self.polys = delta['Polys']
+ self.polytypes = delta['PolyTypes']
+ self.cams = delta['Cams']
+
+
+class Icon(object):
+ def __init__(self, data):
+ self.icon = data["Icon"];
+ self.count = data["Count"];
+ self.points = [IconPoint(x) for x in data["IconPoints"]]
+
+ def write_to_minimap(self, rom:Rom, addr):
+ rom.write_sbyte(addr, self.icon)
+ rom.write_byte(addr + 1, self.count)
+ cur = 2
+ for p in self.points:
+ p.write_to_minimap(rom, addr + cur)
+ cur += 0x03
+
+ def write_to_floormap(self, rom:Rom, addr):
+ rom.write_int16(addr, self.icon)
+ rom.write_int32(addr + 0x10, self.count)
+
+ cur = 0x14
+ for p in self.points:
+ p.write_to_floormap(rom, addr + cur)
+ cur += 0x0C
+
+
+class IconPoint(object):
+ def __init__(self, point):
+ self.flag = point["Flag"]
+ self.x = point["x"]
+ self.y = point["y"]
+
+ def write_to_minimap(self, rom:Rom, addr):
+ rom.write_sbyte(addr, self.flag)
+ rom.write_byte(addr+1, self.x)
+ rom.write_byte(addr+2, self.y)
+
+ def write_to_floormap(self, rom:Rom, addr):
+ rom.write_int16(addr, self.flag)
+ rom.write_f32(addr + 4, float(self.x))
+ rom.write_f32(addr + 8, float(self.y))
+
+
+class Scene(object):
+ def __init__(self, scene):
+ self.file = File(scene['File'])
+ self.id = scene['Id']
+ self.transition_actors = [convert_actor_data(x) for x in scene['TActors']]
+ self.rooms = [Room(x) for x in scene['Rooms']]
+ self.paths = []
+ self.coldelta = ColDelta(scene["ColDelta"])
+ self.minimaps = [[Icon(icon) for icon in minimap['Icons']] for minimap in scene['Minimaps']]
+ self.floormaps = [[Icon(icon) for icon in floormap['Icons']] for floormap in scene['Floormaps']]
+ temp_paths = scene['Paths']
+ for item in temp_paths:
+ self.paths.append(item['Points'])
+
+
+ def write_data(self, rom:Rom):
+ # write floormap and minimap data
+ self.write_map_data(rom)
+
+ # move file to remap address
+ if self.file.remap is not None:
+ self.file.relocate(rom)
+
+ start = self.file.start
+ headcur = self.file.start
+
+ room_list_offset = 0
+
+ code = rom.read_byte(headcur)
+ loop = 0x20
+ while loop > 0 and code != 0x14: #terminator
+ loop -= 1
+
+ if code == 0x03: #collision
+ col_mesh_offset = rom.read_int24(headcur + 5)
+ col_mesh = CollisionMesh(rom, start, col_mesh_offset)
+ self.patch_mesh(rom, col_mesh);
+
+ elif code == 0x04: #rooms
+ room_list_offset = rom.read_int24(headcur + 5)
+
+ elif code == 0x0D: #paths
+ path_offset = self.append_path_data(rom)
+ rom.write_int32(headcur + 4, path_offset)
+
+ elif code == 0x0E: #transition actors
+ t_offset = rom.read_int24(headcur + 5)
+ addr = self.file.start + t_offset
+ write_actor_data(rom, addr, self.transition_actors)
+
+ headcur += 8
+ code = rom.read_byte(headcur)
+
+ # update file references
+ self.file.end = align16(self.file.end)
+ update_dmadata(rom, self.file)
+ update_scene_table(rom, self.id, self.file.start, self.file.end)
+
+ # write room file data
+ for room in self.rooms:
+ room.write_data(rom)
+ if self.id == 6 and room.id == 6:
+ patch_spirit_temple_mq_room_6(rom, room.file.start)
+
+ cur = self.file.start + room_list_offset
+ for room in self.rooms:
+ rom.write_int32s(cur, [room.file.start, room.file.end])
+ cur += 0x08
+
+
+ def write_map_data(self, rom:Rom):
+ if self.id >= 10:
+ return
+
+ # write floormap
+ floormap_indices = 0xB6C934
+ floormap_vrom = 0xBC7E00
+ floormap_index = rom.read_int16(floormap_indices + (self.id * 2))
+ floormap_index //= 2 # game uses texture index, where two textures are used per floor
+
+ cur = floormap_vrom + (floormap_index * 0x1EC)
+ for floormap in self.floormaps:
+ for icon in floormap:
+ Icon.write_to_floormap(icon, rom, cur)
+ cur += 0xA4
+
+
+ # fixes jabu jabu floor B1 having no chest data
+ if self.id == 2:
+ cur = floormap_vrom + (0x08 * 0x1EC + 4)
+ kaleido_scope_chest_verts = 0x803A3DA0 # hax, should be vram 0x8082EA00
+ rom.write_int32s(cur, [0x17, kaleido_scope_chest_verts, 0x04])
+
+ # write minimaps
+ map_mark_vrom = 0xBF40D0
+ map_mark_vram = 0x808567F0
+ map_mark_array_vram = 0x8085D2DC # ptr array in map_mark_data to minimap "marks"
+
+ array_vrom = map_mark_array_vram - map_mark_vram + map_mark_vrom
+ map_mark_scene_vram = rom.read_int32(self.id * 4 + array_vrom)
+ mark_vrom = map_mark_scene_vram - map_mark_vram + map_mark_vrom
+
+ cur = mark_vrom
+ for minimap in self.minimaps:
+ for icon in minimap:
+ Icon.write_to_minimap(icon, rom, cur)
+ cur += 0x26
+
+
+ def patch_mesh(self, rom:Rom, mesh:CollisionMesh):
+ start = self.file.start
+
+ final_cams = []
+
+ # build final camera data
+ for cam in self.coldelta.cams:
+ data = cam['Data']
+ pos = cam['PositionIndex']
+ if pos < 0:
+ final_cams.append((data, 0))
+ else:
+ addr = start + (mesh.camera_data_addr & 0xFFFFFF)
+ seg_off = rom.read_int32(addr + (pos * 8) + 4)
+ final_cams.append((data, seg_off))
+
+ types_move_addr = 0
+
+ # if data can't fit within the old mesh space, append camera data
+ if self.coldelta.is_larger:
+ types_move_addr = mesh.camera_data_addr
+
+ # append to end of file
+ self.write_cam_data(rom, self.file.end, final_cams)
+ mesh.camera_data_addr = get_segment_address(2, self.file.end - self.file.start)
+ self.file.end += len(final_cams) * 8
+
+ else:
+ types_move_addr = mesh.camera_data_addr + (len(final_cams) * 8)
+
+ # append in place
+ addr = self.file.start + (mesh.camera_data_addr & 0xFFFFFF)
+ self.write_cam_data(rom, addr, final_cams)
+
+ # if polytypes needs to be moved, do so
+ if (types_move_addr != mesh.polytypes_addr):
+ a_start = self.file.start + (mesh.polytypes_addr & 0xFFFFFF)
+ b_start = self.file.start + (types_move_addr & 0xFFFFFF)
+ size = mesh.polytypes * 8
+
+ rom.buffer[b_start:b_start + size] = rom.buffer[a_start:a_start + size]
+ mesh.polytypes_addr = types_move_addr
+
+ # patch polytypes
+ for item in self.coldelta.polytypes:
+ id = item['Id']
+ high = item['High']
+ low = item['Low']
+ addr = self.file.start + (mesh.polytypes_addr & 0xFFFFFF) + (id * 8)
+ rom.write_int32s(addr, [high, low])
+
+ # patch poly data
+ for item in self.coldelta.polys:
+ id = item['Id']
+ t = item['Type']
+ flags = item['Flags']
+
+ addr = self.file.start + (mesh.poly_addr & 0xFFFFFF) + (id * 0x10)
+ vert_bit = rom.read_byte(addr + 0x02) & 0x1F # VertexA id data
+ rom.write_int16(addr, t)
+ rom.write_byte(addr + 0x02, (flags << 5) + vert_bit)
+
+ # Write Mesh to Scene
+ mesh.write_to_scene(rom, self.file.start)
+
+
+ def write_cam_data(self, rom:Rom, addr, cam_data):
+
+ for item in cam_data:
+ data, pos = item
+ rom.write_int32s(addr, [data, pos])
+ addr += 8
+
+
+ # appends path data to the end of the rom
+ # returns segment address to path data
+ def append_path_data(self, rom:Rom):
+ start = self.file.start
+ cur = self.file.end
+ records = []
+
+ for path in self.paths:
+ nodes = len(path)
+ offset = get_segment_address(2, cur - start)
+ records.append((nodes, offset))
+
+ #flatten
+ points = [x for points in path for x in points]
+ rom.write_int16s(cur, points)
+ path_size = align4(len(path) * 6)
+ cur += path_size
+
+ records_offset = get_segment_address(2, cur - start)
+ for node, offset in records:
+ rom.write_byte(cur, node)
+ rom.write_int32(cur + 4, offset)
+ cur += 8
+
+ self.file.end = cur
+ return records_offset
+
+
+class Room(object):
+ def __init__(self, room):
+ self.file = File(room['File'])
+ self.id = room['Id']
+ self.objects = [int(x, 16) for x in room['Objects']]
+ self.actors = [convert_actor_data(x) for x in room['Actors']]
+
+ def write_data(self, rom:Rom):
+ # move file to remap address
+ if self.file.remap is not None:
+ self.file.relocate(rom)
+
+ headcur = self.file.start
+
+ code = rom.read_byte(headcur)
+ loop = 0x20
+ while loop != 0 and code != 0x14: #terminator
+ loop -= 1
+
+ if code == 0x01: # actors
+ offset = self.file.end - self.file.start
+ write_actor_data(rom, self.file.end, self.actors)
+ self.file.end += len(self.actors) * 0x10
+
+ rom.write_byte(headcur + 1, len(self.actors))
+ rom.write_int32(headcur + 4, get_segment_address(3, offset))
+
+ elif code == 0x0B: # objects
+ offset = self.append_object_data(rom, self.objects)
+
+ rom.write_byte(headcur + 1, len(self.objects))
+ rom.write_int32(headcur + 4, get_segment_address(3, offset))
+
+ headcur += 8
+ code = rom.read_byte(headcur)
+
+ # update file reference
+ self.file.end = align16(self.file.end)
+ update_dmadata(rom, self.file)
+
+
+ def append_object_data(self, rom:Rom, objects):
+ offset = self.file.end - self.file.start
+ cur = self.file.end
+ rom.write_int16s(cur, objects)
+
+ objects_size = align4(len(objects) * 2)
+ self.file.end += objects_size
+ return offset
+
+
+def patch_files(rom:Rom, mq_scenes:list):
+
+ data = get_json()
+ scenes = [Scene(x) for x in data]
+ for scene in scenes:
+ if scene.id in mq_scenes:
+ if scene.id == 9:
+ patch_ice_cavern_scene_header(rom)
+ scene.write_data(rom)
+
+
+
+def get_json():
+ with open(data_path('mqu.json'), 'r') as stream:
+ data = json.load(stream)
+ return data
+
+
+def convert_actor_data(str):
+ spawn_args = str.split(" ")
+ return [ int(x,16) for x in spawn_args ]
+
+
+def get_segment_address(base, offset):
+ offset &= 0xFFFFFF
+ base *= 0x01000000
+ return base + offset
+
+
+def patch_ice_cavern_scene_header(rom):
+ rom.buffer[0x2BEB000:0x2BEB038] = rom.buffer[0x2BEB008:0x2BEB040]
+ rom.write_int32s(0x2BEB038, [0x0D000000, 0x02000000])
+
+
+def patch_spirit_temple_mq_room_6(rom:Rom, room_addr):
+ cur = room_addr
+
+ actor_list_addr = 0
+ cmd_actors_offset = 0
+
+ # scan for actor list and header end
+ code = rom.read_byte(cur)
+ while code != 0x14: #terminator
+ if code == 0x01: # actors
+ actor_list_addr = rom.read_int32(cur + 4)
+ cmd_actors_offset = cur - room_addr
+
+ cur += 8
+ code = rom.read_byte(cur)
+
+ cur += 8
+
+ # original header size
+ header_size = cur - room_addr
+
+ # set alternate header data location
+ alt_data_off = header_size + 8
+
+ # set new alternate header offset
+ alt_header_off = align16(alt_data_off + (4 * 3)) # alt header record size * num records
+
+ # write alternate header data
+ # the first 3 words are mandatory. the last 3 are just to make the binary
+ # cleaner to read
+ rom.write_int32s(room_addr + alt_data_off,
+ [0, get_segment_address(3, alt_header_off), 0, 0, 0, 0])
+
+ # clone header
+ a_start = room_addr
+ a_end = a_start + header_size
+ b_start = room_addr + alt_header_off
+ b_end = b_start + header_size
+
+ rom.buffer[b_start:b_end] = rom.buffer[a_start:a_end]
+
+ # make the child header skip the first actor,
+ # which avoids the spawning of the block while in the hole
+ cmd_addr = room_addr + cmd_actors_offset
+ actor_list_addr += 0x10
+ actors = rom.read_byte(cmd_addr + 1)
+ rom.write_byte(cmd_addr+1, actors - 1)
+ rom.write_int32(cmd_addr + 4, actor_list_addr)
+
+ # move header
+ rom.buffer[a_start + 8:a_end + 8] = rom.buffer[a_start:a_end]
+
+ # write alternate header command
+ seg = get_segment_address(3, alt_data_off)
+ rom.write_int32s(room_addr, [0x18000000, seg])
+
+
+def verify_remap(scenes):
+ def test_remap(file:File):
+ if file.remap is not None:
+ if file.start < file.remap:
+ return False
+ return True
+ print("test code: verify remap won't corrupt data")
+
+ for scene in scenes:
+ file = scene.file
+ result = test_remap(file)
+ print("{0} - {1}".format(result, file))
+
+ for room in scene.rooms:
+ file = room.file
+ result = test_remap(file)
+ print("{0} - {1}".format(result, file))
+
+
+def update_dmadata(rom:Rom, file:File):
+ key, start, end, from_file = file.dma_key, file.start, file.end, file.from_file
+ rom.update_dmadata_record(key, start, end, from_file)
+ file.dma_key = file.start
+
+def update_scene_table(rom:Rom, sceneId, start, end):
+ cur = sceneId * 0x14 + SCENE_TABLE
+ rom.write_int32s(cur, [start, end])
+
+
+def write_actor_data(rom:Rom, cur, actors):
+ for actor in actors:
+ rom.write_int16s(cur, actor)
+ cur += 0x10
+
+def align4(value):
+ return ((value + 3) // 4) * 4
+
+def align16(value):
+ return ((value + 0xF) // 0x10) * 0x10
+
+# This function inserts space in a ovl section at the section's offset
+# The section size is expanded
+# Every relocation entry in the section after the offet is moved accordingly
+# Every relocation value that is after the inserted space is increased accordingly
+def insert_space(rom, file, vram_start, insert_section, insert_offset, insert_size):
+ sections = []
+ val_hi = {}
+ adr_hi = {}
+
+ # get the ovl header
+ cur = file.end - rom.read_int32(file.end - 4)
+ section_total = 0
+ for i in range(0, 4):
+ # build the section offsets
+ section_size = rom.read_int32(cur)
+ sections.append(section_total)
+ section_total += section_size
+
+ # increase the section to be expanded
+ if insert_section == i:
+ rom.write_int32(cur, section_size + insert_size)
+
+ cur += 4
+
+ # calculate the insert address in vram
+ insert_vram = sections[insert_section] + insert_offset + vram_start
+ insert_rom = sections[insert_section] + insert_offset + file.start
+
+ # iterate over the relocation table
+ relocate_count = rom.read_int32(cur)
+ cur += 4
+ for i in range(0, relocate_count):
+ entry = rom.read_int32(cur)
+
+ # parse relocation entry
+ section = ((entry & 0xC0000000) >> 30) - 1
+ type = (entry & 0x3F000000) >> 24
+ offset = entry & 0x00FFFFFF
+
+ # calculate relocation address in rom
+ address = file.start + sections[section] + offset
+
+ # move relocation if section is increased and it's after the insert
+ if insert_section == section and offset >= insert_offset:
+ # rebuild new relocation entry
+ rom.write_int32(cur,
+ ((section + 1) << 30) |
+ (type << 24) |
+ (offset + insert_size))
+
+ # value contains the vram address
+ value = rom.read_int32(address)
+ raw_value = value
+ if type == 2:
+ # Data entry: value is the raw vram address
+ pass
+ elif type == 4:
+ # Jump OP: Get the address from a Jump instruction
+ value = 0x80000000 | (value & 0x03FFFFFF) << 2
+ elif type == 5:
+ # Load High: Upper half of an address load
+ reg = (value >> 16) & 0x1F
+ val_hi[reg] = (value & 0x0000FFFF) << 16
+ adr_hi[reg] = address
+ # Do not process, wait until the lower half is read
+ value = None
+ elif type == 6:
+ # Load Low: Lower half of the address load
+ reg = (value >> 21) & 0x1F
+ val_low = value & 0x0000FFFF
+ val_low = unpack('h', pack('H', val_low))[0]
+ # combine with previous load high
+ value = val_hi[reg] + val_low
+ else:
+ # unknown. OoT does not use any other types
+ value = None
+
+ # update the vram values if it's been moved
+ if value != None and value >= insert_vram:
+ # value = new vram address
+ new_value = value + insert_size
+
+ if type == 2:
+ # Data entry: value is the raw vram address
+ rom.write_int32(address, new_value)
+ elif type == 4:
+ # Jump OP: Set the address in the Jump instruction
+ op = rom.read_int32(address) & 0xFC000000
+ new_value = (new_value & 0x0FFFFFFC) >> 2
+ new_value = op | new_value
+ rom.write_int32(address, new_value)
+ elif type == 6:
+ # Load Low: Lower half of the address load
+ op = rom.read_int32(address) & 0xFFFF0000
+ new_val_low = new_value & 0x0000FFFF
+ rom.write_int32(address, op | new_val_low)
+
+ # Load High: Upper half of an address load
+ op = rom.read_int32(adr_hi[reg]) & 0xFFFF0000
+ new_val_hi = (new_value & 0xFFFF0000) >> 16
+ if new_val_low >= 0x8000:
+ # add 1 if the lower part is negative for borrow
+ new_val_hi += 1
+ rom.write_int32(adr_hi[reg], op | new_val_hi)
+
+ cur += 4
+
+ # Move rom bytes
+ rom.buffer[(insert_rom + insert_size):(file.end + insert_size)] = rom.buffer[insert_rom:file.end]
+ rom.buffer[insert_rom:(insert_rom + insert_size)] = [0] * insert_size
+ file.end += insert_size
+
+
+def add_relocations(rom, file, addresses):
+ relocations = []
+ sections = []
+ header_size = rom.read_int32(file.end - 4)
+ header = file.end - header_size
+ cur = header
+
+ # read section sizes and build offsets
+ section_total = 0
+ for i in range(0, 4):
+ section_size = rom.read_int32(cur)
+ sections.append(section_total)
+ section_total += section_size
+ cur += 4
+
+ # get all entries in relocation table
+ relocate_count = rom.read_int32(cur)
+ cur += 4
+ for i in range(0, relocate_count):
+ relocations.append(rom.read_int32(cur))
+ cur += 4
+
+ # create new enties
+ for address in addresses:
+ if isinstance(address, tuple):
+ # if type provided use it
+ type, address = address
+ else:
+ # Otherwise, try to infer type from value
+ value = rom.read_int32(address)
+ op = value >> 26
+ type = 2 # default: data
+ if op == 0x02 or op == 0x03: # j or jal
+ type = 4
+ elif op == 0x0F: # lui
+ type = 5
+ elif op == 0x08: # addi
+ type = 6
+
+ # Calculate section and offset
+ address = address - file.start
+ section = 0
+ for section_start in sections:
+ if address >= section_start:
+ section += 1
+ else:
+ break
+ offset = address - sections[section - 1]
+
+ # generate relocation entry
+ relocations.append((section << 30)
+ | (type << 24)
+ | (offset & 0x00FFFFFF))
+
+ # Rebuild Relocation Table
+ cur = header + 0x10
+ relocations.sort(key = lambda val: val & 0xC0FFFFFF)
+ rom.write_int32(cur, len(relocations))
+ cur += 4
+ for relocation in relocations:
+ rom.write_int32(cur, relocation)
+ cur += 4
+
+ # Add padded 0?
+ rom.write_int32(cur, 0)
+ cur += 4
+
+ # Update Header and File size
+ new_header_size = (cur + 4) - header
+ rom.write_int32(cur, new_header_size)
+ file.end += (new_header_size - header_size)
diff --git a/worlds/oot/Messages.py b/worlds/oot/Messages.py
new file mode 100644
index 00000000..fbd5881e
--- /dev/null
+++ b/worlds/oot/Messages.py
@@ -0,0 +1,995 @@
+# text details: https://wiki.cloudmodding.com/oot/Text_Format
+
+import random
+from .TextBox import line_wrap
+
+TEXT_START = 0x92D000
+ENG_TEXT_SIZE_LIMIT = 0x39000
+JPN_TEXT_SIZE_LIMIT = 0x3A150
+
+JPN_TABLE_START = 0xB808AC
+ENG_TABLE_START = 0xB849EC
+CREDITS_TABLE_START = 0xB88C0C
+
+JPN_TABLE_SIZE = ENG_TABLE_START - JPN_TABLE_START
+ENG_TABLE_SIZE = CREDITS_TABLE_START - ENG_TABLE_START
+
+EXTENDED_TABLE_START = JPN_TABLE_START # start writing entries to the jp table instead of english for more space
+EXTENDED_TABLE_SIZE = JPN_TABLE_SIZE + ENG_TABLE_SIZE # 0x8360 bytes, 4204 entries
+
+# name of type, followed by number of additional bytes to read, follwed by a function that prints the code
+CONTROL_CODES = {
+ 0x00: ('pad', 0, lambda _: '' ),
+ 0x01: ('line-break', 0, lambda _: '\n' ),
+ 0x02: ('end', 0, lambda _: '' ),
+ 0x04: ('box-break', 0, lambda _: '\nâ–¼\n' ),
+ 0x05: ('color', 1, lambda d: '' ),
+ 0x06: ('gap', 1, lambda d: '<' + str(d) + 'px gap>' ),
+ 0x07: ('goto', 2, lambda d: '' ),
+ 0x08: ('instant', 0, lambda _: '' ),
+ 0x09: ('un-instant', 0, lambda _: '' ),
+ 0x0A: ('keep-open', 0, lambda _: '' ),
+ 0x0B: ('event', 0, lambda _: '' ),
+ 0x0C: ('box-break-delay', 1, lambda d: '\nâ–¼\n' ),
+ 0x0E: ('fade-out', 1, lambda d: '' ),
+ 0x0F: ('name', 0, lambda _: '' ),
+ 0x10: ('ocarina', 0, lambda _: '' ),
+ 0x12: ('sound', 2, lambda d: '' ),
+ 0x13: ('icon', 1, lambda d: '' ),
+ 0x14: ('speed', 1, lambda d: '' ),
+ 0x15: ('background', 3, lambda d: '' ),
+ 0x16: ('marathon', 0, lambda _: '' ),
+ 0x17: ('race', 0, lambda _: '' ),
+ 0x18: ('points', 0, lambda _: '' ),
+ 0x19: ('skulltula', 0, lambda _: '' ),
+ 0x1A: ('unskippable', 0, lambda _: '' ),
+ 0x1B: ('two-choice', 0, lambda _: '' ),
+ 0x1C: ('three-choice', 0, lambda _: '' ),
+ 0x1D: ('fish', 0, lambda _: '' ),
+ 0x1E: ('high-score', 1, lambda d: '' ),
+ 0x1F: ('time', 0, lambda _: '' ),
+}
+
+SPECIAL_CHARACTERS = {
+ 0x80: 'À',
+ 0x81: 'Ã',
+ 0x82: 'Â',
+ 0x83: 'Ä',
+ 0x84: 'Ç',
+ 0x85: 'È',
+ 0x86: 'É',
+ 0x87: 'Ê',
+ 0x88: 'Ë',
+ 0x89: 'Ã',
+ 0x8A: 'Ô',
+ 0x8B: 'Ö',
+ 0x8C: 'Ù',
+ 0x8D: 'Û',
+ 0x8E: 'Ü',
+ 0x8F: 'ß',
+ 0x90: 'Ã ',
+ 0x91: 'á',
+ 0x92: 'â',
+ 0x93: 'ä',
+ 0x94: 'ç',
+ 0x95: 'è',
+ 0x96: 'é',
+ 0x97: 'ê',
+ 0x98: 'ë',
+ 0x99: 'ï',
+ 0x9A: 'ô',
+ 0x9B: 'ö',
+ 0x9C: 'ù',
+ 0x9D: 'û',
+ 0x9E: 'ü',
+ 0x9F: '[A]',
+ 0xA0: '[B]',
+ 0xA1: '[C]',
+ 0xA2: '[L]',
+ 0xA3: '[R]',
+ 0xA4: '[Z]',
+ 0xA5: '[C Up]',
+ 0xA6: '[C Down]',
+ 0xA7: '[C Left]',
+ 0xA8: '[C Right]',
+ 0xA9: '[Triangle]',
+ 0xAA: '[Control Stick]',
+}
+
+UTF8_TO_OOT_SPECIAL = {
+ (0xc3, 0x80): 0x80,
+ (0xc3, 0xae): 0x81,
+ (0xc3, 0x82): 0x82,
+ (0xc3, 0x84): 0x83,
+ (0xc3, 0x87): 0x84,
+ (0xc3, 0x88): 0x85,
+ (0xc3, 0x89): 0x86,
+ (0xc3, 0x8a): 0x87,
+ (0xc3, 0x8b): 0x88,
+ (0xc3, 0x8f): 0x89,
+ (0xc3, 0x94): 0x8A,
+ (0xc3, 0x96): 0x8B,
+ (0xc3, 0x99): 0x8C,
+ (0xc3, 0x9b): 0x8D,
+ (0xc3, 0x9c): 0x8E,
+ (0xc3, 0x9f): 0x8F,
+ (0xc3, 0xa0): 0x90,
+ (0xc3, 0xa1): 0x91,
+ (0xc3, 0xa2): 0x92,
+ (0xc3, 0xa4): 0x93,
+ (0xc3, 0xa7): 0x94,
+ (0xc3, 0xa8): 0x95,
+ (0xc3, 0xa9): 0x96,
+ (0xc3, 0xaa): 0x97,
+ (0xc3, 0xab): 0x98,
+ (0xc3, 0xaf): 0x99,
+ (0xc3, 0xb4): 0x9A,
+ (0xc3, 0xb6): 0x9B,
+ (0xc3, 0xb9): 0x9C,
+ (0xc3, 0xbb): 0x9D,
+ (0xc3, 0xbc): 0x9E,
+}
+
+GOSSIP_STONE_MESSAGES = list( range(0x0401, 0x04FF) ) # ids of the actual hints
+GOSSIP_STONE_MESSAGES += [0x2053, 0x2054] # shared initial stone messages
+TEMPLE_HINTS_MESSAGES = [0x7057, 0x707A] # dungeon reward hints from the temple of time pedestal
+LIGHT_ARROW_HINT = [0x70CC] # ganondorf's light arrow hint line
+GS_TOKEN_MESSAGES = [0x00B4, 0x00B5] # Get Gold Skulltula Token messages
+ERROR_MESSAGE = 0x0001
+
+# messages for shorter item messages
+# ids are in the space freed up by move_shop_item_messages()
+ITEM_MESSAGES = {
+ 0x0001: "\x08\x06\x30\x05\x41TEXT ID ERROR!\x05\x40",
+ 0x9001: "\x08\x13\x2DYou borrowed a \x05\x41Pocket Egg\x05\x40!\x01A Pocket Cucco will hatch from\x01it overnight. Be sure to give it\x01back.",
+ 0x0002: "\x08\x13\x2FYou returned the Pocket Cucco\x01and got \x05\x41Cojiro\x05\x40 in return!\x01Unlike other Cuccos, Cojiro\x01rarely crows.",
+ 0x0003: "\x08\x13\x30You got an \x05\x41Odd Mushroom\x05\x40!\x01It is sure to spoil quickly! Take\x01it to the Kakariko Potion Shop.",
+ 0x0004: "\x08\x13\x31You received an \x05\x41Odd Potion\x05\x40!\x01It may be useful for something...\x01Hurry to the Lost Woods!",
+ 0x0005: "\x08\x13\x32You returned the Odd Potion \x01and got the \x05\x41Poacher's Saw\x05\x40!\x01The young punk guy must have\x01left this.",
+ 0x0007: "\x08\x13\x48You got a \x01\x05\x41Deku Seeds Bullet Bag\x05\x40.\x01This bag can hold up to \x05\x4640\x05\x40\x01slingshot bullets.",
+ 0x0008: "\x08\x13\x33You traded the Poacher's Saw \x01for a \x05\x41Broken Goron's Sword\x05\x40!\x01Visit Biggoron to get it repaired!",
+ 0x0009: "\x08\x13\x34You checked in the Broken \x01Goron's Sword and received a \x01\x05\x41Prescription\x05\x40!\x01Go see King Zora!",
+ 0x000A: "\x08\x13\x37The Biggoron's Sword...\x01You got a \x05\x41Claim Check \x05\x40for it!\x01You can't wait for the sword!",
+ 0x000B: "\x08\x13\x2EYou got a \x05\x41Pocket Cucco, \x05\x40one\x01of Anju's prized hens! It fits \x01in your pocket.",
+ 0x000C: "\x08\x13\x3DYou got the \x05\x41Biggoron's Sword\x05\x40!\x01This blade was forged by a \x01master smith and won't break!",
+ 0x000D: "\x08\x13\x35You used the Prescription and\x01received an \x05\x41Eyeball Frog\x05\x40!\x01Be quick and deliver it to Lake \x01Hylia!",
+ 0x000E: "\x08\x13\x36You traded the Eyeball Frog \x01for the \x05\x41World's Finest Eye Drops\x05\x40!\x01Hurry! Take them to Biggoron!",
+ 0x0010: "\x08\x13\x25You borrowed a \x05\x41Skull Mask\x05\x40.\x01You feel like a monster while you\x01wear this mask!",
+ 0x0011: "\x08\x13\x26You borrowed a \x05\x41Spooky Mask\x05\x40.\x01You can scare many people\x01with this mask!",
+ 0x0012: "\x08\x13\x24You borrowed a \x05\x41Keaton Mask\x05\x40.\x01You'll be a popular guy with\x01this mask on!",
+ 0x0013: "\x08\x13\x27You borrowed a \x05\x41Bunny Hood\x05\x40.\x01The hood's long ears are so\x01cute!",
+ 0x0014: "\x08\x13\x28You borrowed a \x05\x41Goron Mask\x05\x40.\x01It will make your head look\x01big, though.",
+ 0x0015: "\x08\x13\x29You borrowed a \x05\x41Zora Mask\x05\x40.\x01With this mask, you can\x01become one of the Zoras!",
+ 0x0016: "\x08\x13\x2AYou borrowed a \x05\x41Gerudo Mask\x05\x40.\x01This mask will make you look\x01like...a girl?",
+ 0x0017: "\x08\x13\x2BYou borrowed a \x05\x41Mask of Truth\x05\x40.\x01Show it to many people!",
+ 0x0030: "\x08\x13\x06You found the \x05\x41Fairy Slingshot\x05\x40!",
+ 0x0031: "\x08\x13\x03You found the \x05\x41Fairy Bow\x05\x40!",
+ 0x0032: "\x08\x13\x02You got \x05\x41Bombs\x05\x40!\x01If you see something\x01suspicious, bomb it!",
+ 0x0033: "\x08\x13\x09You got \x05\x41Bombchus\x05\x40!",
+ 0x0034: "\x08\x13\x01You got a \x05\x41Deku Nut\x05\x40!",
+ 0x0035: "\x08\x13\x0EYou found the \x05\x41Boomerang\x05\x40!",
+ 0x0036: "\x08\x13\x0AYou found the \x05\x41Hookshot\x05\x40!\x01It's a spring-loaded chain that\x01you can cast out to hook things.",
+ 0x0037: "\x08\x13\x00You got a \x05\x41Deku Stick\x05\x40!",
+ 0x0038: "\x08\x13\x11You found the \x05\x41Megaton Hammer\x05\x40!\x01It's so heavy, you need to\x01use two hands to swing it!",
+ 0x0039: "\x08\x13\x0FYou found the \x05\x41Lens of Truth\x05\x40!\x01Mysterious things are hidden\x01everywhere!",
+ 0x003A: "\x08\x13\x08You found the \x05\x41Ocarina of Time\x05\x40!\x01It glows with a mystical light...",
+ 0x003C: "\x08\x13\x67You received the \x05\x41Fire\x01Medallion\x05\x40!\x01Darunia awakens as a Sage and\x01adds his power to yours!",
+ 0x003D: "\x08\x13\x68You received the \x05\x43Water\x01Medallion\x05\x40!\x01Ruto awakens as a Sage and\x01adds her power to yours!",
+ 0x003E: "\x08\x13\x66You received the \x05\x42Forest\x01Medallion\x05\x40!\x01Saria awakens as a Sage and\x01adds her power to yours!",
+ 0x003F: "\x08\x13\x69You received the \x05\x46Spirit\x01Medallion\x05\x40!\x01Nabooru awakens as a Sage and\x01adds her power to yours!",
+ 0x0040: "\x08\x13\x6BYou received the \x05\x44Light\x01Medallion\x05\x40!\x01Rauru the Sage adds his power\x01to yours!",
+ 0x0041: "\x08\x13\x6AYou received the \x05\x45Shadow\x01Medallion\x05\x40!\x01Impa awakens as a Sage and\x01adds her power to yours!",
+ 0x0042: "\x08\x13\x14You got an \x05\x41Empty Bottle\x05\x40!\x01You can put something in this\x01bottle.",
+ 0x0043: "\x08\x13\x15You got a \x05\x41Red Potion\x05\x40!\x01It will restore your health",
+ 0x0044: "\x08\x13\x16You got a \x05\x42Green Potion\x05\x40!\x01It will restore your magic.",
+ 0x0045: "\x08\x13\x17You got a \x05\x43Blue Potion\x05\x40!\x01It will recover your health\x01and magic.",
+ 0x0046: "\x08\x13\x18You caught a \x05\x41Fairy\x05\x40 in a bottle!\x01It will revive you\x01the moment you run out of life \x01energy.",
+ 0x0047: "\x08\x13\x19You got a \x05\x41Fish\x05\x40!\x01It looks so fresh and\x01delicious!",
+ 0x0048: "\x08\x13\x10You got a \x05\x41Magic Bean\x05\x40!\x01Find a suitable spot for a garden\x01and plant it.",
+ 0x9048: "\x08\x13\x10You got a \x05\x41Pack of Magic Beans\x05\x40!\x01Find suitable spots for a garden\x01and plant them.",
+ 0x004A: "\x08\x13\x07You received the \x05\x41Fairy Ocarina\x05\x40!\x01This is a memento from Saria.",
+ 0x004B: "\x08\x13\x3DYou got the \x05\x42Giant's Knife\x05\x40!\x01Hold it with both hands to\x01attack! It's so long, you\x01can't use it with a \x05\x44shield\x05\x40.",
+ 0x004C: "\x08\x13\x3EYou got a \x05\x44Deku Shield\x05\x40!",
+ 0x004D: "\x08\x13\x3FYou got a \x05\x44Hylian Shield\x05\x40!",
+ 0x004E: "\x08\x13\x40You found the \x05\x44Mirror Shield\x05\x40!\x01The shield's polished surface can\x01reflect light or energy.",
+ 0x004F: "\x08\x13\x0BYou found the \x05\x41Longshot\x05\x40!\x01It's an upgraded Hookshot.\x01It extends \x05\x41twice\x05\x40 as far!",
+ 0x0050: "\x08\x13\x42You got a \x05\x41Goron Tunic\x05\x40!\x01Going to a hot place? No worry!",
+ 0x0051: "\x08\x13\x43You got a \x05\x43Zora Tunic\x05\x40!\x01Wear it, and you won't drown\x01underwater.",
+ 0x0052: "\x08You got a \x05\x42Magic Jar\x05\x40!\x01Your Magic Meter is filled!",
+ 0x0053: "\x08\x13\x45You got the \x05\x41Iron Boots\x05\x40!\x01So heavy, you can't run.\x01So heavy, you can't float.",
+ 0x0054: "\x08\x13\x46You got the \x05\x41Hover Boots\x05\x40!\x01With these mysterious boots\x01you can hover above the ground.",
+ 0x0055: "\x08You got a \x05\x45Recovery Heart\x05\x40!\x01Your life energy is recovered!",
+ 0x0056: "\x08\x13\x4BYou upgraded your quiver to a\x01\x05\x41Big Quiver\x05\x40!\x01Now you can carry more arrows-\x01\x05\x4640 \x05\x40in total!",
+ 0x0057: "\x08\x13\x4CYou upgraded your quiver to\x01the \x05\x41Biggest Quiver\x05\x40!\x01Now you can carry to a\x01maximum of \x05\x4650\x05\x40 arrows!",
+ 0x0058: "\x08\x13\x4DYou found a \x05\x41Bomb Bag\x05\x40!\x01You found \x05\x4120 Bombs\x05\x40 inside!",
+ 0x0059: "\x08\x13\x4EYou got a \x05\x41Big Bomb Bag\x05\x40!\x01Now you can carry more \x01Bombs, up to a maximum of \x05\x4630\x05\x40!",
+ 0x005A: "\x08\x13\x4FYou got the \x01\x05\x41Biggest Bomb Bag\x05\x40!\x01Now, you can carry up to \x01\x05\x4640\x05\x40 Bombs!",
+ 0x005B: "\x08\x13\x51You found the \x05\x43Silver Gauntlets\x05\x40!\x01You feel the power to lift\x01big things with it!",
+ 0x005C: "\x08\x13\x52You found the \x05\x43Golden Gauntlets\x05\x40!\x01You can feel even more power\x01coursing through your arms!",
+ 0x005D: "\x08\x13\x1CYou put a \x05\x44Blue Fire\x05\x40\x01into the bottle!\x01This is a cool flame you can\x01use on red ice.",
+ 0x005E: "\x08\x13\x56You got an \x05\x43Adult's Wallet\x05\x40!\x01Now you can hold\x01up to \x05\x46200\x05\x40 \x05\x46Rupees\x05\x40.",
+ 0x005F: "\x08\x13\x57You got a \x05\x43Giant's Wallet\x05\x40!\x01Now you can hold\x01up to \x05\x46500\x05\x40 \x05\x46Rupees\x05\x40.",
+ 0x0060: "\x08\x13\x77You found a \x05\x41Small Key\x05\x40!\x01This key will open a locked \x01door. You can use it only\x01in this dungeon.",
+ 0x0066: "\x08\x13\x76You found the \x05\x41Dungeon Map\x05\x40!\x01It's the map to this dungeon.",
+ 0x0067: "\x08\x13\x75You found the \x05\x41Compass\x05\x40!\x01Now you can see the locations\x01of many hidden things in the\x01dungeon!",
+ 0x0068: "\x08\x13\x6FYou obtained the \x05\x41Stone of Agony\x05\x40!\x01If you equip a \x05\x44Rumble Pak\x05\x40, it\x01will react to nearby...secrets.",
+ 0x0069: "\x08\x13\x23You received \x05\x41Zelda's Letter\x05\x40!\x01Wow! This letter has Princess\x01Zelda's autograph!",
+ 0x006C: "\x08\x13\x49Your \x05\x41Deku Seeds Bullet Bag \x01\x05\x40has become bigger!\x01This bag can hold \x05\x4650\x05\x41 \x05\x40bullets!",
+ 0x006F: "\x08You got a \x05\x42Green Rupee\x05\x40!\x01That's \x05\x42one Rupee\x05\x40!",
+ 0x0070: "\x08\x13\x04You got the \x05\x41Fire Arrow\x05\x40!\x01If you hit your target,\x01it will catch fire.",
+ 0x0071: "\x08\x13\x0CYou got the \x05\x43Ice Arrow\x05\x40!\x01If you hit your target,\x01it will freeze.",
+ 0x0072: "\x08\x13\x12You got the \x05\x44Light Arrow\x05\x40!\x01The light of justice\x01will smite evil!",
+ 0x0073: "\x08\x06\x28You have learned the\x01\x06\x2F\x05\x42Minuet of Forest\x05\x40!",
+ 0x0074: "\x08\x06\x28You have learned the\x01\x06\x37\x05\x41Bolero of Fire\x05\x40!",
+ 0x0075: "\x08\x06\x28You have learned the\x01\x06\x29\x05\x43Serenade of Water\x05\x40!",
+ 0x0076: "\x08\x06\x28You have learned the\x01\x06\x2D\x05\x46Requiem of Spirit\x05\x40!",
+ 0x0077: "\x08\x06\x28You have learned the\x01\x06\x28\x05\x45Nocturne of Shadow\x05\x40!",
+ 0x0078: "\x08\x06\x28You have learned the\x01\x06\x32\x05\x44Prelude of Light\x05\x40!",
+ 0x0079: "\x08\x13\x50You got the \x05\x41Goron's Bracelet\x05\x40!\x01Now you can pull up Bomb\x01Flowers.",
+ 0x007A: "\x08\x13\x1DYou put a \x05\x41Bug \x05\x40in the bottle!\x01This kind of bug prefers to\x01live in small holes in the ground.",
+ 0x007B: "\x08\x13\x70You obtained the \x05\x41Gerudo's \x01Membership Card\x05\x40!\x01You can get into the Gerudo's\x01training ground.",
+ 0x0080: "\x08\x13\x6CYou got the \x05\x42Kokiri's Emerald\x05\x40!\x01This is the Spiritual Stone of \x01Forest passed down by the\x01Great Deku Tree.",
+ 0x0081: "\x08\x13\x6DYou obtained the \x05\x41Goron's Ruby\x05\x40!\x01This is the Spiritual Stone of \x01Fire passed down by the Gorons!",
+ 0x0082: "\x08\x13\x6EYou obtained \x05\x43Zora's Sapphire\x05\x40!\x01This is the Spiritual Stone of\x01Water passed down by the\x01Zoras!",
+ 0x0090: "\x08\x13\x00Now you can pick up \x01many \x05\x41Deku Sticks\x05\x40!\x01You can carry up to \x05\x4620\x05\x40 of them!",
+ 0x0091: "\x08\x13\x00You can now pick up \x01even more \x05\x41Deku Sticks\x05\x40!\x01You can carry up to \x05\x4630\x05\x40 of them!",
+ 0x0097: "\x08\x13\x20You caught a \x05\x41Poe \x05\x40in a bottle!\x01Something good might happen!",
+ 0x0098: "\x08\x13\x1AYou got \x05\x41Lon Lon Milk\x05\x40!\x01This milk is very nutritious!\x01There are two drinks in it.",
+ 0x0099: "\x08\x13\x1BYou found \x05\x41Ruto's Letter\x05\x40 in a\x01bottle! Show it to King Zora.",
+ 0x9099: "\x08\x13\x1BYou found \x05\x41a letter in a bottle\x05\x40!\x01You remove the letter from the\x01bottle, freeing it for other uses.",
+ 0x009A: "\x08\x13\x21You got a \x05\x41Weird Egg\x05\x40!\x01Feels like there's something\x01moving inside!",
+ 0x00A4: "\x08\x13\x3BYou got the \x05\x42Kokiri Sword\x05\x40!\x01This is a hidden treasure of\x01the Kokiri.",
+ 0x00A7: "\x08\x13\x01Now you can carry\x01many \x05\x41Deku Nuts\x05\x40!\x01You can hold up to \x05\x4630\x05\x40 nuts!",
+ 0x00A8: "\x08\x13\x01You can now carry even\x01more \x05\x41Deku Nuts\x05\x40! You can carry\x01up to \x05\x4640\x05\x41 \x05\x40nuts!",
+ 0x00AD: "\x08\x13\x05You got \x05\x41Din's Fire\x05\x40!\x01Its fireball engulfs everything!",
+ 0x00AE: "\x08\x13\x0DYou got \x05\x42Farore's Wind\x05\x40!\x01This is warp magic you can use!",
+ 0x00AF: "\x08\x13\x13You got \x05\x43Nayru's Love\x05\x40!\x01Cast this to create a powerful\x01protective barrier.",
+ 0x00B4: "\x08You got a \x05\x41Gold Skulltula Token\x05\x40!\x01You've collected \x05\x41\x19\x05\x40 tokens in total.",
+ 0x00B5: "\x08You destroyed a \x05\x41Gold Skulltula\x05\x40.\x01You got a token proving you \x01destroyed it!", #Unused
+ 0x00C2: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01Collect four pieces total to get\x01another Heart Container.",
+ 0x00C3: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01So far, you've collected two \x01pieces.",
+ 0x00C4: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01Now you've collected three \x01pieces!",
+ 0x00C5: "\x08\x13\x73You got a \x05\x41Piece of Heart\x05\x40!\x01You've completed another Heart\x01Container!",
+ 0x00C6: "\x08\x13\x72You got a \x05\x41Heart Container\x05\x40!\x01Your maximum life energy is \x01increased by one heart.",
+ 0x00C7: "\x08\x13\x74You got the \x05\x41Boss Key\x05\x40!\x01Now you can get inside the \x01chamber where the Boss lurks.",
+ 0x9002: "\x08You are a \x05\x43FOOL\x05\x40!",
+ 0x00CC: "\x08You got a \x05\x43Blue Rupee\x05\x40!\x01That's \x05\x43five Rupees\x05\x40!",
+ 0x00CD: "\x08\x13\x53You got the \x05\x43Silver Scale\x05\x40!\x01You can dive deeper than you\x01could before.",
+ 0x00CE: "\x08\x13\x54You got the \x05\x43Golden Scale\x05\x40!\x01Now you can dive much\x01deeper than you could before!",
+ 0x00D1: "\x08\x06\x14You've learned \x05\x42Saria's Song\x05\x40!",
+ 0x00D2: "\x08\x06\x11You've learned \x05\x41Epona's Song\x05\x40!",
+ 0x00D3: "\x08\x06\x0BYou've learned the \x05\x46Sun's Song\x05\x40!",
+ 0x00D4: "\x08\x06\x15You've learned \x05\x43Zelda's Lullaby\x05\x40!",
+ 0x00D5: "\x08\x06\x05You've learned the \x05\x44Song of Time\x05\x40!",
+ 0x00D6: "\x08You've learned the \x05\x45Song of Storms\x05\x40!",
+ 0x00DC: "\x08\x13\x58You got \x05\x41Deku Seeds\x05\x40!\x01Use these as bullets\x01for your Slingshot.",
+ 0x00DD: "\x08You mastered the secret sword\x01technique of the \x05\x41Spin Attack\x05\x40!",
+ 0x00E4: "\x08You can now use \x05\x42Magic\x05\x40!",
+ 0x00E5: "\x08Your \x05\x44defensive power\x05\x40 is enhanced!",
+ 0x00E6: "\x08You got a \x05\x46bundle of arrows\x05\x40!",
+ 0x00E8: "\x08Your magic power has been \x01enhanced! Now you have twice\x01as much \x05\x41Magic Power\x05\x40!",
+ 0x00E9: "\x08Your defensive power has been \x01enhanced! Damage inflicted by \x01enemies will be \x05\x41reduced by half\x05\x40.",
+ 0x00F0: "\x08You got a \x05\x41Red Rupee\x05\x40!\x01That's \x05\x41twenty Rupees\x05\x40!",
+ 0x00F1: "\x08You got a \x05\x45Purple Rupee\x05\x40!\x01That's \x05\x45fifty Rupees\x05\x40!",
+ 0x00F2: "\x08You got a \x05\x46Huge Rupee\x05\x40!\x01This Rupee is worth a whopping\x01\x05\x46two hundred Rupees\x05\x40!",
+ 0x00F9: "\x08\x13\x1EYou put a \x05\x41Big Poe \x05\x40in a bottle!\x01Let's sell it at the \x05\x41Ghost Shop\x05\x40!\x01Something good might happen!",
+ 0x9003: "\x08You found a piece of the \x05\x41Triforce\x05\x40!",
+}
+
+KEYSANITY_MESSAGES = {
+ 0x001C: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
+ 0x0006: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
+ 0x001D: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
+ 0x001E: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
+ 0x002A: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
+ 0x0061: "\x13\x74\x08You got the \x05\x41Boss Key\x05\x40\x01for \x05\x41Ganon's Castle\x05\x40!\x09",
+ 0x0062: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x42Deku Tree\x05\x40!\x09",
+ 0x0063: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for \x05\x41Dodongo's Cavern\x05\x40!\x09",
+ 0x0064: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for \x05\x43Jabu Jabu's Belly\x05\x40!\x09",
+ 0x0065: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
+ 0x007C: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
+ 0x007D: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
+ 0x007E: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
+ 0x007F: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
+ 0x0087: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x44Ice Cavern\x05\x40!\x09",
+ 0x0088: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x42Deku Tree\x05\x40!\x09",
+ 0x0089: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for \x05\x41Dodongo's Cavern\x05\x40!\x09",
+ 0x008A: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for \x05\x43Jabu Jabu's Belly\x05\x40!\x09",
+ 0x008B: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
+ 0x008C: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
+ 0x008E: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
+ 0x008F: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
+ 0x0092: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x44Ice Cavern\x05\x40!\x09",
+ 0x0093: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x42Forest Temple\x05\x40!\x09",
+ 0x0094: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x41Fire Temple\x05\x40!\x09",
+ 0x0095: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x43Water Temple\x05\x40!\x09",
+ 0x009B: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
+ 0x009F: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Gerudo Training\x01Grounds\x05\x40!\x09",
+ 0x00A0: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Gerudo's Fortress\x05\x40!\x09",
+ 0x00A1: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for \x05\x41Ganon's Castle\x05\x40!\x09",
+ 0x00A2: "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
+ 0x00A3: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
+ 0x00A5: "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for the \x05\x45Bottom of the Well\x05\x40!\x09",
+ 0x00A6: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x46Spirit Temple\x05\x40!\x09",
+ 0x00A9: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
+}
+
+MISC_MESSAGES = {
+ 0x507B: (bytearray(
+ b"\x08I tell you, I saw him!\x04" \
+ b"\x08I saw the ghostly figure of Damp\x96\x01" \
+ b"the gravekeeper sinking into\x01" \
+ b"his grave. It looked like he was\x01" \
+ b"holding some kind of \x05\x41treasure\x05\x40!\x02"
+ ), None),
+ 0x0422: ("They say that once \x05\x41Morpha's Curse\x05\x40\x01is lifted, striking \x05\x42this stone\x05\x40 can\x01shift the tides of \x05\x44Lake Hylia\x05\x40.\x02", 0x23),
+ 0x401C: ("Please find my dear \05\x41Princess Ruto\x05\x40\x01immediately... Zora!\x12\x68\x7A", 0x23),
+ 0x9100: ("I am out of goods now.\x01Sorry!\x04The mark that will lead you to\x01the Spirit Temple is the \x05\x41flag on\x01the left \x05\x40outside the shop.\x01Be seeing you!\x02", 0x00)
+}
+
+
+# convert byte array to an integer
+def bytes_to_int(bytes, signed=False):
+ return int.from_bytes(bytes, byteorder='big', signed=signed)
+
+
+# convert int to an array of bytes of the given width
+def int_to_bytes(num, width, signed=False):
+ return int.to_bytes(num, width, byteorder='big', signed=signed)
+
+
+def display_code_list(codes):
+ message = ""
+ for code in codes:
+ message += str(code)
+ return message
+
+
+def parse_control_codes(text):
+ if isinstance(text, list):
+ bytes = text
+ elif isinstance(text, bytearray):
+ bytes = list(text)
+ else:
+ bytes = list(text.encode('utf-8'))
+
+ # Special characters encoded to utf-8 must be re-encoded to OoT's values for them.
+ # Tuple is used due to utf-8 encoding using two bytes.
+ i = 0
+ while i < len(bytes) - 1:
+ if (bytes[i], bytes[i+1]) in UTF8_TO_OOT_SPECIAL:
+ bytes[i] = UTF8_TO_OOT_SPECIAL[(bytes[i], bytes[i+1])]
+ del bytes[i+1]
+ i += 1
+
+ text_codes = []
+ index = 0
+ while index < len(bytes):
+ next_char = bytes[index]
+ data = 0
+ index += 1
+ if next_char in CONTROL_CODES:
+ extra_bytes = CONTROL_CODES[next_char][1]
+ if extra_bytes > 0:
+ data = bytes_to_int(bytes[index : index + extra_bytes])
+ index += extra_bytes
+ text_code = Text_Code(next_char, data)
+ text_codes.append(text_code)
+ if text_code.code == 0x02: # message end code
+ break
+
+ return text_codes
+
+
+# holds a single character or control code of a string
+class Text_Code():
+
+ def display(self):
+ if self.code in CONTROL_CODES:
+ return CONTROL_CODES[self.code][2](self.data)
+ elif self.code in SPECIAL_CHARACTERS:
+ return SPECIAL_CHARACTERS[self.code]
+ elif self.code >= 0x7F:
+ return '?'
+ else:
+ return chr(self.code)
+
+ def get_python_string(self):
+ if self.code in CONTROL_CODES:
+ ret = ''
+ subdata = self.data
+ for _ in range(0, CONTROL_CODES[self.code][1]):
+ ret = ('\\x%02X' % (subdata & 0xFF)) + ret
+ subdata = subdata >> 8
+ ret = '\\x%02X' % self.code + ret
+ return ret
+ elif self.code in SPECIAL_CHARACTERS:
+ return '\\x%02X' % self.code
+ elif self.code >= 0x7F:
+ return '?'
+ else:
+ return chr(self.code)
+
+ def get_string(self):
+ if self.code in CONTROL_CODES:
+ ret = ''
+ subdata = self.data
+ for _ in range(0, CONTROL_CODES[self.code][1]):
+ ret = chr(subdata & 0xFF) + ret
+ subdata = subdata >> 8
+ ret = chr(self.code) + ret
+ return ret
+ else:
+ return chr(self.code)
+
+ # writes the code to the given offset, and returns the offset of the next byte
+ def size(self):
+ size = 1
+ if self.code in CONTROL_CODES:
+ size += CONTROL_CODES[self.code][1]
+ return size
+
+ # writes the code to the given offset, and returns the offset of the next byte
+ def write(self, rom, offset):
+ rom.write_byte(TEXT_START + offset, self.code)
+
+ extra_bytes = 0
+ if self.code in CONTROL_CODES:
+ extra_bytes = CONTROL_CODES[self.code][1]
+ bytes_to_write = int_to_bytes(self.data, extra_bytes)
+ rom.write_bytes(TEXT_START + offset + 1, bytes_to_write)
+
+ return offset + 1 + extra_bytes
+
+ def __init__(self, code, data):
+ self.code = code
+ if code in CONTROL_CODES:
+ self.type = CONTROL_CODES[code][0]
+ else:
+ self.type = 'character'
+ self.data = data
+
+ __str__ = __repr__ = display
+
+# holds a single message, and all its data
+class Message():
+
+ def display(self):
+ meta_data = ["#" + str(self.index),
+ "ID: 0x" + "{:04x}".format(self.id),
+ "Offset: 0x" + "{:06x}".format(self.offset),
+ "Length: 0x" + "{:04x}".format(self.unpadded_length) + "/0x" + "{:04x}".format(self.length),
+ "Box Type: " + str(self.box_type),
+ "Postion: " + str(self.position)]
+ return ', '.join(meta_data) + '\n' + self.text
+
+ def get_python_string(self):
+ ret = ''
+ for code in self.text_codes:
+ ret = ret + code.get_python_string()
+ return ret
+
+ # check if this is an unused message that just contains it's own id as text
+ def is_id_message(self):
+ if self.unpadded_length == 5:
+ for i in range(4):
+ code = self.text_codes[i].code
+ if not (code in range(ord('0'),ord('9')+1) or code in range(ord('A'),ord('F')+1) or code in range(ord('a'),ord('f')+1) ):
+ return False
+ return True
+ return False
+
+
+ def parse_text(self):
+ self.text_codes = parse_control_codes(self.raw_text)
+
+ index = 0
+ for text_code in self.text_codes:
+ index += text_code.size()
+ if text_code.code == 0x02: # message end code
+ break
+ if text_code.code == 0x07: # goto
+ self.has_goto = True
+ self.ending = text_code
+ if text_code.code == 0x0A: # keep-open
+ self.has_keep_open = True
+ self.ending = text_code
+ if text_code.code == 0x0B: # event
+ self.has_event = True
+ self.ending = text_code
+ if text_code.code == 0x0E: # fade out
+ self.has_fade = True
+ self.ending = text_code
+ if text_code.code == 0x10: # ocarina
+ self.has_ocarina = True
+ self.ending = text_code
+ if text_code.code == 0x1B: # two choice
+ self.has_two_choice = True
+ if text_code.code == 0x1C: # three choice
+ self.has_three_choice = True
+ self.text = display_code_list(self.text_codes)
+ self.unpadded_length = index
+
+ def is_basic(self):
+ return not (self.has_goto or self.has_keep_open or self.has_event or self.has_fade or self.has_ocarina or self.has_two_choice or self.has_three_choice)
+
+
+ # computes the size of a message, including padding
+ def size(self):
+ size = 0
+
+ for code in self.text_codes:
+ size += code.size()
+
+ size = (size + 3) & -4 # align to nearest 4 bytes
+
+ return size
+
+ # applies whatever transformations we want to the dialogs
+ def transform(self, replace_ending=False, ending=None, always_allow_skip=True, speed_up_text=True):
+
+ ending_codes = [0x02, 0x07, 0x0A, 0x0B, 0x0E, 0x10]
+ box_breaks = [0x04, 0x0C]
+ slows_text = [0x08, 0x09, 0x14]
+
+ text_codes = []
+
+ # # speed the text
+ if speed_up_text:
+ text_codes.append(Text_Code(0x08, 0)) # allow instant
+
+ # write the message
+ for code in self.text_codes:
+ # ignore ending codes if it's going to be replaced
+ if replace_ending and code.code in ending_codes:
+ pass
+ # ignore the "make unskippable flag"
+ elif always_allow_skip and code.code == 0x1A:
+ pass
+ # ignore anything that slows down text
+ elif speed_up_text and code.code in slows_text:
+ pass
+ elif speed_up_text and code.code in box_breaks:
+ # some special cases for text that needs to be on a timer
+ if (self.id == 0x605A or # twinrova transformation
+ self.id == 0x706C or # raru ending text
+ self.id == 0x70DD or # ganondorf ending text
+ self.id == 0x7070): # zelda ending text
+ text_codes.append(code)
+ text_codes.append(Text_Code(0x08, 0)) # allow instant
+ else:
+ text_codes.append(Text_Code(0x04, 0)) # un-delayed break
+ text_codes.append(Text_Code(0x08, 0)) # allow instant
+ else:
+ text_codes.append(code)
+
+ if replace_ending:
+ if ending:
+ if speed_up_text and ending.code == 0x10: # ocarina
+ text_codes.append(Text_Code(0x09, 0)) # disallow instant text
+ text_codes.append(ending) # write special ending
+ text_codes.append(Text_Code(0x02, 0)) # write end code
+
+ self.text_codes = text_codes
+
+
+ # writes a Message back into the rom, using the given index and offset to update the table
+ # returns the offset of the next message
+ def write(self, rom, index, offset):
+
+ # construct the table entry
+ id_bytes = int_to_bytes(self.id, 2)
+ offset_bytes = int_to_bytes(offset, 3)
+ entry = id_bytes + bytes([self.opts, 0x00, 0x07]) + offset_bytes
+ # write it back
+ entry_offset = EXTENDED_TABLE_START + 8 * index
+ rom.write_bytes(entry_offset, entry)
+
+ for code in self.text_codes:
+ offset = code.write(rom, offset)
+
+ while offset % 4 > 0:
+ offset = Text_Code(0x00, 0).write(rom, offset) # pad to 4 byte align
+
+ return offset
+
+
+ def __init__(self, raw_text, index, id, opts, offset, length):
+
+ self.raw_text = raw_text
+
+ self.index = index
+ self.id = id
+ self.opts = opts # Textbox type and y position
+ self.box_type = (self.opts & 0xF0) >> 4
+ self.position = (self.opts & 0x0F)
+ self.offset = offset
+ self.length = length
+
+ self.has_goto = False
+ self.has_keep_open = False
+ self.has_event = False
+ self.has_fade = False
+ self.has_ocarina = False
+ self.has_two_choice = False
+ self.has_three_choice = False
+ self.ending = None
+
+ self.parse_text()
+
+ # read a single message from rom
+ @classmethod
+ def from_rom(cls, rom, index):
+
+ entry_offset = ENG_TABLE_START + 8 * index
+ entry = rom.read_bytes(entry_offset, 8)
+ next = rom.read_bytes(entry_offset + 8, 8)
+
+ id = bytes_to_int(entry[0:2])
+ opts = entry[2]
+ offset = bytes_to_int(entry[5:8])
+ length = bytes_to_int(next[5:8]) - offset
+
+ raw_text = rom.read_bytes(TEXT_START + offset, length)
+
+ return cls(raw_text, index, id, opts, offset, length)
+
+ @classmethod
+ def from_string(cls, text, id=0, opts=0x00):
+ bytes = list(text.encode('utf-8')) + [0x02]
+
+ # Clean up garbage values added when encoding special characters again.
+ bytes = list(filter(lambda a: a != 194, bytes)) # 0xC2 added before each accent char.
+ i = 0
+ while i < len(bytes) - 1:
+ if bytes[i] in SPECIAL_CHARACTERS and bytes[i] not in UTF8_TO_OOT_SPECIAL.values(): # This indicates it's one of the button chars (A button, etc).
+ # Have to delete 2 inserted garbage values.
+ del bytes[i-1]
+ del bytes[i-2]
+ i -= 2
+ i+= 1
+
+ return cls(bytes, 0, id, opts, 0, len(bytes) + 1)
+
+ @classmethod
+ def from_bytearray(cls, bytearray, id=0, opts=0x00):
+ bytes = list(bytearray) + [0x02]
+
+ return cls(bytes, 0, id, opts, 0, len(bytes) + 1)
+
+ __str__ = __repr__ = display
+
+# wrapper for updating the text of a message, given its message id
+# if the id does not exist in the list, then it will add it
+def update_message_by_id(messages, id, text, opts=None):
+ # get the message index
+ index = next( (m.index for m in messages if m.id == id), -1)
+ # update if it was found
+ if index >= 0:
+ update_message_by_index(messages, index, text, opts)
+ else:
+ add_message(messages, text, id, opts)
+
+# Gets the message by its ID. Returns None if the index does not exist
+def get_message_by_id(messages, id):
+ # get the message index
+ index = next( (m.index for m in messages if m.id == id), -1)
+ if index >= 0:
+ return messages[index]
+ else:
+ return None
+
+# wrapper for updating the text of a message, given its index in the list
+def update_message_by_index(messages, index, text, opts=None):
+ if opts is None:
+ opts = messages[index].opts
+
+ if isinstance(text, bytearray):
+ messages[index] = Message.from_bytearray(text, messages[index].id, opts)
+ else:
+ messages[index] = Message.from_string(text, messages[index].id, opts)
+ messages[index].index = index
+
+# wrapper for adding a string message to a list of messages
+def add_message(messages, text, id=0, opts=0x00):
+ if isinstance(text, bytearray):
+ messages.append( Message.from_bytearray(text, id, opts) )
+ else:
+ messages.append( Message.from_string(text, id, opts) )
+ messages[-1].index = len(messages) - 1
+
+# holds a row in the shop item table (which contains pointers to the description and purchase messages)
+class Shop_Item():
+
+ def display(self):
+ meta_data = ["#" + str(self.index),
+ "Item: 0x" + "{:04x}".format(self.get_item_id),
+ "Price: " + str(self.price),
+ "Amount: " + str(self.pieces),
+ "Object: 0x" + "{:04x}".format(self.object),
+ "Model: 0x" + "{:04x}".format(self.model),
+ "Description: 0x" + "{:04x}".format(self.description_message),
+ "Purchase: 0x" + "{:04x}".format(self.purchase_message),]
+ func_data = [
+ "func1: 0x" + "{:08x}".format(self.func1),
+ "func2: 0x" + "{:08x}".format(self.func2),
+ "func3: 0x" + "{:08x}".format(self.func3),
+ "func4: 0x" + "{:08x}".format(self.func4),]
+ return ', '.join(meta_data) + '\n' + ', '.join(func_data)
+
+ # write the shop item back
+ def write(self, rom, shop_table_address, index):
+
+ entry_offset = shop_table_address + 0x20 * index
+
+ bytes = []
+ bytes += int_to_bytes(self.object, 2)
+ bytes += int_to_bytes(self.model, 2)
+ bytes += int_to_bytes(self.func1, 4)
+ bytes += int_to_bytes(self.price, 2, signed=True)
+ bytes += int_to_bytes(self.pieces, 2)
+ bytes += int_to_bytes(self.description_message, 2)
+ bytes += int_to_bytes(self.purchase_message, 2)
+ bytes += [0x00, 0x00]
+ bytes += int_to_bytes(self.get_item_id, 2)
+ bytes += int_to_bytes(self.func2, 4)
+ bytes += int_to_bytes(self.func3, 4)
+ bytes += int_to_bytes(self.func4, 4)
+
+ rom.write_bytes(entry_offset, bytes)
+
+ # read a single message
+ def __init__(self, rom, shop_table_address, index):
+
+ entry_offset = shop_table_address + 0x20 * index
+ entry = rom.read_bytes(entry_offset, 0x20)
+
+ self.index = index
+ self.object = bytes_to_int(entry[0x00:0x02])
+ self.model = bytes_to_int(entry[0x02:0x04])
+ self.func1 = bytes_to_int(entry[0x04:0x08])
+ self.price = bytes_to_int(entry[0x08:0x0A])
+ self.pieces = bytes_to_int(entry[0x0A:0x0C])
+ self.description_message = bytes_to_int(entry[0x0C:0x0E])
+ self.purchase_message = bytes_to_int(entry[0x0E:0x10])
+ # 0x10-0x11 is always 0000 padded apparently
+ self.get_item_id = bytes_to_int(entry[0x12:0x14])
+ self.func2 = bytes_to_int(entry[0x14:0x18])
+ self.func3 = bytes_to_int(entry[0x18:0x1C])
+ self.func4 = bytes_to_int(entry[0x1C:0x20])
+
+ __str__ = __repr__ = display
+
+# reads each of the shop items
+def read_shop_items(rom, shop_table_address):
+ shop_items = []
+
+ for index in range(0, 100):
+ shop_items.append( Shop_Item(rom, shop_table_address, index) )
+
+ return shop_items
+
+# writes each of the shop item back into rom
+def write_shop_items(rom, shop_table_address, shop_items):
+ for s in shop_items:
+ s.write(rom, shop_table_address, s.index)
+
+# these are unused shop items, and contain text ids that are used elsewhere, and should not be moved
+SHOP_ITEM_EXCEPTIONS = [0x0A, 0x0B, 0x11, 0x12, 0x13, 0x14, 0x29]
+
+# returns a set of all message ids used for shop items
+def get_shop_message_id_set(shop_items):
+ ids = set()
+ for shop in shop_items:
+ if shop.index not in SHOP_ITEM_EXCEPTIONS:
+ ids.add(shop.description_message)
+ ids.add(shop.purchase_message)
+ return ids
+
+# remove all messages that easy to tell are unused to create space in the message index table
+def remove_unused_messages(messages):
+ messages[:] = [m for m in messages if not m.is_id_message()]
+ for index, m in enumerate(messages):
+ m.index = index
+
+# takes all messages used for shop items, and moves messages from the 00xx range into the unused 80xx range
+def move_shop_item_messages(messages, shop_items):
+ # checks if a message id is in the item message range
+ def is_in_item_range(id):
+ bytes = int_to_bytes(id, 2)
+ return bytes[0] == 0x00
+ # get the ids we want to move
+ ids = set( id for id in get_shop_message_id_set(shop_items) if is_in_item_range(id) )
+ # update them in the message list
+ for id in ids:
+ # should be a singleton list, but in case something funky is going on, handle it as a list regardless
+ relevant_messages = [message for message in messages if message.id == id]
+ if len(relevant_messages) >= 2:
+ raise(TypeError("duplicate id in move_shop_item_messages"))
+
+ for message in relevant_messages:
+ message.id |= 0x8000
+ # update them in the shop item list
+ for shop in shop_items:
+ if is_in_item_range(shop.description_message):
+ shop.description_message |= 0x8000
+ if is_in_item_range(shop.purchase_message):
+ shop.purchase_message |= 0x8000
+
+def make_player_message(text):
+ player_text = '\x05\x42\x0F\x05\x40'
+ pronoun_mapping = {
+ "You have ": player_text + " ",
+ "You are ": player_text + " is ",
+ "You've ": player_text + " ",
+ "Your ": player_text + "'s ",
+ "You ": player_text + " ",
+
+ "you have ": player_text + " ",
+ "you are ": player_text + " is ",
+ "you've ": player_text + " ",
+ "your ": player_text + "'s ",
+ "you ": player_text + " ",
+ }
+
+ verb_mapping = {
+ 'obtained ': 'got ',
+ 'received ': 'got ',
+ 'learned ': 'got ',
+ 'borrowed ': 'got ',
+ 'found ': 'got ',
+ }
+
+ new_text = text
+
+ # Replace the first instance of a 'You' with the player name
+ lower_text = text.lower()
+ you_index = lower_text.find('you')
+ if you_index != -1:
+ for find_text, replace_text in pronoun_mapping.items():
+ # if the index do not match, then it is not the first 'You'
+ if text.find(find_text) == you_index:
+ new_text = new_text.replace(find_text, replace_text, 1)
+ break
+
+ # because names are longer, we shorten the verbs to they fit in the textboxes better
+ for find_text, replace_text in verb_mapping.items():
+ new_text = new_text.replace(find_text, replace_text)
+
+ wrapped_text = line_wrap(new_text, False, False, False)
+ if wrapped_text != new_text:
+ new_text = line_wrap(new_text, True, True, False)
+
+ return new_text
+
+
+# reduce item message sizes and add new item messages
+# make sure to call this AFTER move_shop_item_messages()
+def update_item_messages(messages, world):
+ new_item_messages = {**ITEM_MESSAGES, **KEYSANITY_MESSAGES}
+ for id, text in new_item_messages.items():
+ if len(world.world.worlds) > 1:
+ update_message_by_id(messages, id, make_player_message(text), 0x23)
+ else:
+ update_message_by_id(messages, id, text, 0x23)
+
+ for id, (text, opt) in MISC_MESSAGES.items():
+ update_message_by_id(messages, id, text, opt)
+
+
+# run all keysanity related patching to add messages for dungeon specific items
+def add_item_messages(messages, shop_items, world):
+ move_shop_item_messages(messages, shop_items)
+ update_item_messages(messages, world)
+
+
+# reads each of the game's messages into a list of Message objects
+def read_messages(rom):
+ table_offset = ENG_TABLE_START
+ index = 0
+ messages = []
+ while True:
+ entry = rom.read_bytes(table_offset, 8)
+ id = bytes_to_int(entry[0:2])
+
+ if id == 0xFFFD:
+ table_offset += 8
+ continue # this is only here to give an ending offset
+ if id == 0xFFFF:
+ break # this marks the end of the table
+
+ messages.append( Message.from_rom(rom, index) )
+
+ index += 1
+ table_offset += 8
+
+ return messages
+
+# write the messages back
+def repack_messages(rom, messages, permutation=None, always_allow_skip=True, speed_up_text=True):
+
+ rom.update_dmadata_record(TEXT_START, TEXT_START, TEXT_START + ENG_TEXT_SIZE_LIMIT)
+
+ if permutation is None:
+ permutation = range(len(messages))
+
+ # repack messages
+ offset = 0
+ text_size_limit = ENG_TEXT_SIZE_LIMIT
+
+ for old_index, new_index in enumerate(permutation):
+ old_message = messages[old_index]
+ new_message = messages[new_index]
+ remember_id = new_message.id
+ new_message.id = old_message.id
+
+ # modify message, making it represent how we want it to be written
+ new_message.transform(True, old_message.ending, always_allow_skip, speed_up_text)
+
+ # actually write the message
+ offset = new_message.write(rom, old_index, offset)
+
+ new_message.id = remember_id
+
+ # raise an exception if too much is written
+ # we raise it at the end so that we know how much overflow there is
+ if offset > text_size_limit:
+ raise(TypeError("Message Text table is too large: 0x" + "{:x}".format(offset) + " written / 0x" + "{:x}".format(ENG_TEXT_SIZE_LIMIT) + " allowed."))
+
+ # end the table
+ table_index = len(messages)
+ entry = bytes([0xFF, 0xFD, 0x00, 0x00, 0x07]) + int_to_bytes(offset, 3)
+ entry_offset = EXTENDED_TABLE_START + 8 * table_index
+ rom.write_bytes(entry_offset, entry)
+ table_index += 1
+ entry_offset = EXTENDED_TABLE_START + 8 * table_index
+ if 8 * (table_index + 1) > EXTENDED_TABLE_SIZE:
+ raise(TypeError("Message ID table is too large: 0x" + "{:x}".format(8 * (table_index + 1)) + " written / 0x" + "{:x}".format(EXTENDED_TABLE_SIZE) + " allowed."))
+ rom.write_bytes(entry_offset, [0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])
+
+# shuffles the messages in the game, making sure to keep various message types in their own group
+def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
+
+ permutation = [i for i, _ in enumerate(messages)]
+
+ def is_exempt(m):
+ hint_ids = (
+ GOSSIP_STONE_MESSAGES + TEMPLE_HINTS_MESSAGES + LIGHT_ARROW_HINT +
+ list(KEYSANITY_MESSAGES.keys()) + shuffle_messages.shop_item_messages +
+ shuffle_messages.scrubs_message_ids +
+ [0x5036, 0x70F5] # Chicken count and poe count respectively
+ )
+ shuffle_exempt = [
+ 0x208D, # "One more lap!" for Cow in House race.
+ ]
+ is_hint = (except_hints and m.id in hint_ids)
+ is_error_message = (m.id == ERROR_MESSAGE)
+ is_shuffle_exempt = (m.id in shuffle_exempt)
+ return (is_hint or is_error_message or m.is_id_message() or is_shuffle_exempt)
+
+ have_goto = list( filter(lambda m: not is_exempt(m) and m.has_goto, messages) )
+ have_keep_open = list( filter(lambda m: not is_exempt(m) and m.has_keep_open, messages) )
+ have_event = list( filter(lambda m: not is_exempt(m) and m.has_event, messages) )
+ have_fade = list( filter(lambda m: not is_exempt(m) and m.has_fade, messages) )
+ have_ocarina = list( filter(lambda m: not is_exempt(m) and m.has_ocarina, messages) )
+ have_two_choice = list( filter(lambda m: not is_exempt(m) and m.has_two_choice, messages) )
+ have_three_choice = list( filter(lambda m: not is_exempt(m) and m.has_three_choice, messages) )
+ basic_messages = list( filter(lambda m: not is_exempt(m) and m.is_basic(), messages) )
+
+
+ def shuffle_group(group):
+ group_permutation = [i for i, _ in enumerate(group)]
+ random.shuffle(group_permutation)
+
+ for index_from, index_to in enumerate(group_permutation):
+ permutation[group[index_to].index] = group[index_from].index
+
+ # need to use 'list' to force 'map' to actually run through
+ list( map( shuffle_group, [
+ have_goto + have_keep_open + have_event + have_fade + basic_messages,
+ have_ocarina,
+ have_two_choice,
+ have_three_choice,
+ ]))
+
+ return permutation
diff --git a/worlds/oot/Music.py b/worlds/oot/Music.py
new file mode 100644
index 00000000..6ed1ab54
--- /dev/null
+++ b/worlds/oot/Music.py
@@ -0,0 +1,484 @@
+#Much of this is heavily inspired from and/or based on az64's / Deathbasket's MM randomizer
+
+import random
+import os
+from .Utils import compare_version, data_path
+
+
+# Format: (Title, Sequence ID)
+bgm_sequence_ids = [
+ ("Hyrule Field", 0x02),
+ ("Dodongos Cavern", 0x18),
+ ("Kakariko Adult", 0x19),
+ ("Battle", 0x1A),
+ ("Boss Battle", 0x1B),
+ ("Inside Deku Tree", 0x1C),
+ ("Market", 0x1D),
+ ("Title Theme", 0x1E),
+ ("House", 0x1F),
+ ("Jabu Jabu", 0x26),
+ ("Kakariko Child", 0x27),
+ ("Fairy Fountain", 0x28),
+ ("Zelda Theme", 0x29),
+ ("Fire Temple", 0x2A),
+ ("Forest Temple", 0x2C),
+ ("Castle Courtyard", 0x2D),
+ ("Ganondorf Theme", 0x2E),
+ ("Lon Lon Ranch", 0x2F),
+ ("Goron City", 0x30),
+ ("Miniboss Battle", 0x38),
+ ("Temple of Time", 0x3A),
+ ("Kokiri Forest", 0x3C),
+ ("Lost Woods", 0x3E),
+ ("Spirit Temple", 0x3F),
+ ("Horse Race", 0x40),
+ ("Ingo Theme", 0x42),
+ ("Fairy Flying", 0x4A),
+ ("Deku Tree", 0x4B),
+ ("Windmill Hut", 0x4C),
+ ("Shooting Gallery", 0x4E),
+ ("Sheik Theme", 0x4F),
+ ("Zoras Domain", 0x50),
+ ("Shop", 0x55),
+ ("Chamber of the Sages", 0x56),
+ ("Ice Cavern", 0x58),
+ ("Kaepora Gaebora", 0x5A),
+ ("Shadow Temple", 0x5B),
+ ("Water Temple", 0x5C),
+ ("Gerudo Valley", 0x5F),
+ ("Potion Shop", 0x60),
+ ("Kotake and Koume", 0x61),
+ ("Castle Escape", 0x62),
+ ("Castle Underground", 0x63),
+ ("Ganondorf Battle", 0x64),
+ ("Ganon Battle", 0x65),
+ ("Fire Boss", 0x6B),
+ ("Mini-game", 0x6C)
+]
+
+fanfare_sequence_ids = [
+ ("Game Over", 0x20),
+ ("Boss Defeated", 0x21),
+ ("Item Get", 0x22),
+ ("Ganondorf Appears", 0x23),
+ ("Heart Container Get", 0x24),
+ ("Treasure Chest", 0x2B),
+ ("Spirit Stone Get", 0x32),
+ ("Heart Piece Get", 0x39),
+ ("Escape from Ranch", 0x3B),
+ ("Learn Song", 0x3D),
+ ("Epona Race Goal", 0x41),
+ ("Medallion Get", 0x43),
+ ("Zelda Turns Around", 0x51),
+ ("Master Sword", 0x53),
+ ("Door of Time", 0x59)
+]
+
+ocarina_sequence_ids = [
+ ("Prelude of Light", 0x25),
+ ("Bolero of Fire", 0x33),
+ ("Minuet of Forest", 0x34),
+ ("Serenade of Water", 0x35),
+ ("Requiem of Spirit", 0x36),
+ ("Nocturne of Shadow", 0x37),
+ ("Saria's Song", 0x44),
+ ("Epona's Song", 0x45),
+ ("Zelda's Lullaby", 0x46),
+ ("Sun's Song", 0x47),
+ ("Song of Time", 0x48),
+ ("Song of Storms", 0x49)
+]
+
+# Represents the information associated with a sequence, aside from the sequence data itself
+class TableEntry(object):
+ def __init__(self, name, cosmetic_name, type = 0x0202, instrument_set = 0x03, replaces = -1, vanilla_id = -1):
+ self.name = name
+ self.cosmetic_name = cosmetic_name
+ self.replaces = replaces
+ self.vanilla_id = vanilla_id
+ self.type = type
+ self.instrument_set = instrument_set
+
+
+ def copy(self):
+ copy = TableEntry(self.name, self.cosmetic_name, self.type, self.instrument_set, self.replaces, self.vanilla_id)
+ return copy
+
+
+# Represents actual sequence data, along with metadata for the sequence data block
+class Sequence(object):
+ def __init__(self):
+ self.address = -1
+ self.size = -1
+ self.data = []
+
+
+def process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, ids, seq_type = 'bgm'):
+ # Process vanilla music data
+ for bgm in ids:
+ # Get sequence metadata
+ name = bgm[0]
+ cosmetic_name = name
+ type = rom.read_int16(0xB89AE8 + (bgm[1] * 0x10))
+ instrument_set = rom.read_byte(0xB89911 + 0xDD + (bgm[1] * 2))
+ id = bgm[1]
+
+ # Create new sequences
+ seq = TableEntry(name, cosmetic_name, type, instrument_set, vanilla_id = id)
+ target = TableEntry(name, cosmetic_name, type, instrument_set, replaces = id)
+
+ # Special handling for file select/fairy fountain
+ if seq.vanilla_id != 0x57 and cosmetic_name not in disabled_source_sequences:
+ sequences.append(seq)
+ if cosmetic_name not in disabled_target_sequences:
+ target_sequences.append(target)
+
+ # If present, load the file containing custom music to exclude
+ try:
+ with open(os.path.join(data_path(), u'custom_music_exclusion.txt')) as excl_in:
+ seq_exclusion_list = excl_in.readlines()
+ seq_exclusion_list = [seq.rstrip() for seq in seq_exclusion_list if seq[0] != '#']
+ seq_exclusion_list = [seq for seq in seq_exclusion_list if seq.endswith('.meta')]
+ except FileNotFoundError:
+ seq_exclusion_list = []
+
+ # Process music data in data/Music/
+ # Each sequence requires a valid .seq sequence file and a .meta metadata file
+ # Current .meta format: Cosmetic Name\nInstrument Set\nPool
+ for dirpath, _, filenames in os.walk(u'./data/Music', followlinks=True):
+ for fname in filenames:
+ # Skip if included in exclusion file
+ if fname in seq_exclusion_list:
+ continue
+
+ # Find meta file and check if corresponding seq file exists
+ if fname.endswith('.meta') and os.path.isfile(os.path.join(dirpath, fname.split('.')[0] + '.seq')):
+ # Read meta info
+ try:
+ with open(os.path.join(dirpath, fname), 'r') as stream:
+ lines = stream.readlines()
+ # Strip newline(s)
+ lines = [line.rstrip() for line in lines]
+ except FileNotFoundError as ex:
+ raise FileNotFoundError('No meta file for: "' + fname + '". This should never happen')
+
+ # Create new sequence, checking third line for correct type
+ if (len(lines) > 2 and (lines[2].lower() == seq_type.lower() or lines[2] == '')) or (len(lines) <= 2 and seq_type == 'bgm'):
+ seq = TableEntry(os.path.join(dirpath, fname.split('.')[0]), lines[0], instrument_set = int(lines[1], 16))
+
+ if seq.instrument_set < 0x00 or seq.instrument_set > 0x25:
+ raise Exception('Sequence instrument must be in range [0x00, 0x25]')
+
+ if seq.cosmetic_name not in disabled_source_sequences:
+ sequences.append(seq)
+
+ return sequences, target_sequences
+
+
+def shuffle_music(sequences, target_sequences, music_mapping, log):
+ sequence_dict = {}
+ sequence_ids = []
+
+ for sequence in sequences:
+ if sequence.cosmetic_name == "None":
+ raise Exception('Sequences should not be named "None" as that is used for disabled music. Sequence with improper name: %s' % sequence.name)
+ if sequence.cosmetic_name in sequence_dict:
+ raise Exception('Sequence names should be unique. Duplicate sequence name: %s' % sequence.cosmetic_name)
+ sequence_dict[sequence.cosmetic_name] = sequence
+ if sequence.cosmetic_name not in music_mapping.values():
+ sequence_ids.append(sequence.cosmetic_name)
+
+ # Shuffle the sequences
+ if len(sequences) < len(target_sequences):
+ raise Exception(f"Not enough custom music/fanfares ({len(sequences)}) to omit base Ocarina of Time sequences ({len(target_sequences)}).")
+ random.shuffle(sequence_ids)
+
+ sequences = []
+ for target_sequence in target_sequences:
+ sequence = sequence_dict[sequence_ids.pop()].copy() if target_sequence.cosmetic_name not in music_mapping \
+ else ("None", 0x0) if music_mapping[target_sequence.cosmetic_name] == "None" \
+ else sequence_dict[music_mapping[target_sequence.cosmetic_name]].copy()
+ sequences.append(sequence)
+ sequence.replaces = target_sequence.replaces
+ log[target_sequence.cosmetic_name] = sequence.cosmetic_name
+
+ return sequences, log
+
+
+def rebuild_sequences(rom, sequences):
+ # List of sequences (actual sequence data objects) containing the vanilla sequence data
+ old_sequences = []
+
+ for i in range(0x6E):
+ # Create new sequence object, an entry for the audio sequence
+ entry = Sequence()
+ # Get the address for the entry's pointer table entry
+ entry_address = 0xB89AE0 + (i * 0x10)
+ # Extract the info from the pointer table entry
+ entry.address = rom.read_int32(entry_address)
+ entry.size = rom.read_int32(entry_address + 0x04)
+
+ # If size > 0, read the sequence data from the rom into the sequence object
+ if entry.size > 0:
+ entry.data = rom.read_bytes(entry.address + 0x029DE0, entry.size)
+ else:
+ s = [seq for seq in sequences if seq.replaces == i]
+ if s != [] and entry.address > 0 and entry.address < 128:
+ s = s.pop()
+ if s.replaces != 0x28:
+ s.replaces = entry.address
+ else:
+ # Special handling for file select/fairy fountain
+ entry.data = old_sequences[0x57].data
+ entry.size = old_sequences[0x57].size
+
+ old_sequences.append(entry)
+
+ # List of sequences containing the new sequence data
+ new_sequences = []
+ address = 0
+ # Byte array to hold the data for the whole audio sequence
+ new_audio_sequence = []
+
+ for i in range(0x6E):
+ new_entry = Sequence()
+ # If sequence size is 0, the address doesn't matter and it doesn't effect the current address
+ if old_sequences[i].size == 0:
+ new_entry.address = old_sequences[i].address
+ # Continue from the end of the new sequence table
+ else:
+ new_entry.address = address
+
+ s = [seq for seq in sequences if seq.replaces == i]
+ if s != []:
+ assert len(s) == 1
+ s = s.pop()
+ # If we are using a vanilla sequence, get its data from old_sequences
+ if s.vanilla_id != -1:
+ new_entry.size = old_sequences[s.vanilla_id].size
+ new_entry.data = old_sequences[s.vanilla_id].data
+ else:
+ # Read sequence info
+ try:
+ with open(s.name + '.seq', 'rb') as stream:
+ new_entry.data = bytearray(stream.read())
+ new_entry.size = len(new_entry.data)
+ if new_entry.size <= 0x10:
+ raise Exception('Invalid sequence file "' + s.name + '.seq"')
+ new_entry.data[1] = 0x20
+ except FileNotFoundError as ex:
+ raise FileNotFoundError('No sequence file for: "' + s.name + '"')
+ else:
+ new_entry.size = old_sequences[i].size
+ new_entry.data = old_sequences[i].data
+
+ new_sequences.append(new_entry)
+
+ # Concatenate the full audio sequence and the new sequence data
+ if new_entry.data != [] and new_entry.size > 0:
+ # Align sequences to 0x10
+ if new_entry.size % 0x10 != 0:
+ new_entry.data.extend(bytearray(0x10 - (new_entry.size % 0x10)))
+ new_entry.size += 0x10 - (new_entry.size % 0x10)
+ new_audio_sequence.extend(new_entry.data)
+ # Increment the current address by the size of the new sequence
+ address += new_entry.size
+
+ # Check if the new audio sequence is larger than the vanilla one
+ if address > 0x04F690:
+ # Zero out the old audio sequence
+ rom.buffer[0x029DE0 : 0x029DE0 + 0x04F690] = [0] * 0x04F690
+
+ # Append new audio sequence
+ new_address = rom.free_space()
+ rom.write_bytes(new_address, new_audio_sequence)
+
+ #Update dmatable
+ rom.update_dmadata_record(0x029DE0, new_address, new_address + address)
+
+ else:
+ # Write new audio sequence file
+ rom.write_bytes(0x029DE0, new_audio_sequence)
+
+ # Update pointer table
+ for i in range(0x6E):
+ rom.write_int32(0xB89AE0 + (i * 0x10), new_sequences[i].address)
+ rom.write_int32(0xB89AE0 + (i * 0x10) + 0x04, new_sequences[i].size)
+ s = [seq for seq in sequences if seq.replaces == i]
+ if s != []:
+ assert len(s) == 1
+ s = s.pop()
+ rom.write_int16(0xB89AE0 + (i * 0x10) + 0x08, s.type)
+
+ # Update instrument sets
+ for i in range(0x6E):
+ base = 0xB89911 + 0xDD + (i * 2)
+ j = -1
+ if new_sequences[i].size == 0:
+ try:
+ j = [seq for seq in sequences if seq.replaces == new_sequences[i].address].pop()
+ except:
+ j = -1
+ else:
+ try:
+ j = [seq for seq in sequences if seq.replaces == i].pop()
+ except:
+ j = -1
+ if j != -1:
+ rom.write_byte(base, j.instrument_set)
+
+
+def shuffle_pointers_table(rom, ids, music_mapping, log):
+ # Read in all the Music data
+ bgm_data = {}
+ bgm_ids = []
+
+ for bgm in ids:
+ bgm_sequence = rom.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
+ bgm_instrument = rom.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
+ bgm_data[bgm[0]] = (bgm[0], bgm_sequence, bgm_instrument)
+ if bgm[0] not in music_mapping.values():
+ bgm_ids.append(bgm[0])
+
+ # shuffle data
+ random.shuffle(bgm_ids)
+
+ # Write Music data back in random ordering
+ for bgm in ids:
+ if bgm[0] in music_mapping and music_mapping[bgm[0]] in bgm_data:
+ bgm_name = music_mapping[bgm[0]]
+ else:
+ bgm_name = bgm_ids.pop()
+ bgm_name, bgm_sequence, bgm_instrument = bgm_data[bgm_name]
+ rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
+ rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
+ log[bgm[0]] = bgm_name
+
+ # Write Fairy Fountain instrument to File Select (uses same track but different instrument set pointer for some reason)
+ rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), rom.read_int16(0xB89910 + 0xDD + (0x28 * 2)))
+ return log
+
+
+def randomize_music(rom, ootworld, music_mapping):
+ log = {}
+ errors = []
+ sequences = []
+ target_sequences = []
+ fanfare_sequences = []
+ fanfare_target_sequences = []
+ disabled_source_sequences = {}
+ disabled_target_sequences = {}
+
+ # Make sure we aren't operating directly on these.
+ music_mapping = music_mapping.copy()
+ bgm_ids = bgm_sequence_ids.copy()
+ ff_ids = fanfare_sequence_ids.copy()
+
+ # Check if we have mapped music for BGM, Fanfares, or Ocarina Fanfares
+ bgm_mapped = any(bgm[0] in music_mapping for bgm in bgm_ids)
+ ff_mapped = any(ff[0] in music_mapping for ff in ff_ids)
+ ocarina_mapped = any(ocarina[0] in music_mapping for ocarina in ocarina_sequence_ids)
+
+ # Include ocarina songs in fanfare pool if checked
+ if ootworld.ocarina_fanfares or ocarina_mapped:
+ ff_ids.extend(ocarina_sequence_ids)
+
+ # Flag sequence locations that are set to off for disabling.
+ disabled_ids = []
+ if ootworld.background_music == 'off':
+ disabled_ids += [music_id for music_id in bgm_ids]
+ if ootworld.fanfares == 'off':
+ disabled_ids += [music_id for music_id in ff_ids]
+ disabled_ids += [music_id for music_id in ocarina_sequence_ids]
+ for bgm in [music_id for music_id in bgm_ids + ff_ids + ocarina_sequence_ids]:
+ if music_mapping.get(bgm[0], '') == "None":
+ disabled_target_sequences[bgm[0]] = bgm
+ for bgm in disabled_ids:
+ if bgm[0] not in music_mapping:
+ music_mapping[bgm[0]] = "None"
+ disabled_target_sequences[bgm[0]] = bgm
+
+ # Map music to itself if music is set to normal.
+ normal_ids = []
+ if ootworld.background_music == 'normal' and bgm_mapped:
+ normal_ids += [music_id for music_id in bgm_ids]
+ if ootworld.fanfares == 'normal' and (ff_mapped or ocarina_mapped):
+ normal_ids += [music_id for music_id in ff_ids]
+ if not ootworld.ocarina_fanfares and ootworld.fanfares == 'normal' and ocarina_mapped:
+ normal_ids += [music_id for music_id in ocarina_sequence_ids]
+ for bgm in normal_ids:
+ if bgm[0] not in music_mapping:
+ music_mapping[bgm[0]] = bgm[0]
+
+ # If not creating patch file, shuffle audio sequences. Otherwise, shuffle pointer table
+ # If generating from patch, also do a version check to make sure custom sequences are supported.
+ # custom_sequences_enabled = ootworld.compress_rom != 'Patch'
+ # if ootworld.patch_file != '':
+ # rom_version_bytes = rom.read_bytes(0x35, 3)
+ # rom_version = f'{rom_version_bytes[0]}.{rom_version_bytes[1]}.{rom_version_bytes[2]}'
+ # if compare_version(rom_version, '4.11.13') < 0:
+ # errors.append("Custom music is not supported by this patch version. Only randomizing vanilla music.")
+ # custom_sequences_enabled = False
+ # if custom_sequences_enabled:
+ # if ootworld.background_music in ['random', 'random_custom_only'] or bgm_mapped:
+ # process_sequences(rom, sequences, target_sequences, disabled_source_sequences, disabled_target_sequences, bgm_ids)
+ # if ootworld.background_music == 'random_custom_only':
+ # sequences = [seq for seq in sequences if seq.cosmetic_name not in [x[0] for x in bgm_ids] or seq.cosmetic_name in music_mapping.values()]
+ # sequences, log = shuffle_music(sequences, target_sequences, music_mapping, log)
+
+ # if ootworld.fanfares in ['random', 'random_custom_only'] or ff_mapped or ocarina_mapped:
+ # process_sequences(rom, fanfare_sequences, fanfare_target_sequences, disabled_source_sequences, disabled_target_sequences, ff_ids, 'fanfare')
+ # if ootworld.fanfares == 'random_custom_only':
+ # fanfare_sequences = [seq for seq in fanfare_sequences if seq.cosmetic_name not in [x[0] for x in fanfare_sequence_ids] or seq.cosmetic_name in music_mapping.values()]
+ # fanfare_sequences, log = shuffle_music(fanfare_sequences, fanfare_target_sequences, music_mapping, log)
+
+ # if disabled_source_sequences:
+ # log = disable_music(rom, disabled_source_sequences.values(), log)
+
+ # rebuild_sequences(rom, sequences + fanfare_sequences)
+ # else:
+ if ootworld.background_music == 'randomized' or bgm_mapped:
+ log = shuffle_pointers_table(rom, bgm_ids, music_mapping, log)
+
+ if ootworld.fanfares == 'randomized' or ff_mapped or ocarina_mapped:
+ log = shuffle_pointers_table(rom, ff_ids, music_mapping, log)
+ # end_else
+ if disabled_target_sequences:
+ log = disable_music(rom, disabled_target_sequences.values(), log)
+
+ return log, errors
+
+
+def disable_music(rom, ids, log):
+ # First track is no music
+ blank_track = rom.read_bytes(0xB89AE0 + (0 * 0x10), 0x10)
+ for bgm in ids:
+ rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), blank_track)
+ log[bgm[0]] = "None"
+
+ return log
+
+
+def restore_music(rom):
+ # Restore all music from original
+ for bgm in bgm_sequence_ids + fanfare_sequence_ids + ocarina_sequence_ids:
+ bgm_sequence = rom.original.read_bytes(0xB89AE0 + (bgm[1] * 0x10), 0x10)
+ rom.write_bytes(0xB89AE0 + (bgm[1] * 0x10), bgm_sequence)
+ bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (bgm[1] * 2))
+ rom.write_int16(0xB89910 + 0xDD + (bgm[1] * 2), bgm_instrument)
+
+ # restore file select instrument
+ bgm_instrument = rom.original.read_int16(0xB89910 + 0xDD + (0x57 * 2))
+ rom.write_int16(0xB89910 + 0xDD + (0x57 * 2), bgm_instrument)
+
+ # Rebuild audioseq
+ orig_start, orig_end, orig_size = rom.original._get_dmadata_record(0x7470)
+ rom.write_bytes(orig_start, rom.original.read_bytes(orig_start, orig_size))
+
+ # If Audioseq was relocated
+ start, end, size = rom._get_dmadata_record(0x7470)
+ if start != 0x029DE0:
+ # Zero out old audioseq
+ rom.write_bytes(start, [0] * size)
+ rom.update_dmadata_record(start, orig_start, orig_end)
+
diff --git a/worlds/oot/N64Patch.py b/worlds/oot/N64Patch.py
new file mode 100644
index 00000000..01c8e0f6
--- /dev/null
+++ b/worlds/oot/N64Patch.py
@@ -0,0 +1,271 @@
+import struct
+import random
+import io
+import array
+import zlib
+import copy
+import zipfile
+from .ntype import BigStream
+
+
+# get the next XOR key. Uses some location in the source rom.
+# This will skip of 0s, since if we hit a block of 0s, the
+# patch data will be raw.
+def key_next(rom, key_address, address_range):
+ key = 0
+ while key == 0:
+ key_address += 1
+ if key_address > address_range[1]:
+ key_address = address_range[0]
+ key = rom.original.buffer[key_address]
+ return key, key_address
+
+
+# creates a XOR block for the patch. This might break it up into
+# multiple smaller blocks if there is a concern about the XOR key
+# or if it is too long.
+def write_block(rom, xor_address, xor_range, block_start, data, patch_data):
+ new_data = []
+ key_offset = 0
+ continue_block = False
+
+ for b in data:
+ if b == 0:
+ # Leave 0s as 0s. Do not XOR
+ new_data += [0]
+ else:
+ # get the next XOR key
+ key, xor_address = key_next(rom, xor_address, xor_range)
+
+ # if the XOR would result in 0, change the key.
+ # This requires breaking up the block.
+ if b == key:
+ write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
+ new_data = []
+ key_offset = 0
+ continue_block = True
+
+ # search for next safe XOR key
+ while b == key:
+ key_offset += 1
+ key, xor_address = key_next(rom, xor_address, xor_range)
+ # if we aren't able to find one quickly, we may need to break again
+ if key_offset == 0xFF:
+ write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
+ new_data = []
+ key_offset = 0
+ continue_block = True
+
+ # XOR the key with the byte
+ new_data += [b ^ key]
+
+ # Break the block if it's too long
+ if (len(new_data) == 0xFFFF):
+ write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
+ new_data = []
+ key_offset = 0
+ continue_block = True
+
+ # Save the block
+ write_block_section(block_start, key_offset, new_data, patch_data, continue_block)
+ return xor_address
+
+
+# This saves a sub-block for the XOR block. If it's the first part
+# then it will include the address to write to. Otherwise it will
+# have a number of XOR keys to skip and then continue writing after
+# the previous block
+def write_block_section(start, key_skip, in_data, patch_data, is_continue):
+ if not is_continue:
+ patch_data.append_int32(start)
+ else:
+ patch_data.append_bytes([0xFF, key_skip])
+ patch_data.append_int16(len(in_data))
+ patch_data.append_bytes(in_data)
+
+
+# This will create the patch file. Which can be applied to a source rom.
+# xor_range is the range the XOR key will read from. This range is not
+# too important, but I tried to choose from a section that didn't really
+# have big gaps of 0s which we want to avoid.
+def create_patch_file(rom, file, xor_range=(0x00B8AD30, 0x00F029A0)):
+ dma_start, dma_end = rom.get_dma_table_range()
+
+ # add header
+ patch_data = BigStream([])
+ patch_data.append_bytes(list(map(ord, 'ZPFv1')))
+ patch_data.append_int32(dma_start)
+ patch_data.append_int32(xor_range[0])
+ patch_data.append_int32(xor_range[1])
+
+ # get random xor key. This range is chosen because it generally
+ # doesn't have many sections of 0s
+ xor_address = random.Random().randint(*xor_range)
+ patch_data.append_int32(xor_address)
+
+ new_buffer = copy.copy(rom.original.buffer)
+
+ # write every changed DMA entry
+ for dma_index, (from_file, start, size) in rom.changed_dma.items():
+ patch_data.append_int16(dma_index)
+ patch_data.append_int32(from_file)
+ patch_data.append_int32(start)
+ patch_data.append_int24(size)
+
+ # We don't trust files that have modified DMA to have their
+ # changed addresses tracked correctly, so we invalidate the
+ # entire file
+ for address in range(start, start + size):
+ rom.changed_address[address] = rom.buffer[address]
+
+ # Simulate moving the files to know which addresses have changed
+ if from_file >= 0:
+ old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
+ copy_size = min(size, old_size)
+ new_buffer[start:start+copy_size] = rom.original.read_bytes(from_file, copy_size)
+ new_buffer[start+copy_size:start+size] = [0] * (size - copy_size)
+ else:
+ # this is a new file, so we just fill with null data
+ new_buffer[start:start+size] = [0] * size
+
+ # end of DMA entries
+ patch_data.append_int16(0xFFFF)
+
+ # filter down the addresses that will actually need to change.
+ # Make sure to not include any of the DMA table addresses
+ changed_addresses = [address for address,value in rom.changed_address.items() \
+ if (address >= dma_end or address < dma_start) and \
+ (address in rom.force_patch or new_buffer[address] != value)]
+ changed_addresses.sort()
+
+ # Write the address changes. We'll store the data with XOR so that
+ # the patch data won't be raw data from the patched rom.
+ data = []
+ block_start = None
+ BLOCK_HEADER_SIZE = 7 # this is used to break up gaps
+ for address in changed_addresses:
+ # if there's a block to write and there's a gap, write it
+ if block_start:
+ block_end = block_start + len(data) - 1
+ if address > block_end + BLOCK_HEADER_SIZE:
+ xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
+ data = []
+ block_start = None
+ block_end = None
+
+ # start a new block
+ if not block_start:
+ block_start = address
+ block_end = address - 1
+
+ # save the new data
+ data += rom.buffer[block_end+1:address+1]
+
+ # if there was any left over blocks, write them out
+ if block_start:
+ xor_address = write_block(rom, xor_address, xor_range, block_start, data, patch_data)
+
+ # compress the patch file
+ patch_data = bytes(patch_data.buffer)
+ patch_data = zlib.compress(patch_data)
+
+ # save the patch file
+ with open(file, 'wb') as outfile:
+ outfile.write(patch_data)
+
+
+# This will apply a patch file to a source rom to generate a patched rom.
+def apply_patch_file(rom, file, sub_file=None):
+ # load the patch file and decompress
+ if sub_file:
+ with zipfile.ZipFile(file, 'r') as patch_archive:
+ try:
+ with patch_archive.open(sub_file, 'r') as stream:
+ patch_data = stream.read()
+ except KeyError as ex:
+ raise FileNotFoundError('Patch file missing from archive. Invalid Player ID.')
+ else:
+ with open(file, 'rb') as stream:
+ patch_data = stream.read()
+ patch_data = BigStream(zlib.decompress(patch_data))
+
+ # make sure the header is correct
+ if patch_data.read_bytes(length=4) != b'ZPFv':
+ raise Exception("File is not in a Zelda Patch Format")
+ if patch_data.read_byte() != ord('1'):
+ # in the future we might want to have revisions for this format
+ raise Exception("Unsupported patch version.")
+
+ # load the patch configuration info. The fact that the DMA Table is
+ # included in the patch is so that this might be able to work with
+ # other N64 games.
+ dma_start = patch_data.read_int32()
+ xor_range = (patch_data.read_int32(), patch_data.read_int32())
+ xor_address = patch_data.read_int32()
+
+ # Load all the DMA table updates. This will move the files around.
+ # A key thing is that some of these entries will list a source file
+ # that they are from, so we know where to copy from, no matter where
+ # in the DMA table this file has been moved to. Also important if a file
+ # is copied. This list is terminated with 0xFFFF
+ while True:
+ # Load DMA update
+ dma_index = patch_data.read_int16()
+ if dma_index == 0xFFFF:
+ break
+
+ from_file = patch_data.read_int32()
+ start = patch_data.read_int32()
+ size = patch_data.read_int24()
+
+ # Save new DMA Table entry
+ dma_entry = dma_start + (dma_index * 0x10)
+ end = start + size
+ rom.write_int32(dma_entry, start)
+ rom.write_int32(None, end)
+ rom.write_int32(None, start)
+ rom.write_int32(None, 0)
+
+ if from_file != 0xFFFFFFFF:
+ # If a source file is listed, copy from there
+ old_dma_start, old_dma_end, old_size = rom.original.get_dmadata_record_by_key(from_file)
+ copy_size = min(size, old_size)
+ rom.write_bytes(start, rom.original.read_bytes(from_file, copy_size))
+ rom.buffer[start+copy_size:start+size] = [0] * (size - copy_size)
+ else:
+ # if it's a new file, fill with 0s
+ rom.buffer[start:start+size] = [0] * size
+
+ # Read in the XOR data blocks. This goes to the end of the file.
+ block_start = None
+ while not patch_data.eof():
+ is_new_block = patch_data.read_byte() != 0xFF
+
+ if is_new_block:
+ # start writing a new block
+ patch_data.seek_address(delta=-1)
+ block_start = patch_data.read_int32()
+ block_size = patch_data.read_int16()
+ else:
+ # continue writing from previous block
+ key_skip = patch_data.read_byte()
+ block_size = patch_data.read_int16()
+ # skip specified XOR keys
+ for _ in range(key_skip):
+ key, xor_address = key_next(rom, xor_address, xor_range)
+
+ # read in the new data
+ data = []
+ for b in patch_data.read_bytes(length=block_size):
+ if b == 0:
+ # keep 0s as 0s
+ data += [0]
+ else:
+ # The XOR will always be safe and will never produce 0
+ key, xor_address = key_next(rom, xor_address, xor_range)
+ data += [b ^ key]
+
+ # Save the new data to rom
+ rom.write_bytes(block_start, data)
+ block_start = block_start+block_size
+
diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py
new file mode 100644
index 00000000..36cf4d80
--- /dev/null
+++ b/worlds/oot/Options.py
@@ -0,0 +1,782 @@
+import typing
+from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionList
+from .Colors import *
+import worlds.oot.Sounds as sfx
+
+
+class Logic(Choice):
+ """Set the logic used for the generator."""
+ displayname = "Logic Rules"
+ option_glitchless = 0
+ option_glitched = 1
+ option_no_logic = 2
+
+
+class NightTokens(Toggle):
+ """Nighttime skulltulas will logically require Sun's Song."""
+ displayname = "Nighttime Skulltulas Expect Sun's Song"
+
+
+class Forest(Choice):
+ """Set the state of Kokiri Forest and the path to Deku Tree."""
+ displayname = "Forest"
+ option_open = 0
+ option_closed_deku = 1
+ option_closed = 2
+ alias_open_forest = 0
+ alias_closed_forest = 2
+
+
+class Gate(Choice):
+ """Set the state of the Kakariko Village gate."""
+ displayname = "Kakariko Gate"
+ option_open = 0
+ option_zelda = 1
+ option_closed = 2
+
+
+class DoorOfTime(DefaultOnToggle):
+ """Open the Door of Time by default, without the Song of Time."""
+ displayname = "Open Door of Time"
+
+
+class Fountain(Choice):
+ """Set the state of King Zora, blocking the way to Zora's Fountain."""
+ displayname = "Zora's Fountain"
+ option_open = 0
+ option_adult = 1
+ option_closed = 2
+ default = 2
+
+
+class Fortress(Choice):
+ """Set the requirements for access to Gerudo Fortress."""
+ displayname = "Gerudo Fortress"
+ option_normal = 0
+ option_fast = 1
+ option_open = 2
+ default = 1
+
+
+class Bridge(Choice):
+ """Set the requirements for the Rainbow Bridge."""
+ displayname = "Rainbow Bridge Requirement"
+ option_open = 0
+ option_vanilla = 1
+ option_stones = 2
+ option_medallions = 3
+ option_dungeons = 4
+ option_tokens = 5
+ default = 3
+
+
+class Trials(Range):
+ """Set the number of required trials in Ganon's Castle."""
+ displayname = "Ganon's Trials Count"
+ range_start = 0
+ range_end = 6
+
+
+open_options: typing.Dict[str, type(Option)] = {
+ "open_forest": Forest,
+ "open_kakariko": Gate,
+ "open_door_of_time": DoorOfTime,
+ "zora_fountain": Fountain,
+ "gerudo_fortress": Fortress,
+ "bridge": Bridge,
+ "trials": Trials,
+}
+
+
+class StartingAge(Choice):
+ """Choose which age Link will start as."""
+ displayname = "Starting Age"
+ option_child = 0
+ option_adult = 1
+
+
+# TODO: document and name ER options
+class InteriorEntrances(Choice):
+ option_off = 0
+ option_simple = 1
+ option_all = 2
+ alias_false = 0
+
+
+class TriforceHunt(Toggle):
+ """Gather pieces of the Triforce scattered around the world to complete the game."""
+ displayname = "Triforce Hunt"
+
+
+class TriforceGoal(Range):
+ """Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting."""
+ displayname = "Required Triforce Pieces"
+ range_start = 1
+ range_end = 50
+ default = 20
+
+
+class LogicalChus(Toggle):
+ """Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling."""
+ displayname = "Bombchus Considered in Logic"
+
+
+world_options: typing.Dict[str, type(Option)] = {
+ "starting_age": StartingAge,
+ # "shuffle_interior_entrances": InteriorEntrances,
+ # "shuffle_grotto_entrances": Toggle,
+ # "shuffle_dungeon_entrances": Toggle,
+ # "shuffle_overworld_entrances": Toggle,
+ # "owl_drops": Toggle,
+ # "warp_songs": Toggle,
+ # "spawn_positions": Toggle,
+ "triforce_hunt": TriforceHunt,
+ "triforce_goal": TriforceGoal,
+ "bombchus_in_logic": LogicalChus,
+ # "mq_dungeons": make_range(0, 12),
+}
+
+
+class LacsCondition(Choice):
+ """Set the requirements for the Light Arrow Cutscene in the Temple of Time."""
+ displayname = "Light Arrow Cutscene Requirement"
+ option_vanilla = 0
+ option_stones = 1
+ option_medallions = 2
+ option_dungeons = 3
+ option_tokens = 4
+
+
+class LacsStones(Range):
+ """Set the number of Spiritual Stones required for LACS."""
+ displayname = "Spiritual Stones Required for LACS"
+ range_start = 0
+ range_end = 3
+ default = 3
+
+
+class LacsMedallions(Range):
+ """Set the number of medallions required for LACS."""
+ displayname = "Medallions Required for LACS"
+ range_start = 0
+ range_end = 6
+ default = 6
+
+
+class LacsRewards(Range):
+ """Set the number of dungeon rewards required for LACS."""
+ displayname = "Dungeon Rewards Required for LACS"
+ range_start = 0
+ range_end = 9
+ default = 9
+
+
+class LacsTokens(Range):
+ """Set the number of Gold Skulltula Tokens required for LACS."""
+ displayname = "Tokens Required for LACS"
+ range_start = 0
+ range_end = 100
+ default = 100
+
+
+lacs_options: typing.Dict[str, type(Option)] = {
+ "lacs_condition": LacsCondition,
+ "lacs_stones": LacsStones,
+ "lacs_medallions": LacsMedallions,
+ "lacs_rewards": LacsRewards,
+ "lacs_tokens": LacsTokens,
+}
+
+
+class BridgeStones(Range):
+ """Set the number of Spiritual Stones required for the rainbow bridge."""
+ displayname = "Spiritual Stones Required for Bridge"
+ range_start = 0
+ range_end = 3
+ default = 3
+
+
+class BridgeMedallions(Range):
+ """Set the number of medallions required for the rainbow bridge."""
+ displayname = "Medallions Required for Bridge"
+ range_start = 0
+ range_end = 6
+ default = 6
+
+
+class BridgeRewards(Range):
+ """Set the number of dungeon rewards required for the rainbow bridge."""
+ displayname = "Dungeon Rewards Required for Bridge"
+ range_start = 0
+ range_end = 9
+ default = 9
+
+
+class BridgeTokens(Range):
+ """Set the number of Gold Skulltula Tokens required for the rainbow bridge."""
+ displayname = "Tokens Required for Bridge"
+ range_start = 0
+ range_end = 100
+ default = 100
+
+
+bridge_options: typing.Dict[str, type(Option)] = {
+ "bridge_stones": BridgeStones,
+ "bridge_medallions": BridgeMedallions,
+ "bridge_rewards": BridgeRewards,
+ "bridge_tokens": BridgeTokens,
+}
+
+
+class SongShuffle(Choice):
+ """Set where songs can appear."""
+ displayname = "Shuffle Songs"
+ option_song = 0
+ option_dungeon = 1
+ option_any = 2
+
+
+class ShopShuffle(Choice):
+ """Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops."""
+ displayname = "Shopsanity"
+ option_0 = 0
+ option_1 = 1
+ option_2 = 2
+ option_3 = 3
+ option_4 = 4
+ option_random_value = 5
+ option_off = 6
+ default = 6
+ alias_false = 6
+
+
+class TokenShuffle(Choice):
+ """Token rewards from Gold Skulltulas are shuffled into the pool."""
+ displayname = "Tokensanity"
+ option_off = 0
+ option_dungeons = 1
+ option_overworld = 2
+ option_all = 3
+ alias_false = 0
+
+
+class ScrubShuffle(Choice):
+ """Shuffle the items sold by Business Scrubs, and set the prices."""
+ displayname = "Scrub Shuffle"
+ option_off = 0
+ option_low = 1
+ option_regular = 2
+ option_random_prices = 3
+ alias_false = 0
+ alias_affordable = 1
+ alias_expensive = 2
+
+
+class ShuffleCows(Toggle):
+ """Cows give items when Epona's Song is played."""
+ displayname = "Shuffle Cows"
+
+
+class ShuffleSword(Toggle):
+ """Shuffle Kokiri Sword into the item pool."""
+ displayname = "Shuffle Kokiri Sword"
+
+
+class ShuffleOcarinas(Toggle):
+ """Shuffle the Fairy Ocarina and Ocarina of Time into the item pool."""
+ displayname = "Shuffle Ocarinas"
+
+
+class ShuffleEgg(Toggle):
+ """Shuffle the Weird Egg from Malon at Hyrule Castle."""
+ displayname = "Shuffle Weird Egg"
+
+
+class ShuffleCard(Toggle):
+ """Shuffle the Gerudo Membership Card into the item pool."""
+ displayname = "Shuffle Gerudo Card"
+
+
+class ShuffleBeans(Toggle):
+ """Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees."""
+ displayname = "Shuffle Magic Beans"
+
+
+class ShuffleMedigoronCarpet(Toggle):
+ """Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman."""
+ displayname = "Shuffle Medigoron & Carpet Salesman"
+
+
+shuffle_options: typing.Dict[str, type(Option)] = {
+ "shuffle_song_items": SongShuffle,
+ "shopsanity": ShopShuffle,
+ "tokensanity": TokenShuffle,
+ "shuffle_scrubs": ScrubShuffle,
+ "shuffle_cows": ShuffleCows,
+ "shuffle_kokiri_sword": ShuffleSword,
+ "shuffle_ocarinas": ShuffleOcarinas,
+ "shuffle_weird_egg": ShuffleEgg,
+ "shuffle_gerudo_card": ShuffleCard,
+ "shuffle_beans": ShuffleBeans,
+ "shuffle_medigoron_carpet_salesman": ShuffleMedigoronCarpet,
+}
+
+
+class ShuffleMapCompass(Choice):
+ """Control where to shuffle dungeon maps and compasses."""
+ displayname = "Maps & Compasses"
+ option_remove = 0
+ option_startwith = 1
+ option_vanilla = 2
+ option_dungeon = 3
+ option_overworld = 4
+ option_any_dungeon = 5
+ option_keysanity = 6
+ default = 1
+ alias_anywhere = 6
+
+
+class ShuffleKeys(Choice):
+ """Control where to shuffle dungeon small keys."""
+ displayname = "Small Keys"
+ option_remove = 0
+ option_vanilla = 2
+ option_dungeon = 3
+ option_overworld = 4
+ option_any_dungeon = 5
+ option_keysanity = 6
+ default = 3
+ alias_keysy = 0
+ alias_anywhere = 6
+
+
+class ShuffleGerudoKeys(Choice):
+ """Control where to shuffle the Gerudo Fortress small keys."""
+ displayname = "Gerudo Fortress Keys"
+ option_vanilla = 0
+ option_overworld = 1
+ option_any_dungeon = 2
+ option_keysanity = 3
+ alias_anywhere = 3
+
+
+class ShuffleBossKeys(Choice):
+ """Control where to shuffle boss keys, except the Ganon's Castle Boss Key."""
+ displayname = "Boss Keys"
+ option_remove = 0
+ option_vanilla = 2
+ option_dungeon = 3
+ option_overworld = 4
+ option_any_dungeon = 5
+ option_keysanity = 6
+ default = 3
+ alias_keysy = 0
+ alias_anywhere = 6
+
+
+class ShuffleGanonBK(Choice):
+ """Control where to shuffle the Ganon's Castle Boss Key."""
+ displayname = "Ganon's Boss Key"
+ option_remove = 0
+ option_vanilla = 2
+ option_dungeon = 3
+ option_overworld = 4
+ option_any_dungeon = 5
+ option_keysanity = 6
+ option_on_lacs = 7
+ default = 0
+ alias_keysy = 0
+ alias_anywhere = 6
+
+
+class EnhanceMC(Toggle):
+ """Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is."""
+ displayname = "Maps and Compasses Give Information"
+
+
+dungeon_items_options: typing.Dict[str, type(Option)] = {
+ "shuffle_mapcompass": ShuffleMapCompass,
+ "shuffle_smallkeys": ShuffleKeys,
+ "shuffle_fortresskeys": ShuffleGerudoKeys,
+ "shuffle_bosskeys": ShuffleBossKeys,
+ "shuffle_ganon_bosskey": ShuffleGanonBK,
+ "enhance_map_compass": EnhanceMC,
+}
+
+
+class SkipChildZelda(Toggle):
+ """Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed."""
+ displayname = "Skip Child Zelda"
+
+
+class SkipEscape(DefaultOnToggle):
+ """Skips the tower collapse sequence between the Ganondorf and Ganon fights."""
+ displayname = "Skip Tower Escape Sequence"
+
+
+class SkipStealth(DefaultOnToggle):
+ """The crawlspace into Hyrule Castle skips straight to Zelda."""
+ displayname = "Skip Child Stealth"
+
+
+class SkipEponaRace(DefaultOnToggle):
+ """Epona can always be summoned with Epona's Song."""
+ displayname = "Skip Epona Race"
+
+
+class SkipMinigamePhases(DefaultOnToggle):
+ """Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt."""
+ displayname = "Skip Some Minigame Phases"
+
+
+class CompleteMaskQuest(Toggle):
+ """All masks are immediately available to borrow from the Happy Mask Shop."""
+ displayname = "Complete Mask Quest"
+
+
+class UsefulCutscenes(Toggle):
+ """Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched."""
+ displayname = "Enable Useful Cutscenes"
+
+
+class FastChests(DefaultOnToggle):
+ """All chest animations are fast. If disabled, major items have a slow animation."""
+ displayname = "Fast Chest Cutscenes"
+
+
+class FreeScarecrow(Toggle):
+ """Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song."""
+ displayname = "Free Scarecrow's Song"
+
+
+class FastBunny(Toggle):
+ """Bunny Hood lets you move 1.5x faster like in Majora's Mask."""
+ displayname = "Fast Bunny Hood"
+
+
+class ChickenCount(Range):
+ """Controls the number of Cuccos for Anju to give an item as child."""
+ displayname = "Cucco Count"
+ range_start = 0
+ range_end = 7
+ default = 7
+
+
+timesavers_options: typing.Dict[str, type(Option)] = {
+ "skip_child_zelda": SkipChildZelda,
+ "no_escape_sequence": SkipEscape,
+ "no_guard_stealth": SkipStealth,
+ "no_epona_race": SkipEponaRace,
+ "skip_some_minigame_phases": SkipMinigamePhases,
+ "complete_mask_quest": CompleteMaskQuest,
+ "useful_cutscenes": UsefulCutscenes,
+ "fast_chests": FastChests,
+ "free_scarecrow": FreeScarecrow,
+ "fast_bunny_hood": FastBunny,
+ "chicken_count": ChickenCount,
+ # "big_poe_count": make_range(1, 10, 1),
+}
+
+
+class Hints(Choice):
+ """Gossip Stones can give hints about item locations."""
+ displayname = "Gossip Stones"
+ option_none = 0
+ option_mask = 1
+ option_agony = 2
+ option_always = 3
+ default = 3
+ alias_false = 0
+
+
+class HintDistribution(Choice):
+ """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
+ displayname = "Hint Distribution"
+ option_balanced = 0
+ option_ddr = 1
+ option_league = 2
+ option_mw2 = 3
+ option_scrubs = 4
+ option_strong = 5
+ option_tournament = 6
+ option_useless = 7
+ option_very_strong = 8
+
+
+class TextShuffle(Choice):
+ """Randomizes text in the game for comedic effect."""
+ displayname = "Text Shuffle"
+ option_none = 0
+ option_except_hints = 1
+ option_complete = 2
+ alias_false = 0
+
+
+class DamageMultiplier(Choice):
+ """Controls the amount of damage Link takes."""
+ displayname = "Damage Multiplier"
+ option_half = 0
+ option_normal = 1
+ option_double = 2
+ option_quadruple = 3
+ option_ohko = 4
+ default = 1
+
+
+class HeroMode(Toggle):
+ """Hearts will not drop from enemies or objects."""
+ displayname = "Hero Mode"
+
+
+class StartingToD(Choice):
+ """Change the starting time of day."""
+ displayname = "Starting Time of Day"
+ option_default = 0
+ option_sunrise = 1
+ option_morning = 2
+ option_noon = 3
+ option_afternoon = 4
+ option_sunset = 5
+ option_evening = 6
+ option_midnight = 7
+ option_witching_hour = 8
+
+
+class ConsumableStart(Toggle):
+ """Start the game with full Deku Sticks and Deku Nuts."""
+ displayname = "Start with Consumables"
+
+
+class RupeeStart(Toggle):
+ """Start with a full wallet. Wallet upgrades will also fill your wallet."""
+ displayname = "Start with Rupees"
+
+
+misc_options: typing.Dict[str, type(Option)] = {
+ # "clearer_hints": DefaultOnToggle,
+ "hints": Hints,
+ "hint_dist": HintDistribution,
+ "text_shuffle": TextShuffle,
+ "damage_multiplier": DamageMultiplier,
+ "no_collectible_hearts": HeroMode,
+ "starting_tod": StartingToD,
+ "start_with_consumables": ConsumableStart,
+ "start_with_rupees": RupeeStart,
+}
+
+class ItemPoolValue(Choice):
+ """Changes the number of items available in the game."""
+ displayname = "Item Pool"
+ option_plentiful = 0
+ option_balanced = 1
+ option_scarce = 2
+ option_minimal = 3
+ default = 1
+
+
+class IceTraps(Choice):
+ """Adds ice traps to the item pool."""
+ displayname = "Ice Traps"
+ option_off = 0
+ option_normal = 1
+ option_on = 2
+ option_mayhem = 3
+ option_onslaught = 4
+ default = 1
+ alias_false = 0
+ alias_true = 2
+ alias_extra = 2
+
+
+class IceTrapVisual(Choice):
+ """Changes the appearance of ice traps as freestanding items."""
+ displayname = "Ice Trap Appearance"
+ option_major_only = 0
+ option_junk_only = 1
+ option_anything = 2
+
+
+class AdultTradeItem(Choice):
+ option_pocket_egg = 0
+ option_pocket_cucco = 1
+ option_cojiro = 2
+ option_odd_mushroom = 3
+ option_poachers_saw = 4
+ option_broken_sword = 5
+ option_prescription = 6
+ option_eyeball_frog = 7
+ option_eyedrops = 8
+ option_claim_check = 9
+
+
+class EarlyTradeItem(AdultTradeItem):
+ """Earliest item that can appear in the adult trade sequence."""
+ displayname = "Adult Trade Sequence Earliest Item"
+ default = 6
+
+
+class LateTradeItem(AdultTradeItem):
+ """Latest item that can appear in the adult trade sequence."""
+ displayname = "Adult Trade Sequence Latest Item"
+ default = 9
+
+
+itempool_options: typing.Dict[str, type(Option)] = {
+ "item_pool_value": ItemPoolValue,
+ "junk_ice_traps": IceTraps,
+ "ice_trap_appearance": IceTrapVisual,
+ "logic_earliest_adult_trade": EarlyTradeItem,
+ "logic_latest_adult_trade": LateTradeItem,
+}
+
+# Start of cosmetic options
+
+def assemble_color_option(func, display_name: str, default_option: str, outer=False):
+ color_options = func()
+ if outer:
+ color_options.append("Match Inner")
+ format_color = lambda color: color.replace(' ', '_').lower()
+ color_to_id = {format_color(color): index for index, color in enumerate(color_options)}
+ class ColorOption(Choice):
+ """Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code."""
+ displayname = display_name
+ default = color_options.index(default_option)
+ ColorOption.options.update(color_to_id)
+ ColorOption.name_lookup.update({id: color for (color, id) in color_to_id.items()})
+ return ColorOption
+
+
+class Targeting(Choice):
+ """Default targeting option."""
+ displayname = "Default Targeting Option"
+ option_hold = 0
+ option_switch = 1
+
+
+class DisplayDpad(DefaultOnToggle):
+ """Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots)."""
+ displayname = "Display D-Pad HUD"
+
+
+class CorrectColors(DefaultOnToggle):
+ """Makes in-game models match their HUD element colors."""
+ displayname = "Item Model Colors Match Cosmetics"
+
+
+class Music(Choice):
+ option_normal = 0
+ option_off = 1
+ option_randomized = 2
+ alias_false = 1
+
+
+class BackgroundMusic(Music):
+ """Randomize or disable background music."""
+ displayname = "Background Music"
+
+
+class Fanfares(Music):
+ """Randomize or disable item fanfares."""
+ displayname = "Fanfares"
+
+
+class OcarinaFanfares(Toggle):
+ """Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized."""
+ displayname = "Ocarina Songs as Fanfares"
+
+
+class SwordTrailDuration(Range):
+ """Set the duration for sword trails."""
+ displayname = "Sword Trail Duration"
+ range_start = 4
+ range_end = 20
+ default = 4
+
+
+cosmetic_options: typing.Dict[str, type(Option)] = {
+ "default_targeting": Targeting,
+ "display_dpad": DisplayDpad,
+ "correct_model_colors": CorrectColors,
+ "background_music": BackgroundMusic,
+ "fanfares": Fanfares,
+ "ocarina_fanfares": OcarinaFanfares,
+ "kokiri_color": assemble_color_option(get_tunic_color_options, "Kokiri Tunic", "Kokiri Green"),
+ "goron_color": assemble_color_option(get_tunic_color_options, "Goron Tunic", "Goron Red"),
+ "zora_color": assemble_color_option(get_tunic_color_options, "Zora Tunic", "Zora Blue"),
+ "silver_gauntlets_color": assemble_color_option(get_gauntlet_color_options, "Silver Gauntlets Color", "Silver"),
+ "golden_gauntlets_color": assemble_color_option(get_gauntlet_color_options, "Golden Gauntlets Color", "Gold"),
+ "mirror_shield_frame_color": assemble_color_option(get_shield_frame_color_options, "Mirror Shield Frame Color", "Red"),
+ "navi_color_default_inner": assemble_color_option(get_navi_color_options, "Navi Idle Inner", "White"),
+ "navi_color_default_outer": assemble_color_option(get_navi_color_options, "Navi Idle Outer", "Match Inner", outer=True),
+ "navi_color_enemy_inner": assemble_color_option(get_navi_color_options, "Navi Targeting Enemy Inner", "Yellow"),
+ "navi_color_enemy_outer": assemble_color_option(get_navi_color_options, "Navi Targeting Enemy Outer", "Match Inner", outer=True),
+ "navi_color_npc_inner": assemble_color_option(get_navi_color_options, "Navi Targeting NPC Inner", "Light Blue"),
+ "navi_color_npc_outer": assemble_color_option(get_navi_color_options, "Navi Targeting NPC Outer", "Match Inner", outer=True),
+ "navi_color_prop_inner": assemble_color_option(get_navi_color_options, "Navi Targeting Prop Inner", "Green"),
+ "navi_color_prop_outer": assemble_color_option(get_navi_color_options, "Navi Targeting Prop Outer", "Match Inner", outer=True),
+ "sword_trail_duration": SwordTrailDuration,
+ "sword_trail_color_inner": assemble_color_option(get_sword_trail_color_options, "Sword Trail Inner", "White"),
+ "sword_trail_color_outer": assemble_color_option(get_sword_trail_color_options, "Sword Trail Outer", "Match Inner", outer=True),
+ "bombchu_trail_color_inner": assemble_color_option(get_bombchu_trail_color_options, "Bombchu Trail Inner", "Red"),
+ "bombchu_trail_color_outer": assemble_color_option(get_bombchu_trail_color_options, "Bombchu Trail Outer", "Match Inner", outer=True),
+ "boomerang_trail_color_inner": assemble_color_option(get_boomerang_trail_color_options, "Boomerang Trail Inner", "Yellow"),
+ "boomerang_trail_color_outer": assemble_color_option(get_boomerang_trail_color_options, "Boomerang Trail Outer", "Match Inner", outer=True),
+ "heart_color": assemble_color_option(get_heart_color_options, "Heart Color", "Red"),
+ "magic_color": assemble_color_option(get_magic_color_options, "Magic Color", "Green"),
+ "a_button_color": assemble_color_option(get_a_button_color_options, "A Button Color", "N64 Blue"),
+ "b_button_color": assemble_color_option(get_b_button_color_options, "B Button Color", "N64 Green"),
+ "c_button_color": assemble_color_option(get_c_button_color_options, "C Button Color", "Yellow"),
+ "start_button_color": assemble_color_option(get_start_button_color_options, "Start Button Color", "N64 Red"),
+}
+
+def assemble_sfx_option(sound_hook: sfx.SoundHooks, display_name: str):
+ options = sfx.get_setting_choices(sound_hook).keys()
+ sfx_to_id = {sfx.replace('-', '_'): index for index, sfx in enumerate(options)}
+ class SfxOption(Choice):
+ """Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound."""
+ displayname = display_name
+ SfxOption.options.update(sfx_to_id)
+ SfxOption.name_lookup.update({id: sfx for (sfx, id) in sfx_to_id.items()})
+ return SfxOption
+
+class SfxOcarina(Choice):
+ """Change the sound of the ocarina."""
+ displayname = "Ocarina Instrument"
+ option_ocarina = 1
+ option_malon = 2
+ option_whistle = 3
+ option_harp = 4
+ option_grind_organ = 5
+ option_flute = 6
+ default = 1
+
+sfx_options: typing.Dict[str, type(Option)] = {
+ "sfx_navi_overworld": assemble_sfx_option(sfx.SoundHooks.NAVI_OVERWORLD, "Navi Overworld"),
+ "sfx_navi_enemy": assemble_sfx_option(sfx.SoundHooks.NAVI_ENEMY, "Navi Enemy"),
+ "sfx_low_hp": assemble_sfx_option(sfx.SoundHooks.HP_LOW, "Low HP"),
+ "sfx_menu_cursor": assemble_sfx_option(sfx.SoundHooks.MENU_CURSOR, "Menu Cursor"),
+ "sfx_menu_select": assemble_sfx_option(sfx.SoundHooks.MENU_SELECT, "Menu Select"),
+ "sfx_nightfall": assemble_sfx_option(sfx.SoundHooks.NIGHTFALL, "Nightfall"),
+ "sfx_horse_neigh": assemble_sfx_option(sfx.SoundHooks.HORSE_NEIGH, "Horse"),
+ "sfx_hover_boots": assemble_sfx_option(sfx.SoundHooks.BOOTS_HOVER, "Hover Boots"),
+ "sfx_ocarina": SfxOcarina,
+}
+
+
+# All options assembled into a single dict
+oot_options: typing.Dict[str, type(Option)] = {
+ "logic_rules": Logic,
+ "logic_no_night_tokens_without_suns_song": NightTokens,
+ **open_options,
+ **world_options,
+ **bridge_options,
+ **dungeon_items_options,
+ **lacs_options,
+ **shuffle_options,
+ **timesavers_options,
+ **misc_options,
+ **itempool_options,
+ **cosmetic_options,
+ **sfx_options,
+ "logic_tricks": OptionList,
+}
diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py
new file mode 100644
index 00000000..1a27ab02
--- /dev/null
+++ b/worlds/oot/Patches.py
@@ -0,0 +1,2166 @@
+import struct
+import itertools
+import re
+import zlib
+from collections import defaultdict
+
+from .LocationList import business_scrubs
+from .Hints import writeGossipStoneHints, buildAltarHints, \
+ buildGanonText, getSimpleHintNoPrefix
+from .Utils import data_path
+from .Messages import read_messages, update_message_by_id, read_shop_items, \
+ write_shop_items, remove_unused_messages, make_player_message, \
+ add_item_messages, repack_messages, shuffle_messages, \
+ get_message_by_id
+from .MQ import patch_files, File, update_dmadata, insert_space, add_relocations
+from .SaveContext import SaveContext
+from .TextBox import character_table, NORMAL_LINE_WIDTH
+
+
+# "Spoiler" argument deleted; can probably be replaced with calls to world.world
+def patch_rom(world, rom):
+ with open(data_path('generated/rom_patch.txt'), 'r') as stream:
+ for line in stream:
+ address, value = [int(x, 16) for x in line.split(',')]
+ rom.write_int32(address, value)
+ rom.scan_dmadata_update()
+
+ # Write Randomizer title screen logo
+ with open(data_path('title.bin'), 'rb') as stream:
+ writeAddress = 0x01795300
+ titleBytesComp = stream.read()
+ titleBytesDiff = zlib.decompress(titleBytesComp)
+
+ originalBytes = rom.original.buffer[writeAddress: writeAddress+ len(titleBytesDiff)]
+ titleBytes = bytearray([a ^ b for a, b in zip(titleBytesDiff, originalBytes)])
+ rom.write_bytes(writeAddress, titleBytes)
+
+ # Fixes the typo of keatan mask in the item select screen
+ with open(data_path('keaton.bin'), 'rb') as stream:
+ writeAddress = 0x8A7C00
+ keatonBytesComp = stream.read()
+ keatonBytesDiff = zlib.decompress(keatonBytesComp)
+
+ originalBytes = rom.original.buffer[writeAddress: writeAddress+ len(keatonBytesDiff)]
+ keatonBytes = bytearray([a ^ b for a, b in zip(keatonBytesDiff, originalBytes)])
+ rom.write_bytes(writeAddress, keatonBytes)
+
+ # Load Triforce model into a file
+ triforce_obj_file = File({ 'Name': 'object_gi_triforce' })
+ triforce_obj_file.copy(rom)
+ with open(data_path('triforce.bin'), 'rb') as stream:
+ obj_data = stream.read()
+ rom.write_bytes(triforce_obj_file.start, obj_data)
+ triforce_obj_file.end = triforce_obj_file.start + len(obj_data)
+ update_dmadata(rom, triforce_obj_file)
+ # Add it to the extended object table
+ add_to_extended_object_table(rom, 0x193, triforce_obj_file)
+
+ # Build a Double Defense model from the Heart Container model
+ dd_obj_file = File({
+ 'Name': 'object_gi_hearts',
+ 'Start': '014D9000',
+ 'End': '014DA590',
+ })
+ dd_obj_file.copy(rom)
+ # Update colors for the Double Defense variant
+ rom.write_bytes(dd_obj_file.start + 0x1294, [0xFF, 0xCF, 0x0F]) # Exterior Primary Color
+ rom.write_bytes(dd_obj_file.start + 0x12B4, [0xFF, 0x46, 0x32]) # Exterior Env Color
+ rom.write_int32s(dd_obj_file.start + 0x12A8, [0xFC173C60, 0x150C937F]) # Exterior Combine Mode
+ rom.write_bytes(dd_obj_file.start + 0x1474, [0xFF, 0xFF, 0xFF]) # Interior Primary Color
+ rom.write_bytes(dd_obj_file.start + 0x1494, [0xFF, 0xFF, 0xFF]) # Interior Env Color
+ update_dmadata(rom, dd_obj_file)
+ # Add it to the extended object table
+ add_to_extended_object_table(rom, 0x194, dd_obj_file)
+
+ # Set default targeting option to Hold. I got fed up enough with this that I made it a main option
+ if world.default_targeting == 'hold':
+ rom.write_byte(0xB71E6D, 0x01)
+ else:
+ rom.write_byte(0xB71E6D, 0x00)
+
+ # Create an option so that recovery hearts no longer drop by changing the code which checks Link's health when an item is spawned.
+ if world.no_collectible_hearts:
+ rom.write_byte(0xA895B7, 0x2E)
+
+ # Force language to be English in the event a Japanese rom was submitted
+ rom.write_byte(0x3E, 0x45)
+ rom.force_patch.append(0x3E)
+
+ # Increase the instance size of Bombchus prevent the heap from becoming corrupt when
+ # a Dodongo eats a Bombchu. Does not fix stale pointer issues with the animation
+ rom.write_int32(0xD6002C, 0x1F0)
+
+ # Can always return to youth
+ rom.write_byte(0xCB6844, 0x35)
+ rom.write_byte(0x253C0E2, 0x03) # Moves sheik from pedestal
+
+ # Fix Ice Cavern Alcove Camera
+ if not world.dungeon_mq['Ice Cavern']:
+ rom.write_byte(0x2BECA25,0x01);
+ rom.write_byte(0x2BECA2D,0x01);
+
+ # Fix GS rewards to be static
+ rom.write_int32(0xEA3934, 0)
+ rom.write_bytes(0xEA3940, [0x10, 0x00])
+
+ # Fix horseback archery rewards to be static
+ rom.write_byte(0xE12BA5, 0x00)
+ rom.write_byte(0xE12ADD, 0x00)
+
+ # Fix deku theater rewards to be static
+ rom.write_bytes(0xEC9A7C, [0x00, 0x00, 0x00, 0x00]) #Sticks
+ rom.write_byte(0xEC9CD5, 0x00) #Nuts
+
+ # Fix deku scrub who sells stick upgrade
+ rom.write_bytes(0xDF8060, [0x00, 0x00, 0x00, 0x00])
+
+ # Fix deku scrub who sells nut upgrade
+ rom.write_bytes(0xDF80D4, [0x00, 0x00, 0x00, 0x00])
+
+ # Fix rolling goron as child reward to be static
+ rom.write_bytes(0xED2960, [0x00, 0x00, 0x00, 0x00])
+
+ # Fix proximity text boxes (Navi) (Part 1)
+ rom.write_bytes(0xDF8B84, [0x00, 0x00, 0x00, 0x00])
+
+ # Fix final magic bean to cost 99
+ rom.write_byte(0xE20A0F, 0x63)
+ rom.write_bytes(0x94FCDD, [0x08, 0x39, 0x39])
+
+ # Remove locked door to Boss Key Chest in Fire Temple
+ if not world.keysanity and not world.dungeon_mq['Fire Temple']:
+ rom.write_byte(0x22D82B7, 0x3F)
+ # Remove the unused locked door in water temple
+ if not world.dungeon_mq['Water Temple']:
+ rom.write_byte(0x25B8197, 0x3F)
+
+ if world.bombchus_in_logic:
+ rom.write_int32(rom.sym('BOMBCHUS_IN_LOGIC'), 1)
+
+ # Change graveyard graves to not allow grabbing on to the ledge
+ rom.write_byte(0x0202039D, 0x20)
+ rom.write_byte(0x0202043C, 0x24)
+
+
+ # Fix Castle Courtyard to check for meeting Zelda, not Zelda fleeing, to block you
+ rom.write_bytes(0xCD5E76, [0x0E, 0xDC])
+ rom.write_bytes(0xCD5E12, [0x0E, 0xDC])
+
+ # Cutscene for all medallions never triggers when leaving shadow or spirit temples(hopefully stops warp to colossus on shadow completion with boss reward shuffle)
+ rom.write_byte(0xACA409, 0xAD)
+ rom.write_byte(0xACA49D, 0xCE)
+
+ # Speed Zelda's Letter scene
+ rom.write_bytes(0x290E08E, [0x05, 0xF0])
+ rom.write_byte(0xEFCBA7, 0x08)
+ rom.write_byte(0xEFE7C7, 0x05)
+ #rom.write_byte(0xEFEAF7, 0x08)
+ #rom.write_byte(0xEFE7C7, 0x05)
+ rom.write_bytes(0xEFE938, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xEFE948, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xEFE950, [0x00, 0x00, 0x00, 0x00])
+
+ # Speed Zelda escaping from Hyrule Castle
+ Block_code = [0x00, 0x00, 0x00, 0x01, 0x00, 0x21, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02]
+ rom.write_bytes(0x1FC0CF8, Block_code)
+
+ # songs as items flag
+ songs_as_items = (world.shuffle_song_items != 'song') or world.starting_songs
+
+ if songs_as_items:
+ rom.write_byte(rom.sym('SONGS_AS_ITEMS'), 1)
+
+ # Speed learning Zelda's Lullaby
+ rom.write_int32s(0x02E8E90C, [0x000003E8, 0x00000001]) # Terminator Execution
+ if songs_as_items:
+ rom.write_int16s(None, [0x0073, 0x001, 0x0002, 0x0002]) # ID, start, end, end
+ else:
+ rom.write_int16s(None, [0x0073, 0x003B, 0x003C, 0x003C]) # ID, start, end, end
+
+
+ rom.write_int32s(0x02E8E91C, [0x00000013, 0x0000000C]) # Textbox, Count
+ if songs_as_items:
+ rom.write_int16s(None, [0xFFFF, 0x0000, 0x0010, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ else:
+ rom.write_int16s(None, [0x0017, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x00D4, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ # Speed learning Sun's Song
+ if songs_as_items:
+ rom.write_int32(0x0332A4A4, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x0332A4A4, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x0332A868, [0x00000013, 0x00000008]) # Textbox, Count
+ rom.write_int16s(None, [0x0018, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x00D3, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ # Speed learning Saria's Song
+ if songs_as_items:
+ rom.write_int32(0x020B1734, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x020B1734, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x20B1DA8, [0x00000013, 0x0000000C]) # Textbox, Count
+ rom.write_int16s(None, [0x0015, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x00D1, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x020B19C0, [0x0000000A, 0x00000006]) # Link, Count
+ rom.write_int16s(0x020B19C8, [0x0011, 0x0000, 0x0010, 0x0000]) #action, start, end, ????
+ rom.write_int16s(0x020B19F8, [0x003E, 0x0011, 0x0020, 0x0000]) #action, start, end, ????
+ rom.write_int32s(None, [0x80000000, # ???
+ 0x00000000, 0x000001D4, 0xFFFFF731, # start_XYZ
+ 0x00000000, 0x000001D4, 0xFFFFF712]) # end_XYZ
+
+ # Speed learning Epona's Song
+ rom.write_int32s(0x029BEF60, [0x000003E8, 0x00000001]) # Terminator Execution
+ if songs_as_items:
+ rom.write_int16s(None, [0x005E, 0x0001, 0x0002, 0x0002]) # ID, start, end, end
+ else:
+ rom.write_int16s(None, [0x005E, 0x000A, 0x000B, 0x000B]) # ID, start, end, end
+
+ rom.write_int32s(0x029BECB0, [0x00000013, 0x00000002]) # Textbox, Count
+ if songs_as_items:
+ rom.write_int16s(None, [0xFFFF, 0x0000, 0x0009, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ else:
+ rom.write_int16s(None, [0x00D2, 0x0000, 0x0009, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0xFFFF, 0x000A, 0x003C, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ # Speed learning Song of Time
+ rom.write_int32s(0x0252FB98, [0x000003E8, 0x00000001]) # Terminator Execution
+ if songs_as_items:
+ rom.write_int16s(None, [0x0035, 0x0001, 0x0002, 0x0002]) # ID, start, end, end
+ else:
+ rom.write_int16s(None, [0x0035, 0x003B, 0x003C, 0x003C]) # ID, start, end, end
+
+ rom.write_int32s(0x0252FC80, [0x00000013, 0x0000000C]) # Textbox, Count
+ if songs_as_items:
+ rom.write_int16s(None, [0xFFFF, 0x0000, 0x0010, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ else:
+ rom.write_int16s(None, [0x0019, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x00D5, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32(0x01FC3B84, 0xFFFFFFFF) # Other Header?: frame_count
+
+ # Speed learning Song of Storms
+ if songs_as_items:
+ rom.write_int32(0x03041084, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x03041084, 0x0000000A) # Header: frame_count
+
+ rom.write_int32s(0x03041088, [0x00000013, 0x00000002]) # Textbox, Count
+ rom.write_int16s(None, [0x00D6, 0x0000, 0x0009, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0xFFFF, 0x00BE, 0x00C8, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ # Speed learning Minuet of Forest
+ if songs_as_items:
+ rom.write_int32(0x020AFF84, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x020AFF84, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x020B0800, [0x00000013, 0x0000000A]) # Textbox, Count
+ rom.write_int16s(None, [0x000F, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0073, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x020AFF88, [0x0000000A, 0x00000005]) # Link, Count
+ rom.write_int16s(0x020AFF90, [0x0011, 0x0000, 0x0010, 0x0000]) #action, start, end, ????
+ rom.write_int16s(0x020AFFC1, [0x003E, 0x0011, 0x0020, 0x0000]) #action, start, end, ????
+
+ rom.write_int32s(0x020B0488, [0x00000056, 0x00000001]) # Music Change, Count
+ rom.write_int16s(None, [0x003F, 0x0021, 0x0022, 0x0000]) #action, start, end, ????
+
+ rom.write_int32s(0x020B04C0, [0x0000007C, 0x00000001]) # Music Fade Out, Count
+ rom.write_int16s(None, [0x0004, 0x0000, 0x0000, 0x0000]) #action, start, end, ????
+
+ # Speed learning Bolero of Fire
+ if songs_as_items:
+ rom.write_int32(0x0224B5D4, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x0224B5D4, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x0224D7E8, [0x00000013, 0x0000000A]) # Textbox, Count
+ rom.write_int16s(None, [0x0010, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0074, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x0224B5D8, [0x0000000A, 0x0000000B]) # Link, Count
+ rom.write_int16s(0x0224B5E0, [0x0011, 0x0000, 0x0010, 0x0000]) #action, start, end, ????
+ rom.write_int16s(0x0224B610, [0x003E, 0x0011, 0x0020, 0x0000]) #action, start, end, ????
+
+ rom.write_int32s(0x0224B7F0, [0x0000002F, 0x0000000E]) # Sheik, Count
+ rom.write_int16s(0x0224B7F8, [0x0000]) #action
+ rom.write_int16s(0x0224B828, [0x0000]) #action
+ rom.write_int16s(0x0224B858, [0x0000]) #action
+ rom.write_int16s(0x0224B888, [0x0000]) #action
+
+ # Speed learning Serenade of Water
+ if songs_as_items:
+ rom.write_int32(0x02BEB254, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x02BEB254, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x02BEC880, [0x00000013, 0x00000010]) # Textbox, Count
+ rom.write_int16s(None, [0x0011, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0075, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x02BEB258, [0x0000000A, 0x0000000F]) # Link, Count
+ rom.write_int16s(0x02BEB260, [0x0011, 0x0000, 0x0010, 0x0000]) #action, start, end, ????
+ rom.write_int16s(0x02BEB290, [0x003E, 0x0011, 0x0020, 0x0000]) #action, start, end, ????
+
+ rom.write_int32s(0x02BEB530, [0x0000002F, 0x00000006]) # Sheik, Count
+ rom.write_int16s(0x02BEB538, [0x0000, 0x0000, 0x018A, 0x0000]) #action, start, end, ????
+ rom.write_int32s(None, [0x1BBB0000, # ???
+ 0xFFFFFB10, 0x8000011A, 0x00000330, # start_XYZ
+ 0xFFFFFB10, 0x8000011A, 0x00000330]) # end_XYZ
+
+ rom.write_int32s(0x02BEC848, [0x00000056, 0x00000001]) # Music Change, Count
+ rom.write_int16s(None, [0x0059, 0x0021, 0x0022, 0x0000]) #action, start, end, ????
+
+ # Speed learning Nocturne of Shadow
+ rom.write_int32s(0x01FFE458, [0x000003E8, 0x00000001]) # Other Scene? Terminator Execution
+ rom.write_int16s(None, [0x002F, 0x0001, 0x0002, 0x0002]) # ID, start, end, end
+
+ rom.write_int32(0x01FFFDF4, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x02000FD8, [0x00000013, 0x0000000E]) # Textbox, Count
+ if songs_as_items:
+ rom.write_int16s(None, [0xFFFF, 0x0000, 0x0010, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ else:
+ rom.write_int16s(None, [0x0013, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0077, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x02000128, [0x000003E8, 0x00000001]) # Terminator Execution
+ if songs_as_items:
+ rom.write_int16s(None, [0x0032, 0x0001, 0x0002, 0x0002]) # ID, start, end, end
+ else:
+ rom.write_int16s(None, [0x0032, 0x003A, 0x003B, 0x003B]) # ID, start, end, end
+
+ # Speed learning Requiem of Spirit
+ rom.write_int32(0x0218AF14, 0x0000003C) # Header: frame_count
+
+ rom.write_int32s(0x0218C574, [0x00000013, 0x00000008]) # Textbox, Count
+ if songs_as_items:
+ rom.write_int16s(None, [0xFFFF, 0x0000, 0x0010, 0xFFFF, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ else:
+ rom.write_int16s(None, [0x0012, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0076, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x0218B478, [0x000003E8, 0x00000001]) # Terminator Execution
+ if songs_as_items:
+ rom.write_int16s(None, [0x0030, 0x0001, 0x0002, 0x0002]) # ID, start, end, end
+ else:
+ rom.write_int16s(None, [0x0030, 0x003A, 0x003B, 0x003B]) # ID, start, end, end
+
+ rom.write_int32s(0x0218AF18, [0x0000000A, 0x0000000B]) # Link, Count
+ rom.write_int16s(0x0218AF20, [0x0011, 0x0000, 0x0010, 0x0000]) #action, start, end, ????
+ rom.write_int32s(None, [0x40000000, # ???
+ 0xFFFFFAF9, 0x00000008, 0x00000001, # start_XYZ
+ 0xFFFFFAF9, 0x00000008, 0x00000001, # end_XYZ
+ 0x0F671408, 0x00000000, 0x00000001]) # normal_XYZ
+ rom.write_int16s(0x0218AF50, [0x003E, 0x0011, 0x0020, 0x0000]) #action, start, end, ????
+
+ # Speed learning Prelude of Light
+ if songs_as_items:
+ rom.write_int32(0x0252FD24, 0xFFFFFFFF) # Header: frame_count
+ else:
+ rom.write_int32(0x0252FD24, 0x0000004A) # Header: frame_count
+
+ rom.write_int32s(0x02531320, [0x00000013, 0x0000000E]) # Textbox, Count
+ rom.write_int16s(None, [0x0014, 0x0000, 0x0010, 0x0002, 0x088B, 0xFFFF]) # ID, start, end, type, alt1, alt2
+ rom.write_int16s(None, [0x0078, 0x0011, 0x0020, 0x0000, 0xFFFF, 0xFFFF]) # ID, start, end, type, alt1, alt2
+
+ rom.write_int32s(0x0252FF10, [0x0000002F, 0x00000009]) # Sheik, Count
+ rom.write_int16s(0x0252FF18, [0x0006, 0x0000, 0x0000, 0x0000]) #action, start, end, ????
+
+ rom.write_int32s(0x025313D0, [0x00000056, 0x00000001]) # Music Change, Count
+ rom.write_int16s(None, [0x003B, 0x0021, 0x0022, 0x0000]) #action, start, end, ????
+
+ # Speed scene after Deku Tree
+ rom.write_bytes(0x2077E20, [0x00, 0x07, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+ rom.write_bytes(0x2078A10, [0x00, 0x0E, 0x00, 0x1F, 0x00, 0x20, 0x00, 0x20])
+ Block_code = [0x00, 0x80, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0x00, 0x1E, 0x00, 0x28, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]
+ rom.write_bytes(0x2079570, Block_code)
+
+ # Speed scene after Dodongo's Cavern
+ rom.write_bytes(0x2221E88, [0x00, 0x0C, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0x2223308, [0x00, 0x81, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+
+ # Speed scene after Jabu Jabu's Belly
+ rom.write_bytes(0xCA3530, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0x2113340, [0x00, 0x0D, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0x2113C18, [0x00, 0x82, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+ rom.write_bytes(0x21131D0, [0x00, 0x01, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x3C])
+
+ # Speed scene after Forest Temple
+ rom.write_bytes(0xD4ED68, [0x00, 0x45, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0xD4ED78, [0x00, 0x3E, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+ rom.write_bytes(0x207B9D4, [0xFF, 0xFF, 0xFF, 0xFF])
+
+ # Speed scene after Fire Temple
+ rom.write_bytes(0x2001848, [0x00, 0x1E, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+ rom.write_bytes(0xD100B4, [0x00, 0x62, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0xD10134, [0x00, 0x3C, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+
+ # Speed scene after Water Temple
+ rom.write_bytes(0xD5A458, [0x00, 0x15, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0xD5A3A8, [0x00, 0x3D, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+ rom.write_bytes(0x20D0D20, [0x00, 0x29, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0xC8])
+
+ # Speed scene after Shadow Temple
+ rom.write_bytes(0xD13EC8, [0x00, 0x61, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0xD13E18, [0x00, 0x41, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+
+ # Speed scene after Spirit Temple
+ rom.write_bytes(0xD3A0A8, [0x00, 0x60, 0x00, 0x3B, 0x00, 0x3C, 0x00, 0x3C])
+ rom.write_bytes(0xD39FF0, [0x00, 0x3F, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00])
+
+ # Speed Nabooru defeat scene
+ rom.write_bytes(0x2F5AF84, [0x00, 0x00, 0x00, 0x05])
+ rom.write_bytes(0x2F5C7DA, [0x00, 0x01, 0x00, 0x02])
+ rom.write_bytes(0x2F5C7A2, [0x00, 0x03, 0x00, 0x04])
+ rom.write_byte(0x2F5B369, 0x09)
+ rom.write_byte(0x2F5B491, 0x04)
+ rom.write_byte(0x2F5B559, 0x04)
+ rom.write_byte(0x2F5B621, 0x04)
+ rom.write_byte(0x2F5B761, 0x07)
+ rom.write_bytes(0x2F5B840, [0x00, 0x05, 0x00, 0x01, 0x00, 0x05, 0x00, 0x05]) #shorten white flash
+
+ # Speed scene with all medallions
+ rom.write_bytes(0x2512680, [0x00, 0x74, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+
+ # Speed collapse of Ganon's Tower
+ rom.write_bytes(0x33FB328, [0x00, 0x76, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+
+ # Speed Phantom Ganon defeat scene
+ rom.write_bytes(0xC944D8, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xC94548, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xC94730, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xC945A8, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xC94594, [0x00, 0x00, 0x00, 0x00])
+
+ # Speed Twinrova defeat scene
+ rom.write_bytes(0xD678CC, [0x24, 0x01, 0x03, 0xA2, 0xA6, 0x01, 0x01, 0x42])
+ rom.write_bytes(0xD67BA4, [0x10, 0x00])
+
+ # Speed scenes during final battle
+ # Ganondorf battle end
+ rom.write_byte(0xD82047, 0x09)
+ # Zelda descends
+ rom.write_byte(0xD82AB3, 0x66)
+ rom.write_byte(0xD82FAF, 0x65)
+ rom.write_int16s(0xD82D2E, [0x041F])
+ rom.write_int16s(0xD83142, [0x006B])
+ rom.write_bytes(0xD82DD8, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xD82ED4, [0x00, 0x00, 0x00, 0x00])
+ rom.write_byte(0xD82FDF, 0x33)
+ # After tower collapse
+ rom.write_byte(0xE82E0F, 0x04)
+ # Ganon intro
+ rom.write_bytes(0xE83D28, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xE83B5C, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xE84C80, [0x10, 0x00])
+
+ # Speed completion of the trials in Ganon's Castle
+ rom.write_int16s(0x31A8090, [0x006B, 0x0001, 0x0002, 0x0002]) #Forest
+ rom.write_int16s(0x31A9E00, [0x006E, 0x0001, 0x0002, 0x0002]) #Fire
+ rom.write_int16s(0x31A8B18, [0x006C, 0x0001, 0x0002, 0x0002]) #Water
+ rom.write_int16s(0x31A9430, [0x006D, 0x0001, 0x0002, 0x0002]) #Shadow
+ rom.write_int16s(0x31AB200, [0x0070, 0x0001, 0x0002, 0x0002]) #Spirit
+ rom.write_int16s(0x31AA830, [0x006F, 0x0001, 0x0002, 0x0002]) #Light
+
+ # Speed obtaining Fairy Ocarina
+ rom.write_bytes(0x2151230, [0x00, 0x72, 0x00, 0x3C, 0x00, 0x3D, 0x00, 0x3D])
+ Block_code = [0x00, 0x4A, 0x00, 0x00, 0x00, 0x3A, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0x00, 0x3C, 0x00, 0x81, 0xFF, 0xFF]
+ rom.write_bytes(0x2151240, Block_code)
+ rom.write_bytes(0x2150E20, [0xFF, 0xFF, 0xFA, 0x4C])
+
+ if world.shuffle_ocarinas:
+ symbol = rom.sym('OCARINAS_SHUFFLED')
+ rom.write_byte(symbol,0x01)
+
+ # Speed Zelda Light Arrow cutscene
+ rom.write_bytes(0x2531B40, [0x00, 0x28, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+ rom.write_bytes(0x2532FBC, [0x00, 0x75])
+ rom.write_bytes(0x2532FEA, [0x00, 0x75, 0x00, 0x80])
+ rom.write_byte(0x2533115, 0x05)
+ rom.write_bytes(0x2533141, [0x06, 0x00, 0x06, 0x00, 0x10])
+ rom.write_bytes(0x2533171, [0x0F, 0x00, 0x11, 0x00, 0x40])
+ rom.write_bytes(0x25331A1, [0x07, 0x00, 0x41, 0x00, 0x65])
+ rom.write_bytes(0x2533642, [0x00, 0x50])
+ rom.write_byte(0x253389D, 0x74)
+ rom.write_bytes(0x25338A4, [0x00, 0x72, 0x00, 0x75, 0x00, 0x79])
+ rom.write_bytes(0x25338BC, [0xFF, 0xFF])
+ rom.write_bytes(0x25338C2, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
+ rom.write_bytes(0x25339C2, [0x00, 0x75, 0x00, 0x76])
+ rom.write_bytes(0x2533830, [0x00, 0x31, 0x00, 0x81, 0x00, 0x82, 0x00, 0x82])
+
+ # Speed Bridge of Light cutscene
+ rom.write_bytes(0x292D644, [0x00, 0x00, 0x00, 0xA0])
+ rom.write_bytes(0x292D680, [0x00, 0x02, 0x00, 0x0A, 0x00, 0x6C, 0x00, 0x00])
+ rom.write_bytes(0x292D6E8, [0x00, 0x27])
+ rom.write_bytes(0x292D718, [0x00, 0x32])
+ rom.write_bytes(0x292D810, [0x00, 0x02, 0x00, 0x3C])
+ rom.write_bytes(0x292D924, [0xFF, 0xFF, 0x00, 0x14, 0x00, 0x96, 0xFF, 0xFF])
+
+ #Speed Pushing of All Pushable Objects
+ rom.write_bytes(0xDD2B86, [0x40, 0x80]) #block speed
+ rom.write_bytes(0xDD2D26, [0x00, 0x01]) #block delay
+ rom.write_bytes(0xDD9682, [0x40, 0x80]) #milk crate speed
+ rom.write_bytes(0xDD981E, [0x00, 0x01]) #milk crate delay
+ rom.write_bytes(0xCE1BD0, [0x40, 0x80, 0x00, 0x00]) #amy puzzle speed
+ rom.write_bytes(0xCE0F0E, [0x00, 0x01]) #amy puzzle delay
+ rom.write_bytes(0xC77CA8, [0x40, 0x80, 0x00, 0x00]) #fire block speed
+ rom.write_bytes(0xC770C2, [0x00, 0x01]) #fire block delay
+ rom.write_bytes(0xCC5DBC, [0x29, 0xE1, 0x00, 0x01]) #forest basement puzzle delay
+ rom.write_bytes(0xDBCF70, [0x2B, 0x01, 0x00, 0x00]) #spirit cobra mirror startup
+ rom.write_bytes(0xDBCF70, [0x2B, 0x01, 0x00, 0x01]) #spirit cobra mirror delay
+ rom.write_bytes(0xDBA230, [0x28, 0x41, 0x00, 0x19]) #truth spinner speed
+ rom.write_bytes(0xDBA3A4, [0x24, 0x18, 0x00, 0x00]) #truth spinner delay
+
+ #Speed Deku Seed Upgrade Scrub Cutscene
+ rom.write_bytes(0xECA900, [0x24, 0x03, 0xC0, 0x00]) #scrub angle
+ rom.write_bytes(0xECAE90, [0x27, 0x18, 0xFD, 0x04]) #skip straight to giving item
+ rom.write_bytes(0xECB618, [0x25, 0x6B, 0x00, 0xD4]) #skip straight to digging back in
+ rom.write_bytes(0xECAE70, [0x00, 0x00, 0x00, 0x00]) #never initialize cs camera
+ rom.write_bytes(0xE5972C, [0x24, 0x08, 0x00, 0x01]) #timer set to 1 frame for giving item
+
+ # Remove remaining owls
+ rom.write_bytes(0x1FE30CE, [0x01, 0x4B])
+ rom.write_bytes(0x1FE30DE, [0x01, 0x4B])
+ rom.write_bytes(0x1FE30EE, [0x01, 0x4B])
+ rom.write_bytes(0x205909E, [0x00, 0x3F])
+ rom.write_byte(0x2059094, 0x80)
+
+ # Darunia won't dance
+ rom.write_bytes(0x22769E4, [0xFF, 0xFF, 0xFF, 0xFF])
+
+ # Zora moves quickly
+ rom.write_bytes(0xE56924, [0x00, 0x00, 0x00, 0x00])
+
+ # Speed Jabu Jabu swallowing Link
+ rom.write_bytes(0xCA0784, [0x00, 0x18, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+
+ # Ruto no longer points to Zora Sapphire
+ rom.write_bytes(0xD03BAC, [0xFF, 0xFF, 0xFF, 0xFF])
+
+ # Ruto never disappears from Jabu Jabu's Belly
+ rom.write_byte(0xD01EA3, 0x00)
+
+ #Shift octorock in jabu forward
+ rom.write_bytes(0x275906E, [0xFF, 0xB3, 0xFB, 0x20, 0xF9, 0x56])
+
+ #Move fire/forest temple switches down 1 unit to make it easier to press
+ rom.write_bytes(0x24860A8, [0xFC, 0xF4]) #forest basement 1
+ rom.write_bytes(0x24860C8, [0xFC, 0xF4]) #forest basement 2
+ rom.write_bytes(0x24860E8, [0xFC, 0xF4]) #forest basement 3
+ rom.write_bytes(0x236C148, [0x11, 0x93]) #fire hammer room
+
+ # Speed up Epona race start
+ rom.write_bytes(0x29BE984, [0x00, 0x00, 0x00, 0x02])
+ rom.write_bytes(0x29BE9CA, [0x00, 0x01, 0x00, 0x02])
+
+ # Speed start of Horseback Archery
+ #rom.write_bytes(0x21B2064, [0x00, 0x00, 0x00, 0x02])
+ #rom.write_bytes(0x21B20AA, [0x00, 0x01, 0x00, 0x02])
+
+ # Speed up Epona escape
+ rom.write_bytes(0x1FC8B36, [0x00, 0x2A])
+
+ # Speed up draining the well
+ rom.write_bytes(0xE0A010, [0x00, 0x2A, 0x00, 0x01, 0x00, 0x02, 0x00, 0x02])
+ rom.write_bytes(0x2001110, [0x00, 0x2B, 0x00, 0xB7, 0x00, 0xB8, 0x00, 0xB8])
+
+ # Speed up opening the royal tomb for both child and adult
+ rom.write_bytes(0x2025026, [0x00, 0x01])
+ rom.write_bytes(0x2023C86, [0x00, 0x01])
+ rom.write_byte(0x2025159, 0x02)
+ rom.write_byte(0x2023E19, 0x02)
+
+ #Speed opening of Door of Time
+ rom.write_bytes(0xE0A176, [0x00, 0x02])
+ rom.write_bytes(0xE0A35A, [0x00, 0x01, 0x00, 0x02])
+
+ # Speed up Lake Hylia Owl Flight
+ rom.write_bytes(0x20E60D2, [0x00, 0x01])
+
+ # Speed up Death Mountain Trail Owl Flight
+ rom.write_bytes(0x223B6B2, [0x00, 0x01])
+
+ # Poacher's Saw no longer messes up Forest Stage
+ rom.write_bytes(0xAE72CC, [0x00, 0x00, 0x00, 0x00])
+
+ # Change Prelude CS to check for medallion
+ rom.write_bytes(0x00C805E6, [0x00, 0xA6])
+ rom.write_bytes(0x00C805F2, [0x00, 0x01])
+
+ # Change Nocturne CS to check for medallions
+ rom.write_bytes(0x00ACCD8E, [0x00, 0xA6])
+ rom.write_bytes(0x00ACCD92, [0x00, 0x01])
+ rom.write_bytes(0x00ACCD9A, [0x00, 0x02])
+ rom.write_bytes(0x00ACCDA2, [0x00, 0x04])
+
+ # Change King Zora to move even if Zora Sapphire is in inventory
+ rom.write_bytes(0x00E55BB0, [0x85, 0xCE, 0x8C, 0x3C])
+ rom.write_bytes(0x00E55BB4, [0x84, 0x4F, 0x0E, 0xDA])
+
+ # Remove extra Forest Temple medallions
+ rom.write_bytes(0x00D4D37C, [0x00, 0x00, 0x00, 0x00])
+
+ # Remove extra Fire Temple medallions
+ rom.write_bytes(0x00AC9754, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0x00D0DB8C, [0x00, 0x00, 0x00, 0x00])
+
+ # Remove extra Water Temple medallions
+ rom.write_bytes(0x00D57F94, [0x00, 0x00, 0x00, 0x00])
+
+ # Remove extra Spirit Temple medallions
+ rom.write_bytes(0x00D370C4, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0x00D379C4, [0x00, 0x00, 0x00, 0x00])
+
+ # Remove extra Shadow Temple medallions
+ rom.write_bytes(0x00D116E0, [0x00, 0x00, 0x00, 0x00])
+
+ # Change Mido, Saria, and Kokiri to check for Deku Tree complete flag
+ # bitwise pointer for 0x80
+ kokiriAddresses = [0xE52836, 0xE53A56, 0xE51D4E, 0xE51F3E, 0xE51D96, 0xE51E1E, 0xE51E7E, 0xE51EDE, 0xE51FC6, 0xE51F96, 0xE293B6, 0xE29B8E, 0xE62EDA, 0xE630D6, 0xE633AA, 0xE6369E]
+ for kokiri in kokiriAddresses:
+ rom.write_bytes(kokiri, [0x8C, 0x0C])
+ # Kokiri
+ rom.write_bytes(0xE52838, [0x94, 0x48, 0x0E, 0xD4])
+ rom.write_bytes(0xE53A58, [0x94, 0x49, 0x0E, 0xD4])
+ rom.write_bytes(0xE51D50, [0x94, 0x58, 0x0E, 0xD4])
+ rom.write_bytes(0xE51F40, [0x94, 0x4B, 0x0E, 0xD4])
+ rom.write_bytes(0xE51D98, [0x94, 0x4B, 0x0E, 0xD4])
+ rom.write_bytes(0xE51E20, [0x94, 0x4A, 0x0E, 0xD4])
+ rom.write_bytes(0xE51E80, [0x94, 0x59, 0x0E, 0xD4])
+ rom.write_bytes(0xE51EE0, [0x94, 0x4E, 0x0E, 0xD4])
+ rom.write_bytes(0xE51FC8, [0x94, 0x49, 0x0E, 0xD4])
+ rom.write_bytes(0xE51F98, [0x94, 0x58, 0x0E, 0xD4])
+ # Saria
+ rom.write_bytes(0xE293B8, [0x94, 0x78, 0x0E, 0xD4])
+ rom.write_bytes(0xE29B90, [0x94, 0x68, 0x0E, 0xD4])
+ # Mido
+ rom.write_bytes(0xE62EDC, [0x94, 0x6F, 0x0E, 0xD4])
+ rom.write_bytes(0xE630D8, [0x94, 0x4F, 0x0E, 0xD4])
+ rom.write_bytes(0xE633AC, [0x94, 0x68, 0x0E, 0xD4])
+ rom.write_bytes(0xE636A0, [0x94, 0x48, 0x0E, 0xD4])
+
+ # Change adult Kokiri Forest to check for Forest Temple complete flag
+ rom.write_bytes(0xE5369E, [0xB4, 0xAC])
+ rom.write_bytes(0xD5A83C, [0x80, 0x49, 0x0E, 0xDC])
+
+ # Change adult Goron City to check for Fire Temple complete flag
+ rom.write_bytes(0xED59DC, [0x80, 0xC9, 0x0E, 0xDC])
+
+ # Change Pokey to check DT complete flag
+ rom.write_bytes(0xE5400A, [0x8C, 0x4C])
+ rom.write_bytes(0xE5400E, [0xB4, 0xA4])
+ if world.open_forest != 'closed':
+ rom.write_bytes(0xE5401C, [0x14, 0x0B])
+
+ # Fix Shadow Temple to check for different rewards for scene
+ rom.write_bytes(0xCA3F32, [0x00, 0x00, 0x25, 0x4A, 0x00, 0x10])
+
+ # Fix Spirit Temple to check for different rewards for scene
+ rom.write_bytes(0xCA3EA2, [0x00, 0x00, 0x25, 0x4A, 0x00, 0x08])
+
+ # Fix Biggoron to check a different flag.
+ rom.write_byte(0xED329B, 0x72)
+ rom.write_byte(0xED43E7, 0x72)
+ rom.write_bytes(0xED3370, [0x3C, 0x0D, 0x80, 0x12])
+ rom.write_bytes(0xED3378, [0x91, 0xB8, 0xA6, 0x42, 0xA1, 0xA8, 0xA6, 0x42])
+ rom.write_bytes(0xED6574, [0x00, 0x00, 0x00, 0x00])
+
+ # Remove the check on the number of days that passed for claim check.
+ rom.write_bytes(0xED4470, [0x00, 0x00, 0x00, 0x00])
+ rom.write_bytes(0xED4498, [0x00, 0x00, 0x00, 0x00])
+
+ # Fixed reward order for Bombchu Bowling
+ rom.write_bytes(0xE2E698, [0x80, 0xAA, 0xE2, 0x64])
+ rom.write_bytes(0xE2E6A0, [0x80, 0xAA, 0xE2, 0x4C])
+ rom.write_bytes(0xE2D440, [0x24, 0x19, 0x00, 0x00])
+
+ # Offset kakariko carpenter starting position
+ rom.write_bytes(0x1FF93A4, [0x01, 0x8D, 0x00, 0x11, 0x01, 0x6C, 0xFF, 0x92, 0x00, 0x00, 0x01, 0x78, 0xFF, 0x2E, 0x00, 0x00, 0x00, 0x03, 0xFD, 0x2B, 0x00, 0xC8, 0xFF, 0xF9, 0xFD, 0x03, 0x00, 0xC8, 0xFF, 0xA9, 0xFD, 0x5D, 0x00, 0xC8, 0xFE, 0x5F]) # re order the carpenter's path
+ rom.write_byte(0x1FF93D0, 0x06) # set the path points to 6
+ rom.write_bytes(0x20160B6, [0x01, 0x8D, 0x00, 0x11, 0x01, 0x6C]) # set the carpenter's start position
+
+ # Give hp after first ocarina minigame round
+ rom.write_bytes(0xDF2204, [0x24, 0x03, 0x00, 0x02])
+
+ # Allow owl to always carry the kid down Death Mountain
+ rom.write_bytes(0xE304F0, [0x24, 0x0E, 0x00, 0x01])
+
+ # Fix Vanilla Dodongo's Cavern Gossip Stone to not use a permanent flag for the fairy
+ if not world.dungeon_mq['Dodongos Cavern']:
+ rom.write_byte(0x1F281FE, 0x38)
+
+ # Fix "...???" textbox outside Child Colossus Fairy to use the right flag and disappear once the wall is destroyed
+ rom.write_byte(0x21A026F, 0xDD)
+
+ # Remove the "...???" textbox outside the Crater Fairy (change it to an actor that does nothing)
+ rom.write_int16s(0x225E7DC, [0x00B5, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0xFFFF])
+
+ # Forbid Sun's Song from a bunch of cutscenes
+ Suns_scenes = [0x2016FC9, 0x2017219, 0x20173D9, 0x20174C9, 0x2017679, 0x20C1539, 0x20C15D9, 0x21A0719, 0x21A07F9, 0x2E90129, 0x2E901B9, 0x2E90249, 0x225E829, 0x225E939, 0x306D009]
+ for address in Suns_scenes:
+ rom.write_byte(address,0x01)
+
+ # Allow Warp Songs in additional places
+ rom.write_byte(0xB6D3D2, 0x00) # Gerudo Training Grounds
+ rom.write_byte(0xB6D42A, 0x00) # Inside Ganon's Castle
+
+ #Tell Sheik at Ice Cavern we are always an Adult
+ rom.write_int32(0xC7B9C0, 0x00000000)
+ rom.write_int32(0xC7BAEC, 0x00000000)
+ rom.write_int32(0xc7BCA4, 0x00000000)
+
+ # Allow Farore's Wind in dungeons where it's normally forbidden
+ rom.write_byte(0xB6D3D3, 0x00) # Gerudo Training Grounds
+ rom.write_byte(0xB6D42B, 0x00) # Inside Ganon's Castle
+
+ # Remove disruptive text from Gerudo Training Grounds and early Shadow Temple (vanilla)
+ Wonder_text = [0x27C00BC, 0x27C00CC, 0x27C00DC, 0x27C00EC, 0x27C00FC, 0x27C010C, 0x27C011C, 0x27C012C, 0x27CE080,
+ 0x27CE090, 0x2887070, 0x2887080, 0x2887090, 0x2897070, 0x28C7134, 0x28D91BC, 0x28A60F4, 0x28AE084,
+ 0x28B9174, 0x28BF168, 0x28BF178, 0x28BF188, 0x28A1144, 0x28A6104, 0x28D0094]
+ for address in Wonder_text:
+ rom.write_byte(address, 0xFB)
+
+ # Speed dig text for Dampe
+ rom.write_bytes(0x9532F8, [0x08, 0x08, 0x08, 0x59])
+
+ # Make item descriptions into a single box
+ Short_item_descriptions = [0x92EC84, 0x92F9E3, 0x92F2B4, 0x92F37A, 0x92F513, 0x92F5C6, 0x92E93B, 0x92EA12]
+ for address in Short_item_descriptions:
+ rom.write_byte(address,0x02)
+
+ et_original = rom.read_bytes(0xB6FBF0, 4 * 0x0614)
+
+ exit_updates = []
+
+ def copy_entrance_record(source_index, destination_index, count=4):
+ ti = source_index * 4
+ rom.write_bytes(0xB6FBF0 + destination_index * 4, et_original[ti:ti+(4 * count)])
+
+ def generate_exit_lookup_table():
+ # Assumes that the last exit on a scene's exit list cannot be 0000
+ exit_table = {
+ 0x0028: [0xAC95C2] #Jabu with the fish is entered from a cutscene hardcode
+ }
+
+ def add_scene_exits(scene_start, offset = 0):
+ current = scene_start + offset
+ exit_list_start_off = 0
+ exit_list_end_off = 0
+ command = 0
+
+ while command != 0x14:
+ command = rom.read_byte(current)
+ if command == 0x18: # Alternate header list
+ header_list = scene_start + (rom.read_int32(current + 4) & 0x00FFFFFF)
+ for alt_id in range(0,3):
+ header_offset = rom.read_int32(header_list) & 0x00FFFFFF
+ if header_offset != 0:
+ add_scene_exits(scene_start, header_offset)
+ header_list += 4
+ if command == 0x13: # Exit List
+ exit_list_start_off = rom.read_int32(current + 4) & 0x00FFFFFF
+ if command == 0x0F: # Lighting list, follows exit list
+ exit_list_end_off = rom.read_int32(current + 4) & 0x00FFFFFF
+ current += 8
+
+ if exit_list_start_off == 0 or exit_list_end_off == 0:
+ return
+
+ # calculate the exit list length
+ list_length = (exit_list_end_off - exit_list_start_off) // 2
+ last_id = rom.read_int16(scene_start + exit_list_end_off - 2)
+ if last_id == 0:
+ list_length -= 1
+
+ # update
+ addr = scene_start + exit_list_start_off
+ for _ in range(0, list_length):
+ index = rom.read_int16(addr)
+ if index not in exit_table:
+ exit_table[index] = []
+ exit_table[index].append(addr)
+ addr += 2
+
+ scene_table = 0x00B71440
+ for scene in range(0x00, 0x65):
+ scene_start = rom.read_int32(scene_table + (scene * 0x14));
+ add_scene_exits(scene_start)
+
+ return exit_table
+
+
+ def set_entrance_updates(entrances):
+ for entrance in entrances:
+ new_entrance = entrance.data
+ replaced_entrance = entrance.replaces.data
+
+ exit_updates.append((new_entrance['index'], replaced_entrance['index']))
+
+ for address in new_entrance.get('addresses', []):
+ rom.write_int16(address, replaced_entrance['index'])
+
+ if "blue_warp" in new_entrance:
+ if "blue_warp" in replaced_entrance:
+ blue_out_data = replaced_entrance["blue_warp"]
+ else:
+ blue_out_data = replaced_entrance["index"]
+ # Blue warps have multiple hardcodes leading to them. The good news is
+ # the blue warps (excluding deku sprout and lake fill special cases) each
+ # have a nice consistent 4-entry in the table we can just shuffle. So just
+ # catch all the hardcode with entrance table rewrite. This covers the
+ # Forest temple and Water temple blue warp revisits. Deku sprout remains
+ # vanilla as it never took you to the exit and the lake fill is handled
+ # above by removing the cutscene completely. Child has problems with Adult
+ # blue warps, so always use the return entrance if a child.
+ copy_entrance_record(blue_out_data + 2, new_entrance["blue_warp"] + 2, 2)
+ copy_entrance_record(replaced_entrance["index"], new_entrance["blue_warp"], 2)
+
+ exit_table = generate_exit_lookup_table()
+
+ if world.entrance_shuffle:
+ # Disable the fog state entirely to avoid fog glitches
+ rom.write_byte(rom.sym('NO_FOG_STATE'), 1)
+
+ if world.disable_trade_revert:
+ # Disable trade quest timers and prevent trade items from ever reverting
+ rom.write_byte(rom.sym('DISABLE_TIMERS'), 0x01)
+ rom.write_int16s(0xB6D460, [0x0030, 0x0035, 0x0036]) # Change trade items revert table to prevent all reverts
+
+ if world.shuffle_overworld_entrances:
+ rom.write_byte(rom.sym('OVERWORLD_SHUFFLED'), 1)
+
+ # Prevent the ocarina cutscene from leading straight to hyrule field
+ rom.write_byte(rom.sym('OCARINAS_SHUFFLED'), 1)
+
+ # Combine all fence hopping LLR exits to lead to the main LLR exit
+ for k in [0x028A, 0x028E, 0x0292]: # Southern, Western, Eastern Gates
+ exit_table[0x01F9] += exit_table[k] # Hyrule Field entrance from Lon Lon Ranch (main land entrance)
+ del exit_table[k]
+ exit_table[0x01F9].append(0xD52722) # 0x0476, Front Gate
+
+ # Combine the water exits between Hyrule Field and Zora River to lead to the land entrance instead of the water entrance
+ exit_table[0x00EA] += exit_table[0x01D9] # Hyrule Field -> Zora River
+ exit_table[0x0181] += exit_table[0x0311] # Zora River -> Hyrule Field
+ del exit_table[0x01D9]
+ del exit_table[0x0311]
+
+ # Change Impa escorts to bring link at the hyrule castle grounds entrance from market, instead of hyrule field
+ rom.write_int16(0xACAA2E, 0x0138) # 1st Impa escort
+ rom.write_int16(0xD12D6E, 0x0138) # 2nd+ Impa escort
+
+ if world.shuffle_dungeon_entrances:
+ rom.write_byte(rom.sym('DUNGEONS_SHUFFLED'), 1)
+
+ # Connect lake hylia fill exit to revisit exit
+ rom.write_int16(0xAC995A, 0x060C)
+
+ # Tell the well water we are always a child.
+ rom.write_int32(0xDD5BF4, 0x00000000)
+
+ # Make the Adult well blocking stone dissappear if the well has been drained by
+ # checking the well drain event flag instead of links age. This actor doesn't need a
+ # code check for links age as the stone is absent for child via the scene alternate
+ # lists. So replace the age logic with drain logic.
+ rom.write_int32(0xE2887C, rom.read_int32(0xE28870)) # relocate this to nop delay slot
+ rom.write_int32(0xE2886C, 0x95CEB4B0) # lhu
+ rom.write_int32(0xE28870, 0x31CE0080) # andi
+
+ remove_entrance_blockers(rom)
+
+ # Purge temp flags on entrance to spirit from colossus through the front door.
+ rom.write_byte(0x021862E3, 0xC2)
+
+ if world.shuffle_overworld_entrances or world.shuffle_dungeon_entrances:
+ # Remove deku sprout and drop player at SFM after forest completion
+ rom.write_int16(0xAC9F96, 0x0608)
+
+ if world.spawn_positions:
+ # Fix save warping inside Link's House to not be a special case
+ rom.write_int32(0xB06318, 0x00000000)
+
+ # Set entrances to update, except grotto entrances which are handled on their own at a later point
+ set_entrance_updates(filter(lambda entrance: entrance.type != 'Grotto', world.get_shuffled_entrances()))
+
+ for k, v in [(k,v) for k, v in exit_updates if k in exit_table]:
+ for addr in exit_table[k]:
+ rom.write_int16(addr, v)
+
+ # Fix text for Pocket Cucco.
+ rom.write_byte(0xBEEF45, 0x0B)
+
+ # Fix stupid alcove cameras in Ice Cavern -- thanks to krim and mzx for the help
+ rom.write_byte(0x2BECA25,0x01)
+ rom.write_byte(0x2BECA2D,0x01)
+
+ configure_dungeon_info(rom, world)
+
+ hash_icons = 0
+ for i,icon in enumerate(world.file_hash):
+ hash_icons |= (icon << (5 * i))
+ rom.write_int32(rom.sym('cfg_file_select_hash'), hash_icons)
+
+ save_context = SaveContext()
+
+ # Initial Save Data
+ if not world.useful_cutscenes:
+ save_context.write_bits(0x00D4 + 0x03 * 0x1C + 0x04 + 0x0, 0x08) # Forest Temple switch flag (Poe Sisters cutscene)
+ save_context.write_bits(0x00D4 + 0x05 * 0x1C + 0x04 + 0x1, 0x01) # Water temple switch flag (Ruto)
+ save_context.write_bits(0x00D4 + 0x51 * 0x1C + 0x04 + 0x2, 0x08) # Hyrule Field switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x55 * 0x1C + 0x04 + 0x0, 0x80) # Kokiri Forest switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x56 * 0x1C + 0x04 + 0x2, 0x40) # Sacred Forest Meadow switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x5B * 0x1C + 0x04 + 0x2, 0x01) # Lost Woods switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x5B * 0x1C + 0x04 + 0x3, 0x80) # Lost Woods switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x5C * 0x1C + 0x04 + 0x0, 0x80) # Desert Colossus switch flag (Owl)
+ save_context.write_bits(0x00D4 + 0x5F * 0x1C + 0x04 + 0x3, 0x20) # Hyrule Castle switch flag (Owl)
+
+ save_context.write_bits(0x0ED4, 0x10) # "Met Deku Tree"
+ save_context.write_bits(0x0ED5, 0x20) # "Deku Tree Opened Mouth"
+ save_context.write_bits(0x0ED6, 0x08) # "Rented Horse From Ingo"
+ save_context.write_bits(0x0ED6, 0x10) # "Spoke to Mido After Deku Tree's Death"
+ save_context.write_bits(0x0EDA, 0x08) # "Began Nabooru Battle"
+ save_context.write_bits(0x0EDC, 0x80) # "Entered the Master Sword Chamber"
+ save_context.write_bits(0x0EDD, 0x20) # "Pulled Master Sword from Pedestal"
+ save_context.write_bits(0x0EE0, 0x80) # "Spoke to Kaepora Gaebora by Lost Woods"
+ save_context.write_bits(0x0EE7, 0x20) # "Nabooru Captured by Twinrova"
+ save_context.write_bits(0x0EE7, 0x10) # "Spoke to Nabooru in Spirit Temple"
+ save_context.write_bits(0x0EED, 0x20) # "Sheik, Spawned at Master Sword Pedestal as Adult"
+ save_context.write_bits(0x0EED, 0x01) # "Nabooru Ordered to Fight by Twinrova"
+ save_context.write_bits(0x0EED, 0x80) # "Watched Ganon's Tower Collapse / Caught by Gerudo"
+ save_context.write_bits(0x0EF9, 0x01) # "Greeted by Saria"
+ save_context.write_bits(0x0F0A, 0x04) # "Spoke to Ingo Once as Adult"
+ save_context.write_bits(0x0F0F, 0x40) # "Met Poe Collector in Ruined Market"
+ if not world.useful_cutscenes:
+ save_context.write_bits(0x0F1A, 0x04) # "Met Darunia in Fire Temple"
+
+ save_context.write_bits(0x0ED7, 0x01) # "Spoke to Child Malon at Castle or Market"
+ save_context.write_bits(0x0ED7, 0x20) # "Spoke to Child Malon at Ranch"
+ save_context.write_bits(0x0ED7, 0x40) # "Invited to Sing With Child Malon"
+ save_context.write_bits(0x0F09, 0x10) # "Met Child Malon at Castle or Market"
+ save_context.write_bits(0x0F09, 0x20) # "Child Malon Said Epona Was Scared of You"
+
+ save_context.write_bits(0x0F21, 0x04) # "Ruto in JJ (M3) Talk First Time"
+ save_context.write_bits(0x0F21, 0x02) # "Ruto in JJ (M2) Meet Ruto"
+
+ save_context.write_bits(0x0EE2, 0x01) # "Began Ganondorf Battle"
+ save_context.write_bits(0x0EE3, 0x80) # "Began Bongo Bongo Battle"
+ save_context.write_bits(0x0EE3, 0x40) # "Began Barinade Battle"
+ save_context.write_bits(0x0EE3, 0x20) # "Began Twinrova Battle"
+ save_context.write_bits(0x0EE3, 0x10) # "Began Morpha Battle"
+ save_context.write_bits(0x0EE3, 0x08) # "Began Volvagia Battle"
+ save_context.write_bits(0x0EE3, 0x04) # "Began Phantom Ganon Battle"
+ save_context.write_bits(0x0EE3, 0x02) # "Began King Dodongo Battle"
+ save_context.write_bits(0x0EE3, 0x01) # "Began Gohma Battle"
+
+ save_context.write_bits(0x0EE8, 0x01) # "Entered Deku Tree"
+ save_context.write_bits(0x0EE9, 0x80) # "Entered Temple of Time"
+ save_context.write_bits(0x0EE9, 0x40) # "Entered Goron City"
+ save_context.write_bits(0x0EE9, 0x20) # "Entered Hyrule Castle"
+ save_context.write_bits(0x0EE9, 0x10) # "Entered Zora's Domain"
+ save_context.write_bits(0x0EE9, 0x08) # "Entered Kakariko Village"
+ save_context.write_bits(0x0EE9, 0x02) # "Entered Death Mountain Trail"
+ save_context.write_bits(0x0EE9, 0x01) # "Entered Hyrule Field"
+ save_context.write_bits(0x0EEA, 0x04) # "Entered Ganon's Castle (Exterior)"
+ save_context.write_bits(0x0EEA, 0x02) # "Entered Death Mountain Crater"
+ save_context.write_bits(0x0EEA, 0x01) # "Entered Desert Colossus"
+ save_context.write_bits(0x0EEB, 0x80) # "Entered Zora's Fountain"
+ save_context.write_bits(0x0EEB, 0x40) # "Entered Graveyard"
+ save_context.write_bits(0x0EEB, 0x20) # "Entered Jabu-Jabu's Belly"
+ save_context.write_bits(0x0EEB, 0x10) # "Entered Lon Lon Ranch"
+ save_context.write_bits(0x0EEB, 0x08) # "Entered Gerudo's Fortress"
+ save_context.write_bits(0x0EEB, 0x04) # "Entered Gerudo Valley"
+ save_context.write_bits(0x0EEB, 0x02) # "Entered Lake Hylia"
+ save_context.write_bits(0x0EEB, 0x01) # "Entered Dodongo's Cavern"
+ save_context.write_bits(0x0F08, 0x08) # "Entered Hyrule Castle"
+
+ # Set the number of chickens to collect
+ rom.write_byte(0x00E1E523, world.chicken_count)
+
+ # Change Anju to always say how many chickens are needed
+ # Does not affect text for collecting item or afterwards
+ rom.write_int16(0x00E1F3C2, 0x5036)
+ rom.write_int16(0x00E1F3C4, 0x5036)
+ rom.write_int16(0x00E1F3C6, 0x5036)
+ rom.write_int16(0x00E1F3C8, 0x5036)
+ rom.write_int16(0x00E1F3CA, 0x5036)
+ rom.write_int16(0x00E1F3CC, 0x5036)
+
+ # Make the Kakariko Gate not open with the MS
+ if world.open_kakariko != 'open':
+ rom.write_int32(0xDD3538, 0x34190000) # li t9, 0
+ if world.open_kakariko != 'closed':
+ rom.write_byte(rom.sym('OPEN_KAKARIKO'), 1)
+
+ if world.complete_mask_quest:
+ rom.write_byte(rom.sym('COMPLETE_MASK_QUEST'), 1)
+
+ if world.skip_child_zelda:
+ save_context.give_item('Zeldas Letter')
+ # Archipelago forces this item to be local so it can always be given to the player. Usually it's a song so it's no problem.
+ item = world.get_location('Song from Impa').item
+ save_context.give_raw_item(item.name)
+ save_context.write_bits(0x0ED7, 0x04) # "Obtained Malon's Item"
+ save_context.write_bits(0x0ED7, 0x08) # "Woke Talon in castle"
+ save_context.write_bits(0x0ED7, 0x10) # "Talon has fled castle"
+ save_context.write_bits(0x0EDD, 0x01) # "Obtained Zelda's Letter"
+ save_context.write_bits(0x0EDE, 0x02) # "Learned Zelda's Lullaby"
+ save_context.write_bits(0x00D4 + 0x5F * 0x1C + 0x04 + 0x3, 0x10) # "Moved crates to access the courtyard"
+ if world.open_kakariko != 'closed':
+ save_context.write_bits(0x0F07, 0x40) # "Spoke to Gate Guard About Mask Shop"
+ if world.complete_mask_quest:
+ save_context.write_bits(0x0F07, 0x80) # "Soldier Wears Keaton Mask"
+ save_context.write_bits(0x0EF6, 0x8F) # "Sold Masks & Unlocked Masks" / "Obtained Mask of Truth"
+ save_context.write_bits(0x0EE4, 0xF0) # "Paid Back Mask Fees"
+
+ if world.zora_fountain == 'open':
+ save_context.write_bits(0x0EDB, 0x08) # "Moved King Zora"
+ elif world.zora_fountain == 'adult':
+ rom.write_byte(rom.sym('MOVED_ADULT_KING_ZORA'), 1)
+
+ # Make all chest opening animations fast
+ rom.write_byte(rom.sym('FAST_CHESTS'), int(world.fast_chests))
+
+
+ # Set up Rainbow Bridge conditions
+ symbol = rom.sym('RAINBOW_BRIDGE_CONDITION')
+ count_symbol = rom.sym('RAINBOW_BRIDGE_COUNT')
+ if world.bridge == 'open':
+ rom.write_int32(symbol, 0)
+ save_context.write_bits(0xEDC, 0x20) # "Rainbow Bridge Built by Sages"
+ elif world.bridge == 'medallions':
+ rom.write_int32(symbol, 1)
+ rom.write_int16(count_symbol, world.bridge_medallions)
+ elif world.bridge == 'dungeons':
+ rom.write_int32(symbol, 2)
+ rom.write_int16(count_symbol, world.bridge_rewards)
+ elif world.bridge == 'stones':
+ rom.write_int32(symbol, 3)
+ rom.write_int16(count_symbol, world.bridge_stones)
+ elif world.bridge == 'vanilla':
+ rom.write_int32(symbol, 4)
+ elif world.bridge == 'tokens':
+ rom.write_int32(symbol, 5)
+ rom.write_int16(count_symbol, world.bridge_tokens)
+
+ if world.triforce_hunt:
+ rom.write_int16(rom.sym('triforce_pieces_requied'), world.triforce_goal)
+ rom.write_int16(rom.sym('triforce_hunt_enabled'), 1)
+
+ # Set up LACS conditions.
+ symbol = rom.sym('LACS_CONDITION')
+ count_symbol = rom.sym('LACS_CONDITION_COUNT')
+ if world.lacs_condition == 'medallions':
+ rom.write_int32(symbol, 1)
+ rom.write_int16(count_symbol, world.lacs_medallions)
+ elif world.lacs_condition == 'dungeons':
+ rom.write_int32(symbol, 2)
+ rom.write_int16(count_symbol, world.lacs_rewards)
+ elif world.lacs_condition == 'stones':
+ rom.write_int32(symbol, 3)
+ rom.write_int16(count_symbol, world.lacs_stones)
+ elif world.lacs_condition == 'tokens':
+ rom.write_int32(symbol, 4)
+ rom.write_int16(count_symbol, world.lacs_tokens)
+ else:
+ rom.write_int32(symbol, 0)
+
+ if world.open_forest == 'open':
+ save_context.write_bits(0xED5, 0x10) # "Showed Mido Sword & Shield"
+
+ if world.open_door_of_time:
+ save_context.write_bits(0xEDC, 0x08) # "Opened the Door of Time"
+
+ # "fast-ganon" stuff
+ symbol = rom.sym('NO_ESCAPE_SEQUENCE')
+ if world.no_escape_sequence:
+ rom.write_bytes(0xD82A12, [0x05, 0x17]) # Sets exit from Ganondorf fight to entrance to Ganon fight
+ rom.write_bytes(0xB139A2, [0x05, 0x17]) # Sets Ganon deathwarp back to Ganon
+ rom.write_byte(symbol, 0x01)
+ else:
+ rom.write_byte(symbol, 0x00)
+ if world.skipped_trials['Forest']:
+ save_context.write_bits(0x0EEA, 0x08) # "Completed Forest Trial"
+ if world.skipped_trials['Fire']:
+ save_context.write_bits(0x0EEA, 0x40) # "Completed Fire Trial"
+ if world.skipped_trials['Water']:
+ save_context.write_bits(0x0EEA, 0x10) # "Completed Water Trial"
+ if world.skipped_trials['Spirit']:
+ save_context.write_bits(0x0EE8, 0x20) # "Completed Spirit Trial"
+ if world.skipped_trials['Shadow']:
+ save_context.write_bits(0x0EEA, 0x20) # "Completed Shadow Trial"
+ if world.skipped_trials['Light']:
+ save_context.write_bits(0x0EEA, 0x80) # "Completed Light Trial"
+ if world.trials == 0:
+ save_context.write_bits(0x0EED, 0x08) # "Dispelled Ganon's Tower Barrier"
+
+ # open gerudo fortress
+ if world.gerudo_fortress == 'open':
+ if not world.shuffle_gerudo_card:
+ save_context.write_bits(0x00A5, 0x40) # Give Gerudo Card
+ save_context.write_bits(0x0EE7, 0x0F) # Free all 4 carpenters
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x1, 0x0F) # Thieves' Hideout switch flags (started all fights)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x2, 0x01) # Thieves' Hideout switch flags (heard yells/unlocked doors)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x3, 0xFE) # Thieves' Hideout switch flags (heard yells/unlocked doors)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x0C + 0x2, 0xD4) # Thieves' Hideout collection flags (picked up keys, marks fights finished as well)
+ elif world.gerudo_fortress == 'fast':
+ save_context.write_bits(0x0EE7, 0x0E) # Free 3 carpenters
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x1, 0x0D) # Thieves' Hideout switch flags (started all fights)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x2, 0x01) # Thieves' Hideout switch flags (heard yells/unlocked doors)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x04 + 0x3, 0xDC) # Thieves' Hideout switch flags (heard yells/unlocked doors)
+ save_context.write_bits(0x00D4 + 0x0C * 0x1C + 0x0C + 0x2, 0xC4) # Thieves' Hideout collection flags (picked up keys, marks fights finished as well)
+
+ # Add a gate-opening guard on the Wasteland side of the Gerudo gate when the card is shuffled or certain levels of ER.
+ # Overrides the generic guard at the bottom of the ladder in Gerudo Fortress
+ if world.shuffle_gerudo_card or world.shuffle_overworld_entrances or \
+ world.shuffle_special_interior_entrances or world.spawn_positions:
+ # Add a gate opening guard on the Wasteland side of the Gerudo Fortress' gate
+ new_gate_opening_guard = [0x0138, 0xFAC8, 0x005D, 0xF448, 0x0000, 0x95B0, 0x0000, 0x0301]
+ rom.write_int16s(0x21BD3EC, new_gate_opening_guard) # Adult Day
+ rom.write_int16s(0x21BD62C, new_gate_opening_guard) # Adult Night
+
+ # start with maps/compasses
+ if world.shuffle_mapcompass == 'startwith':
+ for dungeon in ['deku', 'dodongo', 'jabu', 'forest', 'fire', 'water', 'spirit', 'shadow', 'botw', 'ice']:
+ save_context.addresses['dungeon_items'][dungeon]['compass'].value = True
+ save_context.addresses['dungeon_items'][dungeon]['map'].value = True
+
+ if world.shuffle_smallkeys == 'vanilla':
+ if world.dungeon_mq['Spirit Temple']:
+ save_context.addresses['keys']['spirit'].value = 3
+
+ if world.start_with_rupees:
+ rom.write_byte(rom.sym('MAX_RUPEES'), 0x01)
+
+ # Set starting time of day
+ if world.starting_tod != 'default':
+ tod = {
+ 'sunrise': 0x4555,
+ 'morning': 0x6000,
+ 'noon': 0x8001,
+ 'afternoon': 0xA000,
+ 'sunset': 0xC001,
+ 'evening': 0xE000,
+ 'midnight': 0x0000,
+ 'witching-hour': 0x2000,
+
+ }
+ save_context.addresses['time_of_day'].value = tod[world.starting_tod]
+
+ if world.starting_age == 'adult':
+ save_context.addresses['link_age'].value = False # Set link's age to adult
+ save_context.addresses['scene_index'].value = 0x43 # Set the scene index to Temple of Time
+ save_context.addresses['equip_items']['master_sword'].value = True # Equip Master Sword by default
+ save_context.addresses['equip_items']['kokiri_tunic'].value = True # Equip Kokiri Tunic & Kokiri Boots by default
+ save_context.addresses['equip_items']['kokiri_boots'].value = True # (to avoid issues when going back child for the first time)
+ save_context.write_byte(0x0F33, 0x00) # Unset Swordless Flag (to avoid issues with sword getting unequipped)
+
+ # Revert change that Skips the Epona Race
+ if not world.no_epona_race:
+ rom.write_int32(0xA9E838, 0x03E00008)
+ else:
+ save_context.write_bits(0xF0E, 0x01) # Set talked to Malon flag
+
+ # skip castle guard stealth sequence
+ if world.no_guard_stealth:
+ # change the exit at child/day crawlspace to the end of zelda's goddess cutscene
+ rom.write_bytes(0x21F60DE, [0x05, 0xF0])
+
+ # patch mq scenes
+ mq_scenes = []
+ if world.dungeon_mq['Deku Tree']:
+ mq_scenes.append(0)
+ if world.dungeon_mq['Dodongos Cavern']:
+ mq_scenes.append(1)
+ if world.dungeon_mq['Jabu Jabus Belly']:
+ mq_scenes.append(2)
+ if world.dungeon_mq['Forest Temple']:
+ mq_scenes.append(3)
+ if world.dungeon_mq['Fire Temple']:
+ mq_scenes.append(4)
+ if world.dungeon_mq['Water Temple']:
+ mq_scenes.append(5)
+ if world.dungeon_mq['Spirit Temple']:
+ mq_scenes.append(6)
+ if world.dungeon_mq['Shadow Temple']:
+ mq_scenes.append(7)
+ if world.dungeon_mq['Bottom of the Well']:
+ mq_scenes.append(8)
+ if world.dungeon_mq['Ice Cavern']:
+ mq_scenes.append(9)
+ # Scene 10 has no layout changes, so it doesn't need to be patched
+ if world.dungeon_mq['Gerudo Training Grounds']:
+ mq_scenes.append(11)
+ if world.dungeon_mq['Ganons Castle']:
+ mq_scenes.append(13)
+
+ patch_files(rom, mq_scenes)
+
+ ### Load Shop File
+ # Move shop actor file to free space
+ shop_item_file = File({
+ 'Name':'En_GirlA',
+ 'Start':'00C004E0',
+ 'End':'00C02E00',
+ })
+ shop_item_file.relocate(rom)
+
+ # Increase the shop item table size
+ shop_item_vram_start = rom.read_int32(0x00B5E490 + (0x20 * 4) + 0x08)
+ insert_space(rom, shop_item_file, shop_item_vram_start, 1, 0x3C + (0x20 * 50), 0x20 * 50)
+
+ # Add relocation entries for shop item table
+ new_relocations = []
+ for i in range(50, 100):
+ new_relocations.append(shop_item_file.start + 0x1DEC + (i * 0x20) + 0x04)
+ new_relocations.append(shop_item_file.start + 0x1DEC + (i * 0x20) + 0x14)
+ new_relocations.append(shop_item_file.start + 0x1DEC + (i * 0x20) + 0x1C)
+ add_relocations(rom, shop_item_file, new_relocations)
+
+ # update actor table
+ rom.write_int32s(0x00B5E490 + (0x20 * 4),
+ [shop_item_file.start,
+ shop_item_file.end,
+ shop_item_vram_start,
+ shop_item_vram_start + (shop_item_file.end - shop_item_file.start)])
+
+ # Update DMA Table
+ update_dmadata(rom, shop_item_file)
+
+ # Create 2nd Bazaar Room
+ bazaar_room_file = File({
+ 'Name':'shop1_room_1',
+ 'Start':'028E4000',
+ 'End':'0290D7B0',
+ })
+ bazaar_room_file.copy(rom)
+
+ # Add new Bazaar Room to Bazaar Scene
+ rom.write_int32s(0x28E3030, [0x00010000, 0x02000058]) #reduce position list size
+ rom.write_int32s(0x28E3008, [0x04020000, 0x02000070]) #expand room list size
+
+ rom.write_int32s(0x28E3070, [0x028E4000, 0x0290D7B0,
+ bazaar_room_file.start, bazaar_room_file.end]) #room list
+ rom.write_int16s(0x28E3080, [0x0000, 0x0001]) # entrance list
+ rom.write_int16(0x28E4076, 0x0005) # Change shop to Kakariko Bazaar
+ #rom.write_int16(0x3489076, 0x0005) # Change shop to Kakariko Bazaar
+
+ # Load Message and Shop Data
+ messages = read_messages(rom)
+ remove_unused_messages(messages)
+ shop_items = read_shop_items(rom, shop_item_file.start + 0x1DEC)
+
+ # Set Big Poe count to get reward from buyer
+ poe_points = world.big_poe_count * 100
+ rom.write_int16(0xEE69CE, poe_points)
+ # update dialogue
+ new_message = "\x08Hey, young man. What's happening \x01today? If you have a \x05\x41Poe\x05\x40, I will \x01buy it.\x04\x1AIf you earn \x05\x41%d points\x05\x40, you'll\x01be a happy man! Heh heh.\x04\x08Your card now has \x05\x45\x1E\x01 \x05\x40points.\x01Come back again!\x01Heh heh heh!\x02" % poe_points
+ update_message_by_id(messages, 0x70F5, new_message)
+ if world.big_poe_count != 10:
+ new_message = "\x1AOh, you brought a Poe today!\x04\x1AHmmmm!\x04\x1AVery interesting!\x01This is a \x05\x41Big Poe\x05\x40!\x04\x1AI'll buy it for \x05\x4150 Rupees\x05\x40.\x04On top of that, I'll put \x05\x41100\x01points \x05\x40on your card.\x04\x1AIf you earn \x05\x41%d points\x05\x40, you'll\x01be a happy man! Heh heh." % poe_points
+ update_message_by_id(messages, 0x70f7, new_message)
+ new_message = "\x1AWait a minute! WOW!\x04\x1AYou have earned \x05\x41%d points\x05\x40!\x04\x1AYoung man, you are a genuine\x01\x05\x41Ghost Hunter\x05\x40!\x04\x1AIs that what you expected me to\x01say? Heh heh heh!\x04\x1ABecause of you, I have extra\x01inventory of \x05\x41Big Poes\x05\x40, so this will\x01be the last time I can buy a \x01ghost.\x04\x1AYou're thinking about what I \x01promised would happen when you\x01earned %d points. Heh heh.\x04\x1ADon't worry, I didn't forget.\x01Just take this." % (poe_points, poe_points)
+ update_message_by_id(messages, 0x70f8, new_message)
+
+ # Update Child Anju's dialogue
+ new_message = "\x08What should I do!?\x01My \x05\x41Cuccos\x05\x40 have all flown away!\x04You, little boy, please!\x01Please gather at least \x05\x41%d Cuccos\x05\x40\x01for me.\x02" % world.chicken_count
+ update_message_by_id(messages, 0x5036, new_message)
+
+ # Update "Princess Ruto got the Spiritual Stone!" text before the midboss in Jabu
+ reward_text = {'Kokiri Emerald': "\x05\x42Kokiri Emerald\x05\x40",
+ 'Goron Ruby': "\x05\x41Goron Ruby\x05\x40",
+ 'Zora Sapphire': "\x05\x43Zora Sapphire\x05\x40",
+ 'Forest Medallion': "\x05\x42Forest Medallion\x05\x40",
+ 'Fire Medallion': "\x05\x41Fire Medallion\x05\x40",
+ 'Water Medallion': "\x05\x43Water Medallion\x05\x40",
+ 'Spirit Medallion': "\x05\x46Spirit Medallion\x05\x40",
+ 'Shadow Medallion': "\x05\x45Shadow Medallion\x05\x40",
+ 'Light Medallion': "\x05\x44Light Medallion\x05\x40"
+ }
+ new_message = "\x1a\x08Princess Ruto got the \x01%s!\x09\x01\x14\x02But\x14\x00 why Princess Ruto?\x02" % reward_text[world.get_location('Barinade').item.name]
+ update_message_by_id(messages, 0x4050, new_message)
+
+ # use faster jabu elevator
+ if not world.dungeon_mq['Jabu Jabus Belly'] and world.shuffle_scrubs == 'off':
+ symbol = rom.sym('JABU_ELEVATOR_ENABLE')
+ rom.write_byte(symbol, 0x01)
+
+ if world.skip_some_minigame_phases:
+ save_context.write_bits(0x00D4 + 0x48 * 0x1C + 0x08 + 0x3, 0x10) # Beat First Dampe Race (& Chest Spawned)
+ rom.write_byte(rom.sym('CHAIN_HBA_REWARDS'), 1)
+ # Update the first horseback archery text to make it clear both rewards are available from the start
+ update_message_by_id(messages, 0x6040, "Hey newcomer, you have a fine \x01horse!\x04I don't know where you stole \x01it from, but...\x04OK, how about challenging this \x01\x05\x41horseback archery\x05\x40?\x04Once the horse starts galloping,\x01shoot the targets with your\x01arrows. \x04Let's see how many points you \x01can score. You get 20 arrows.\x04If you can score \x05\x411,000 points\x05\x40, I will \x01give you something good! And even \x01more if you score \x05\x411,500 points\x05\x40!\x0B\x02")
+
+ # Sets hooks for gossip stone changes
+
+ symbol = rom.sym("GOSSIP_HINT_CONDITION");
+
+ if world.hints == 'none':
+ rom.write_int32(symbol, 0)
+ else:
+ writeGossipStoneHints(world, messages)
+
+ if world.hints == 'mask':
+ rom.write_int32(symbol, 0)
+ elif world.hints == 'always':
+ rom.write_int32(symbol, 2)
+ else:
+ rom.write_int32(symbol, 1)
+
+
+ # build silly ganon lines
+ if world.misc_hints:
+ buildGanonText(world, messages)
+
+ # Write item overrides
+ override_table = get_override_table(world)
+ rom.write_bytes(rom.sym('cfg_item_overrides'), get_override_table_bytes(override_table))
+ rom.write_byte(rom.sym('PLAYER_ID'), world.player) # Write player ID
+ rom.write_bytes(rom.sym('AP_PLAYER_NAME'), bytearray(world.world.get_player_name(world.player), 'ascii'))
+
+ # Revert Song Get Override Injection
+ if not songs_as_items:
+ # general get song
+ rom.write_int32(0xAE5DF8, 0x240200FF)
+ rom.write_int32(0xAE5E04, 0xAD0F00A4)
+ # requiem of spirit
+ rom.write_int32s(0xAC9ABC, [0x3C010001, 0x00300821])
+ # sun song
+ rom.write_int32(0xE09F68, 0x8C6F00A4)
+ rom.write_int32(0xE09F74, 0x01CFC024)
+ rom.write_int32(0xE09FB0, 0x240F0001)
+ # song of time
+ rom.write_int32(0xDB532C, 0x24050003)
+
+
+ # Set damage multiplier
+ if world.damage_multiplier == 'half':
+ rom.write_byte(rom.sym('CFG_DAMAGE_MULTIPLYER'), 0xFF)
+ if world.damage_multiplier == 'normal':
+ rom.write_byte(rom.sym('CFG_DAMAGE_MULTIPLYER'), 0)
+ if world.damage_multiplier == 'double':
+ rom.write_byte(rom.sym('CFG_DAMAGE_MULTIPLYER'), 1)
+ if world.damage_multiplier == 'quadruple':
+ rom.write_byte(rom.sym('CFG_DAMAGE_MULTIPLYER'), 2)
+ if world.damage_multiplier == 'ohko':
+ rom.write_byte(rom.sym('CFG_DAMAGE_MULTIPLYER'), 3)
+
+ # Patch songs and boss rewards
+ for location in world.world.get_filled_locations(world.player):
+ item = location.item
+ special = item.special if item.game == 'Ocarina of Time' else {} # this shouldn't matter hopefully
+ locationaddress = location.address1
+ secondaryaddress = location.address2
+
+ if location.type == 'Song' and not songs_as_items:
+ bit_mask_pointer = 0x8C34 + ((special['item_id'] - 0x65) * 4)
+ rom.write_byte(locationaddress, special['song_id'])
+ next_song_id = special['song_id'] + 0x0D
+ rom.write_byte(secondaryaddress, next_song_id)
+ if location.name == 'Song from Impa':
+ rom.write_byte(0x0D12ECB, special['item_id'])
+ rom.write_byte(0x2E8E931, special['text_id']) #Fix text box
+ elif location.name == 'Song from Malon':
+ rom.write_byte(rom.sym('MALON_TEXT_ID'), special['text_id'])
+ elif location.name == 'Song from Composers Grave':
+ rom.write_int16(0xE09F66, bit_mask_pointer)
+ rom.write_byte(0x332A87D, special['text_id']) #Fix text box
+ elif location.name == 'Song from Saria':
+ rom.write_byte(0x0E2A02B, special['item_id'])
+ rom.write_byte(0x20B1DBD, special['text_id']) #Fix text box
+ elif location.name == 'Song from Ocarina of Time':
+ rom.write_byte(0x252FC95, special['text_id']) #Fix text box
+ elif location.name == 'Song from Windmill':
+ rom.write_byte(rom.sym('WINDMILL_SONG_ID'), next_song_id)
+ rom.write_byte(rom.sym('WINDMILL_TEXT_ID'), special['text_id'])
+ elif location.name == 'Sheik in Forest':
+ rom.write_byte(0x0C7BAA3, special['item_id'])
+ rom.write_byte(0x20B0815, special['text_id']) #Fix text box
+ elif location.name == 'Sheik at Temple':
+ rom.write_byte(0x0C805EF, special['item_id'])
+ rom.write_byte(0x2531335, special['text_id']) #Fix text box
+ elif location.name == 'Sheik in Crater':
+ rom.write_byte(0x0C7BC57, special['item_id'])
+ rom.write_byte(0x224D7FD, special['text_id']) #Fix text box
+ elif location.name == 'Sheik in Ice Cavern':
+ rom.write_byte(0x0C7BD77, special['item_id'])
+ rom.write_byte(0x2BEC895, special['text_id']) #Fix text box
+ elif location.name == 'Sheik in Kakariko':
+ rom.write_byte(0x0AC9A5B, special['item_id'])
+ rom.write_byte(0x2000FED, special['text_id']) #Fix text box
+ elif location.name == 'Sheik at Colossus':
+ rom.write_byte(0x218C589, special['text_id']) #Fix text box
+ elif location.type == 'Boss':
+ if location.name == 'Links Pocket':
+ save_context.give_item(item.name)
+ else:
+ rom.write_byte(locationaddress, special['item_id'])
+ rom.write_byte(secondaryaddress, special['addr2_data'])
+ bit_mask_hi = special['bit_mask'] >> 16
+ bit_mask_lo = special['bit_mask'] & 0xFFFF
+ if location.name == 'Bongo Bongo':
+ rom.write_int16(0xCA3F32, bit_mask_hi)
+ rom.write_int16(0xCA3F36, bit_mask_lo)
+ elif location.name == 'Twinrova':
+ rom.write_int16(0xCA3EA2, bit_mask_hi)
+ rom.write_int16(0xCA3EA6, bit_mask_lo)
+
+ # add a cheaper bombchu pack to the bombchu shop
+ # describe
+ update_message_by_id(messages, 0x80FE, '\x08\x05\x41Bombchu (5 pieces) 60 Rupees\x01\x05\x40This looks like a toy mouse, but\x01it\'s actually a self-propelled time\x01bomb!\x09\x0A', 0x03)
+ # purchase
+ update_message_by_id(messages, 0x80FF, '\x08Bombchu 5 Pieces 60 Rupees\x01\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x09', 0x03)
+ rbl_bombchu = shop_items[0x0018]
+ rbl_bombchu.price = 60
+ rbl_bombchu.pieces = 5
+ rbl_bombchu.get_item_id = 0x006A
+ rbl_bombchu.description_message = 0x80FE
+ rbl_bombchu.purchase_message = 0x80FF
+
+ # Reduce 10 Pack Bombchus from 100 to 99 Rupees
+ shop_items[0x0015].price = 99
+ shop_items[0x0019].price = 99
+ shop_items[0x001C].price = 99
+ update_message_by_id(messages, shop_items[0x001C].description_message, "\x08\x05\x41Bombchu (10 pieces) 99 Rupees\x01\x05\x40This looks like a toy mouse, but\x01it's actually a self-propelled time\x01bomb!\x09\x0A")
+ update_message_by_id(messages, shop_items[0x001C].purchase_message, "\x08Bombchu 10 pieces 99 Rupees\x09\x01\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40")
+
+ shuffle_messages.shop_item_messages = []
+
+ # kokiri shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('KF Kokiri Shop').locations, True)
+ shop_objs |= {0x00FC, 0x00B2, 0x0101, 0x0102, 0x00FD, 0x00C5} # Shop objects
+ rom.write_byte(0x2587029, len(shop_objs))
+ rom.write_int32(0x258702C, 0x0300F600)
+ rom.write_int16s(0x2596600, list(shop_objs))
+
+ # kakariko bazaar
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('Kak Bazaar').locations)
+ shop_objs |= {0x005B, 0x00B2, 0x00C5, 0x0107, 0x00C9, 0x016B} # Shop objects
+ rom.write_byte(0x28E4029, len(shop_objs))
+ rom.write_int32(0x28E402C, 0x03007A40)
+ rom.write_int16s(0x28EBA40, list(shop_objs))
+
+ # castle town bazaar
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('Market Bazaar').locations)
+ shop_objs |= {0x005B, 0x00B2, 0x00C5, 0x0107, 0x00C9, 0x016B} # Shop objects
+ rom.write_byte(bazaar_room_file.start + 0x29, len(shop_objs))
+ rom.write_int32(bazaar_room_file.start + 0x2C, 0x03007A40)
+ rom.write_int16s(bazaar_room_file.start + 0x7A40, list(shop_objs))
+
+ # goron shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('GC Shop').locations)
+ shop_objs |= {0x00C9, 0x00B2, 0x0103, 0x00AF} # Shop objects
+ rom.write_byte(0x2D33029, len(shop_objs))
+ rom.write_int32(0x2D3302C, 0x03004340)
+ rom.write_int16s(0x2D37340, list(shop_objs))
+
+ # zora shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('ZD Shop').locations)
+ shop_objs |= {0x005B, 0x00B2, 0x0104, 0x00FE} # Shop objects
+ rom.write_byte(0x2D5B029, len(shop_objs))
+ rom.write_int32(0x2D5B02C, 0x03004B40)
+ rom.write_int16s(0x2D5FB40, list(shop_objs))
+
+ # kakariko potion shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('Kak Potion Shop Front').locations)
+ shop_objs |= {0x0159, 0x00B2, 0x0175, 0x0122} # Shop objects
+ rom.write_byte(0x2D83029, len(shop_objs))
+ rom.write_int32(0x2D8302C, 0x0300A500)
+ rom.write_int16s(0x2D8D500, list(shop_objs))
+
+ # market potion shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('Market Potion Shop').locations)
+ shop_objs |= {0x0159, 0x00B2, 0x0175, 0x00C5, 0x010C, 0x016B} # Shop objects
+ rom.write_byte(0x2DB0029, len(shop_objs))
+ rom.write_int32(0x2DB002C, 0x03004E40)
+ rom.write_int16s(0x2DB4E40, list(shop_objs))
+
+ # bombchu shop
+ shop_objs = place_shop_items(rom, world, shop_items, messages,
+ world.get_region('Market Bombchu Shop').locations)
+ shop_objs |= {0x0165, 0x00B2} # Shop objects
+ rom.write_byte(0x2DD8029, len(shop_objs))
+ rom.write_int32(0x2DD802C, 0x03006A40)
+ rom.write_int16s(0x2DDEA40, list(shop_objs))
+
+ # Scrub text stuff.
+ def update_scrub_text(message, text_replacement, default_price, price, item_name=None):
+ scrub_strip_text = ["some ", "1 piece ", "5 pieces ", "30 pieces "]
+ for text in scrub_strip_text:
+ message = message.replace(text.encode(), b'')
+ message = message.replace(text_replacement[0].encode(), text_replacement[1].encode())
+ message = message.replace(b'they are', b'it is')
+ if default_price != price:
+ message = message.replace(('%d Rupees' % default_price).encode(), ('%d Rupees' % price).encode())
+ if item_name is not None:
+ message = message.replace(b'mysterious item', item_name.encode())
+ return message
+
+ single_item_scrubs = {
+ 0x3E: world.get_location("HF Deku Scrub Grotto"),
+ 0x77: world.get_location("LW Deku Scrub Near Bridge"),
+ 0x79: world.get_location("LW Deku Scrub Grotto Front"),
+ }
+
+ scrub_message_dict = {}
+ if world.shuffle_scrubs == 'off':
+ # Revert Deku Scrubs changes
+ rom.write_int32s(0xEBB85C, [
+ 0x24010002, # addiu at, zero, 2
+ 0x3C038012, # lui v1, 0x8012
+ 0x14410004, # bne v0, at, 0xd8
+ 0x2463A5D0, # addiu v1, v1, -0x5a30
+ 0x94790EF0])# lhu t9, 0xef0(v1)
+ rom.write_int32(0xDF7CB0,
+ 0xA44F0EF0) # sh t7, 0xef0(v0)
+
+ # Replace scrub text for 3 default shuffled scrubs.
+ for (scrub_item, default_price, text_id, text_replacement) in business_scrubs:
+ if scrub_item not in single_item_scrubs.keys():
+ continue
+ scrub_message_dict[text_id] = update_scrub_text(get_message_by_id(messages, text_id).raw_text, text_replacement, default_price, default_price)
+ else:
+ # Rebuild Business Scrub Item Table
+ rom.seek_address(0xDF8684)
+ for (scrub_item, default_price, text_id, text_replacement) in business_scrubs:
+ price = world.scrub_prices[scrub_item]
+ rom.write_int16(None, price) # Price
+ rom.write_int16(None, 1) # Count
+ rom.write_int32(None, scrub_item) # Item
+ rom.write_int32(None, 0x80A74FF8) # Can_Buy_Func
+ rom.write_int32(None, 0x80A75354) # Buy_Func
+
+ scrub_message_dict[text_id] = update_scrub_text(get_message_by_id(messages, text_id).raw_text, text_replacement, default_price, price)
+
+ # update actor IDs
+ set_deku_salesman_data(rom)
+
+ # Update scrub messages.
+ shuffle_messages.scrubs_message_ids = []
+ for text_id, message in scrub_message_dict.items():
+ update_message_by_id(messages, text_id, message)
+ if world.shuffle_scrubs == 'random':
+ shuffle_messages.scrubs_message_ids.append(text_id)
+
+ if world.shuffle_grotto_entrances:
+ # Build the Grotto Load Table based on grotto entrance data
+ for entrance in world.get_shuffled_entrances(type='Grotto'):
+ if entrance.primary:
+ load_table_pointer = rom.sym('GROTTO_LOAD_TABLE') + 4 * entrance.data['grotto_id']
+ rom.write_int16(load_table_pointer, entrance.data['entrance'])
+ rom.write_byte(load_table_pointer + 2, entrance.data['content'])
+
+ # Update grotto actors based on their new entrance
+ set_grotto_shuffle_data(rom, world)
+
+ if world.shuffle_cows:
+ rom.write_byte(rom.sym('SHUFFLE_COWS'), 0x01)
+ # Move some cows because they are too close from each other in vanilla
+ rom.write_bytes(0x33650CA, [0xFE, 0xD3, 0x00, 0x00, 0x00, 0x6E, 0x00, 0x00, 0x4A, 0x34]) # LLR Tower right cow
+ rom.write_bytes(0x2C550AE, [0x00, 0x82]) # LLR Stable right cow
+ set_cow_id_data(rom, world)
+
+ if world.shuffle_beans:
+ rom.write_byte(rom.sym('SHUFFLE_BEANS'), 0x01)
+ # Update bean salesman messages to better fit the fact that he sells a randomized item
+ update_message_by_id(messages, 0x405E, "\x1AChomp chomp chomp...\x01We have... \x05\x41a mysterious item\x05\x40! \x01Do you want it...huh? Huh?\x04\x05\x41\x0860 Rupees\x05\x40 and it's yours!\x01Keyahahah!\x01\x1B\x05\x42Yes\x01No\x05\x40\x02")
+ update_message_by_id(messages, 0x4069, "You don't have enough money.\x01I can't sell it to you.\x01Chomp chomp...\x02")
+ update_message_by_id(messages, 0x406C, "We hope you like it!\x01Chomp chomp chomp.\x02")
+ # Change first magic bean to cost 60 (is used as the price for the one time item when beans are shuffled)
+ rom.write_byte(0xE209FD, 0x3C)
+
+ if world.shuffle_medigoron_carpet_salesman:
+ rom.write_byte(rom.sym('SHUFFLE_CARPET_SALESMAN'), 0x01)
+ # Update carpet salesman messages to better fit the fact that he sells a randomized item
+ update_message_by_id(messages, 0x6077, "\x06\x41Well Come!\x04I am selling stuff, strange and \x01rare, from all over the world to \x01everybody.\x01Today's special is...\x04A mysterious item! \x01Intriguing! \x01I won't tell you what it is until \x01I see the money....\x04How about \x05\x41200 Rupees\x05\x40?\x01\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40\x02")
+ update_message_by_id(messages, 0x6078, "Thank you very much!\x04The mark that will lead you to\x01the Spirit Temple is the \x05\x41flag on\x01the left \x05\x40outside the shop.\x01Be seeing you!\x02")
+
+ rom.write_byte(rom.sym('SHUFFLE_MEDIGORON'), 0x01)
+ # Update medigoron messages to better fit the fact that he sells a randomized item
+ update_message_by_id(messages, 0x304C, "I have something cool right here.\x01How about it...\x07\x30\x4F\x02")
+ update_message_by_id(messages, 0x304D, "How do you like it?\x02")
+ update_message_by_id(messages, 0x304F, "How about buying this cool item for \x01200 Rupees?\x01\x1B\x05\x42Buy\x01Don't buy\x05\x40\x02")
+
+ if world.shuffle_smallkeys == 'remove' or world.shuffle_bosskeys == 'remove' or world.shuffle_ganon_bosskey == 'remove':
+ locked_doors = get_locked_doors(rom, world)
+ for _,[door_byte, door_bits] in locked_doors.items():
+ save_context.write_bits(door_byte, door_bits)
+
+ # Fix chest animations
+ if world.bombchus_in_logic:
+ bombchu_ids = [0x6A, 0x03, 0x6B]
+ for i in bombchu_ids:
+ item = read_rom_item(rom, i)
+ item['chest_type'] = 0
+ write_rom_item(rom, i, item)
+ if world.bridge == 'tokens' or world.lacs_condition == 'tokens':
+ item = read_rom_item(rom, 0x5B)
+ item['chest_type'] = 0
+ write_rom_item(rom, 0x5B, item)
+
+ # Update chest type sizes
+ if world.correct_chest_sizes:
+ symbol = rom.sym('CHEST_SIZE_MATCH_CONTENTS')
+ rom.write_int32(symbol, 0x00000001)
+ # Move Ganon's Castle's Zelda's Lullaby Chest back so is reachable if large
+ if not world.dungeon_mq['Ganons Castle']:
+ rom.write_int16(0x321B176, 0xFC40) # original 0xFC48
+
+ # Move Spirit Temple Compass Chest if it is a small chest so it is reachable with hookshot
+ if not world.dungeon_mq['Spirit Temple']:
+ chest_name = 'Spirit Temple Compass Chest'
+ chest_address = 0x2B6B07C
+ location = world.get_location(chest_name)
+ item = read_rom_item(rom, location.item.index)
+ if item['chest_type'] in (1, 3):
+ rom.write_int16(chest_address + 2, 0x0190) # X pos
+ rom.write_int16(chest_address + 6, 0xFABC) # Z pos
+
+ # Move Silver Gauntlets chest if it is small so it is reachable from Spirit Hover Seam
+ if world.logic_rules != 'glitchless':
+ chest_name = 'Spirit Temple Silver Gauntlets Chest'
+ chest_address_0 = 0x21A02D0 # Address in setup 0
+ chest_address_2 = 0x21A06E4 # Address in setup 2
+ location = world.get_location(chest_name)
+ item = read_rom_item(rom, location.item.index)
+ if item['chest_type'] in (1, 3):
+ rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
+ rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
+
+ # give dungeon items the correct messages
+ add_item_messages(messages, shop_items, world)
+ if world.enhance_map_compass:
+ reward_list = {'Kokiri Emerald': "\x05\x42Kokiri Emerald\x05\x40",
+ 'Goron Ruby': "\x05\x41Goron Ruby\x05\x40",
+ 'Zora Sapphire': "\x05\x43Zora Sapphire\x05\x40",
+ 'Forest Medallion': "\x05\x42Forest Medallion\x05\x40",
+ 'Fire Medallion': "\x05\x41Fire Medallion\x05\x40",
+ 'Water Medallion': "\x05\x43Water Medallion\x05\x40",
+ 'Spirit Medallion': "\x05\x46Spirit Medallion\x05\x40",
+ 'Shadow Medallion': "\x05\x45Shadow Medallion\x05\x40",
+ 'Light Medallion': "\x05\x44Light Medallion\x05\x40"
+ }
+ dungeon_list = {'Deku Tree': ("the \x05\x42Deku Tree", 'Queen Gohma', 0x62, 0x88),
+ 'Dodongos Cavern': ("\x05\x41Dodongo\'s Cavern", 'King Dodongo', 0x63, 0x89),
+ 'Jabu Jabus Belly': ("\x05\x43Jabu Jabu\'s Belly", 'Barinade', 0x64, 0x8a),
+ 'Forest Temple': ("the \x05\x42Forest Temple", 'Phantom Ganon', 0x65, 0x8b),
+ 'Fire Temple': ("the \x05\x41Fire Temple", 'Volvagia', 0x7c, 0x8c),
+ 'Water Temple': ("the \x05\x43Water Temple", 'Morpha', 0x7d, 0x8e),
+ 'Spirit Temple': ("the \x05\x46Spirit Temple", 'Twinrova', 0x7e, 0x8f),
+ 'Ice Cavern': ("the \x05\x44Ice Cavern", None, 0x87, 0x92),
+ 'Bottom of the Well': ("the \x05\x45Bottom of the Well", None, 0xa2, 0xa5),
+ 'Shadow Temple': ("the \x05\x45Shadow Temple", 'Bongo Bongo', 0x7f, 0xa3),
+ }
+ for dungeon in world.dungeon_mq:
+ if dungeon in ['Gerudo Training Grounds', 'Ganons Castle']:
+ pass
+ elif dungeon in ['Bottom of the Well', 'Ice Cavern']:
+ dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
+ if len(world.world.worlds) > 1:
+ map_message = "\x13\x76\x08\x05\x42\x0F\x05\x40 found the \x05\x41Dungeon Map\x05\x40\x01for %s\x05\x40!\x09" % (dungeon_name)
+ else:
+ map_message = "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for %s\x05\x40!\x01It\'s %s!\x09" % (dungeon_name, "masterful" if world.dungeon_mq[dungeon] else "ordinary")
+
+ if world.mq_dungeons_random or world.mq_dungeons != 0 and world.mq_dungeons != 12:
+ update_message_by_id(messages, map_id, map_message)
+ else:
+ dungeon_name, boss_name, compass_id, map_id = dungeon_list[dungeon]
+ dungeon_reward = reward_list[world.get_location(boss_name).item.name]
+ if len(world.world.worlds) > 1:
+ compass_message = "\x13\x75\x08\x05\x42\x0F\x05\x40 found the \x05\x41Compass\x05\x40\x01for %s\x05\x40!\x09" % (dungeon_name)
+ else:
+ compass_message = "\x13\x75\x08You found the \x05\x41Compass\x05\x40\x01for %s\x05\x40!\x01It holds the %s!\x09" % (dungeon_name, dungeon_reward)
+ update_message_by_id(messages, compass_id, compass_message)
+ if world.mq_dungeons_random or world.mq_dungeons != 0 and world.mq_dungeons != 12:
+ if len(world.world.worlds) > 1:
+ map_message = "\x13\x76\x08\x05\x42\x0F\x05\x40 found the \x05\x41Dungeon Map\x05\x40\x01for %s\x05\x40!\x09" % (dungeon_name)
+ else:
+ map_message = "\x13\x76\x08You found the \x05\x41Dungeon Map\x05\x40\x01for %s\x05\x40!\x01It\'s %s!\x09" % (dungeon_name, "masterful" if world.dungeon_mq[dungeon] else "ordinary")
+ update_message_by_id(messages, map_id, map_message)
+
+ # Set hints on the altar inside ToT
+ rom.write_int16(0xE2ADB2, 0x707A)
+ rom.write_int16(0xE2ADB6, 0x7057)
+ buildAltarHints(world, messages, include_rewards=world.misc_hints and not world.enhance_map_compass, include_wincons=world.misc_hints)
+
+ # Set Dungeon Reward actors in Jabu Jabu to be accurate
+ jabu_actor_type = world.get_location('Barinade').item.special['actor_type']
+ set_jabu_stone_actors(rom, jabu_actor_type)
+ # Also set the right object for the actor, since medallions and stones require different objects
+ # MQ is handled separately, as we include both objects in the object list in mqu.json (Scene 2, Room 6)
+ if not world.dungeon_mq['Jabu Jabus Belly']:
+ jabu_stone_object = world.get_location('Barinade').item.special['object_id']
+ rom.write_int16(0x277D068, jabu_stone_object)
+ rom.write_int16(0x277D168, jabu_stone_object)
+
+ # update happy mask shop to use new SOLD OUT text id
+ rom.write_int16(shop_item_file.start + 0x1726, shop_items[0x26].description_message)
+
+ # Add 3rd Wallet Upgrade
+ rom.write_int16(0xB6D57E, 0x0003)
+ rom.write_int16(0xB6EC52, 999)
+ tycoon_message = "\x08\x13\x57You got a \x05\x43Tycoon's Wallet\x05\x40!\x01Now you can hold\x01up to \x05\x46999\x05\x40 \x05\x46Rupees\x05\x40."
+ if len(world.world.worlds) > 1:
+ tycoon_message = make_player_message(tycoon_message)
+ update_message_by_id(messages, 0x00F8, tycoon_message, 0x23)
+
+ write_shop_items(rom, shop_item_file.start + 0x1DEC, shop_items)
+
+ permutation = None
+
+ # text shuffle
+ if world.text_shuffle == 'except_hints':
+ permutation = shuffle_messages(messages, except_hints=True)
+ elif world.text_shuffle == 'complete':
+ permutation = shuffle_messages(messages, except_hints=False)
+
+ repack_messages(rom, messages, permutation)
+
+ # output a text dump, for testing...
+ #with open('keysanity_' + str(world.seed) + '_dump.txt', 'w', encoding='utf-16') as f:
+ # messages = read_messages(rom)
+ # f.write('item_message_strings = {\n')
+ # for m in messages:
+ # f.write("\t0x%04X: \"%s\",\n" % (m.id, m.get_python_string()))
+ # f.write('}\n')
+
+ if world.free_scarecrow:
+ # Played song as adult
+ save_context.write_bits(0x0EE6, 0x10)
+ # Direct scarecrow behavior
+ symbol = rom.sym('FREE_SCARECROW_ENABLED')
+ rom.write_byte(symbol, 0x01)
+
+ # Enable MM-like Bunny Hood behavior (1.5× speed)
+ if world.fast_bunny_hood:
+ symbol = rom.sym('FAST_BUNNY_HOOD_ENABLED')
+ rom.write_byte(symbol, 0x01)
+
+ # actually write the save table to rom
+ for (name, count) in world.starting_items.items():
+ if count == 0:
+ continue
+ save_context.give_item(name, count)
+
+ if world.starting_age == 'adult':
+ # When starting as adult, the pedestal doesn't handle child default equips when going back child the first time, so we have to equip them ourselves
+ save_context.equip_default_items('child')
+ save_context.equip_current_items(world.starting_age)
+ save_context.write_save_table(rom)
+
+ return rom
+
+
+NUM_VANILLA_OBJECTS = 0x192
+def add_to_extended_object_table(rom, object_id, object_file):
+ extended_id = object_id - NUM_VANILLA_OBJECTS - 1
+ extended_object_table = rom.sym('EXTENDED_OBJECT_TABLE')
+ rom.write_int32s(extended_object_table + extended_id * 8, [object_file.start, object_file.end])
+
+
+item_row_struct = struct.Struct('>BBHHBBIIhh') # Match item_row_t in item_table.h
+item_row_fields = [
+ 'base_item_id', 'action_id', 'text_id', 'object_id', 'graphic_id', 'chest_type',
+ 'upgrade_fn', 'effect_fn', 'effect_arg1', 'effect_arg2',
+]
+
+
+def read_rom_item(rom, item_id):
+ addr = rom.sym('item_table') + (item_id * item_row_struct.size)
+ row_bytes = rom.read_bytes(addr, item_row_struct.size)
+ row = item_row_struct.unpack(row_bytes)
+ return { item_row_fields[i]: row[i] for i in range(len(item_row_fields)) }
+
+
+def write_rom_item(rom, item_id, item):
+ addr = rom.sym('item_table') + (item_id * item_row_struct.size)
+ row = [item[f] for f in item_row_fields]
+ row_bytes = item_row_struct.pack(*row)
+ rom.write_bytes(addr, row_bytes)
+
+
+
+def get_override_table(world):
+ return list(filter(lambda val: val != None, map(get_override_entry, world.world.get_filled_locations(world.player))))
+
+
+override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c
+def get_override_table_bytes(override_table):
+ return b''.join(sorted(itertools.starmap(override_struct.pack, override_table)))
+
+
+def get_override_entry(location):
+ scene = location.scene
+ default = location.default
+ player_id = location.item.player
+ if location.item.game != 'Ocarina of Time':
+ # This is an AP sendable. It's guaranteed to not be None.
+ item_id = 0x0C # Ocarina of Time item, otherwise unused
+ looks_like_item_id = 0
+ else:
+ item_id = location.item.index
+ if None in [scene, default, item_id]:
+ return None
+
+ if location.item.looks_like_item is not None:
+ looks_like_item_id = location.item.looks_like_item.index
+ else:
+ looks_like_item_id = 0
+
+ if location.type in ['NPC', 'BossHeart']:
+ type = 0
+ elif location.type == 'Chest':
+ type = 1
+ default &= 0x1F
+ elif location.type == 'Collectable':
+ type = 2
+ elif location.type == 'GS Token':
+ type = 3
+ elif location.type == 'Shop' and location.item.type != 'Shop':
+ type = 0
+ elif location.type == 'GrottoNPC' and location.item.type != 'Shop':
+ type = 4
+ elif location.type in ['Song', 'Cutscene']:
+ type = 5
+ else:
+ return None
+
+ return (scene, type, default, item_id, player_id, looks_like_item_id)
+
+
+chestTypeMap = {
+ # small big boss
+ 0x0000: [0x5000, 0x0000, 0x2000], #Large
+ 0x1000: [0x7000, 0x1000, 0x1000], #Large, Appears, Clear Flag
+ 0x2000: [0x5000, 0x0000, 0x2000], #Boss Key’s Chest
+ 0x3000: [0x8000, 0x3000, 0x3000], #Large, Falling, Switch Flag
+ 0x4000: [0x6000, 0x4000, 0x4000], #Large, Invisible
+ 0x5000: [0x5000, 0x0000, 0x2000], #Small
+ 0x6000: [0x6000, 0x4000, 0x4000], #Small, Invisible
+ 0x7000: [0x7000, 0x1000, 0x1000], #Small, Appears, Clear Flag
+ 0x8000: [0x8000, 0x3000, 0x3000], #Small, Falling, Switch Flag
+ 0x9000: [0x9000, 0x9000, 0x9000], #Large, Appears, Zelda's Lullaby
+ 0xA000: [0xA000, 0xA000, 0xA000], #Large, Appears, Sun's Song Triggered
+ 0xB000: [0xB000, 0xB000, 0xB000], #Large, Appears, Switch Flag
+ 0xC000: [0x5000, 0x0000, 0x2000], #Large
+ 0xD000: [0x5000, 0x0000, 0x2000], #Large
+ 0xE000: [0x5000, 0x0000, 0x2000], #Large
+ 0xF000: [0x5000, 0x0000, 0x2000], #Large
+}
+
+
+def room_get_actors(rom, actor_func, room_data, scene, alternate=None):
+ actors = {}
+ room_start = alternate if alternate else room_data
+ command = 0
+ while command != 0x14: # 0x14 = end header
+ command = rom.read_byte(room_data)
+ if command == 0x01: # actor list
+ actor_count = rom.read_byte(room_data + 1)
+ actor_list = room_start + (rom.read_int32(room_data + 4) & 0x00FFFFFF)
+ for _ in range(0, actor_count):
+ actor_id = rom.read_int16(actor_list)
+ entry = actor_func(rom, actor_id, actor_list, scene)
+ if entry:
+ actors[actor_list] = entry
+ actor_list = actor_list + 16
+ if command == 0x18: # Alternate header list
+ header_list = room_start + (rom.read_int32(room_data + 4) & 0x00FFFFFF)
+ for alt_id in range(0,3):
+ header_data = room_start + (rom.read_int32(header_list) & 0x00FFFFFF)
+ if header_data != 0 and not alternate:
+ actors.update(room_get_actors(rom, actor_func, header_data, scene, room_start))
+ header_list = header_list + 4
+ room_data = room_data + 8
+ return actors
+
+
+def scene_get_actors(rom, actor_func, scene_data, scene, alternate=None, processed_rooms=None):
+ if processed_rooms == None:
+ processed_rooms = []
+ actors = {}
+ scene_start = alternate if alternate else scene_data
+ command = 0
+ while command != 0x14: # 0x14 = end header
+ command = rom.read_byte(scene_data)
+ if command == 0x04: #room list
+ room_count = rom.read_byte(scene_data + 1)
+ room_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
+ for _ in range(0, room_count):
+ room_data = rom.read_int32(room_list);
+
+ if not room_data in processed_rooms:
+ actors.update(room_get_actors(rom, actor_func, room_data, scene))
+ processed_rooms.append(room_data)
+ room_list = room_list + 8
+ if command == 0x0E: #transition actor list
+ actor_count = rom.read_byte(scene_data + 1)
+ actor_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
+ for _ in range(0, actor_count):
+ actor_id = rom.read_int16(actor_list + 4)
+ entry = actor_func(rom, actor_id, actor_list, scene)
+ if entry:
+ actors[actor_list] = entry
+ actor_list = actor_list + 16
+ if command == 0x18: # Alternate header list
+ header_list = scene_start + (rom.read_int32(scene_data + 4) & 0x00FFFFFF)
+ for alt_id in range(0,3):
+ header_data = scene_start + (rom.read_int32(header_list) & 0x00FFFFFF)
+ if header_data != 0 and not alternate:
+ actors.update(scene_get_actors(rom, actor_func, header_data, scene, scene_start, processed_rooms))
+ header_list = header_list + 4
+
+ scene_data = scene_data + 8
+ return actors
+
+
+def get_actor_list(rom, actor_func):
+ actors = {}
+ scene_table = 0x00B71440
+ for scene in range(0x00, 0x65):
+ scene_data = rom.read_int32(scene_table + (scene * 0x14));
+ actors.update(scene_get_actors(rom, actor_func, scene_data, scene))
+ return actors
+
+
+def get_override_itemid(override_table, scene, type, flags):
+ for entry in override_table:
+ if entry[0] == scene and (entry[1] & 0x07) == type and entry[2] == flags:
+ return entry[4]
+ return None
+
+def remove_entrance_blockers(rom):
+ def remove_entrance_blockers_do(rom, actor_id, actor, scene):
+ if actor_id == 0x014E and scene == 97:
+ actor_var = rom.read_int16(actor + 14);
+ if actor_var == 0xFF01:
+ rom.write_int16(actor + 14, 0x0700)
+ get_actor_list(rom, remove_entrance_blockers_do)
+
+def set_cow_id_data(rom, world):
+ def set_cow_id(rom, actor_id, actor, scene):
+ nonlocal last_scene
+ nonlocal cow_count
+ nonlocal last_actor
+
+ if actor_id == 0x01C6: #Cow
+ if scene == last_scene and last_actor != actor:
+ cow_count += 1
+ else:
+ cow_count = 1
+
+ last_scene = scene
+ last_actor = actor
+ if world.dungeon_mq['Jabu Jabus Belly'] and scene == 2: #If its an MQ jabu cow
+ rom.write_int16(actor + 0x8, 1 if cow_count == 17 else 0) #Give all wall cows ID 0, and set cow 11's ID to 1
+ else:
+ rom.write_int16(actor + 0x8, cow_count)
+
+ last_actor = -1
+ last_scene = -1
+ cow_count = 1
+
+ get_actor_list(rom, set_cow_id)
+
+
+def set_grotto_shuffle_data(rom, world):
+ def override_grotto_data(rom, actor_id, actor, scene):
+ if actor_id == 0x009B: #Grotto
+ actor_zrot = rom.read_int16(actor + 12)
+ actor_var = rom.read_int16(actor + 14)
+ grotto_type = (actor_var >> 8) & 0x0F
+ grotto_actor_id = (scene << 8) + (actor_var & 0x00FF)
+
+ rom.write_int16(actor + 12, grotto_entrances_override[grotto_actor_id])
+ rom.write_byte(actor + 14, grotto_type + 0x20)
+
+ # Build the override table based on shuffled grotto entrances
+ grotto_entrances_override = {}
+ for entrance in world.get_shuffled_entrances(type='Grotto'):
+ if entrance.primary:
+ grotto_actor_id = (entrance.data['scene'] << 8) + entrance.data['content']
+ grotto_entrances_override[grotto_actor_id] = entrance.replaces.data['index']
+ else:
+ rom.write_int16(rom.sym('GROTTO_EXIT_LIST') + 2 * entrance.data['grotto_id'], entrance.replaces.data['index'])
+
+ # Override grotto actors data with the new data
+ get_actor_list(rom, override_grotto_data)
+
+
+def set_deku_salesman_data(rom):
+ def set_deku_salesman(rom, actor_id, actor, scene):
+ if actor_id == 0x0195: #Salesman
+ actor_var = rom.read_int16(actor + 14)
+ if actor_var == 6:
+ rom.write_int16(actor + 14, 0x0003)
+
+ get_actor_list(rom, set_deku_salesman)
+
+
+def set_jabu_stone_actors(rom, jabu_actor_type):
+ def set_jabu_stone_actor(rom, actor_id, actor, scene):
+ if scene == 2 and actor_id == 0x008B: # Demo_Effect in Jabu Jabu
+ actor_type = rom.read_byte(actor + 15)
+ if actor_type == 0x15:
+ rom.write_byte(actor + 15, jabu_actor_type)
+
+ get_actor_list(rom, set_jabu_stone_actor)
+
+
+def get_locked_doors(rom, world):
+ def locked_door(rom, actor_id, actor, scene):
+ actor_var = rom.read_int16(actor + 14)
+ actor_type = actor_var >> 6
+ actor_flag = actor_var & 0x003F
+
+ flag_id = (1 << actor_flag)
+ flag_byte = 3 - (actor_flag >> 3)
+ flag_bits = 1 << (actor_flag & 0x07)
+
+ # If locked door, set the door's unlock flag
+ if world.shuffle_smallkeys == 'remove':
+ if actor_id == 0x0009 and actor_type == 0x02:
+ return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
+ if actor_id == 0x002E and actor_type == 0x0B:
+ return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
+
+ # If boss door, set the door's unlock flag
+ if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A):
+ if actor_id == 0x002E and actor_type == 0x05:
+ return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits]
+
+ return get_actor_list(rom, locked_door)
+
+
+def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=False):
+ if init_shop_id:
+ world.current_shop_id = 0x32
+
+ shop_objs = { 0x0148 } # "Sold Out" object
+ for location in locations:
+ if location.item.type == 'Shop':
+ shop_objs.add(location.item.special['object'])
+ rom.write_int16(location.address1, location.item.index)
+ else:
+ if location.item.game != "Ocarina of Time":
+ item_display = location.item
+ item_display.index = 0x0C # Ocarina of Time item
+ item_display.special = {}
+ elif location.item.looks_like_item is not None:
+ item_display = location.item.looks_like_item
+ else:
+ item_display = location.item
+
+ # bottles in shops should look like empty bottles
+ # so that that are different than normal shop refils
+ if 'shop_object' in item_display.special:
+ rom_item = read_rom_item(rom, item_display.special['shop_object'])
+ else:
+ rom_item = read_rom_item(rom, item_display.index)
+
+ shop_objs.add(rom_item['object_id'])
+ shop_id = world.current_shop_id
+ rom.write_int16(location.address1, shop_id)
+ shop_item = shop_items[shop_id]
+
+ shop_item.object = rom_item['object_id']
+ shop_item.model = rom_item['graphic_id'] - 1
+ shop_item.price = location.price
+ shop_item.pieces = 1
+ shop_item.get_item_id = location.default
+ shop_item.func1 = 0x808648CC
+ shop_item.func2 = 0x808636B8
+ shop_item.func3 = 0x00000000
+ shop_item.func4 = 0x80863FB4
+
+ message_id = (shop_id - 0x32) * 2
+ shop_item.description_message = 0x8100 + message_id
+ shop_item.purchase_message = 0x8100 + message_id + 1
+
+ shuffle_messages.shop_item_messages.extend(
+ [shop_item.description_message, shop_item.purchase_message])
+
+ if getattr(item_display, 'dungeonitem', False) and location.item.game == "Ocarina of Time":
+ split_item_name = item_display.name.split('(')
+ split_item_name[1] = '(' + split_item_name[1]
+
+ if location.item.name == 'Ice Trap':
+ split_item_name[0] = create_fake_name(split_item_name[0])
+
+ if len(world.world.worlds) > 1: # OOTWorld.MultiWorld.AutoWorld[]
+ description_text = '\x08\x05\x41%s %d Rupees\x01%s\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (split_item_name[0], location.price, split_item_name[1], world.world.get_player_name(location.item.player))
+ else:
+ description_text = '\x08\x05\x41%s %d Rupees\x01%s\x01\x05\x40Special deal! ONE LEFT!\x01Get it while it lasts!\x09\x0A\x02' % (split_item_name[0], location.price, split_item_name[1])
+ purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1])
+ else:
+ if item_display.game == "Ocarina of Time":
+ shop_item_name = getSimpleHintNoPrefix(item_display)
+ else:
+ shop_item_name = item_display.name
+
+ if location.item.name == 'Ice Trap':
+ shop_item_name = create_fake_name(shop_item_name)
+
+ if len(world.world.worlds) > 1:
+ shop_item_name = ''.join(filter(lambda char: char in character_table, shop_item_name))
+ do_line_break = sum(character_table[char] for char in f"{shop_item_name} {location.price} Rupees") > NORMAL_LINE_WIDTH
+ description_text = '\x08\x05\x41%s%s%d Rupees\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (shop_item_name, '\x01' if do_line_break else ' ', location.price, world.world.get_player_name(location.item.player))
+ else:
+ description_text = '\x08\x05\x41%s %d Rupees\x01\x05\x40Special deal! ONE LEFT!\x01Get it while it lasts!\x09\x0A\x02' % (shop_item_name, location.price)
+ purchase_text = '\x08%s %d Rupees\x09\x01\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (shop_item_name, location.price)
+
+ update_message_by_id(messages, shop_item.description_message, description_text, 0x03)
+ update_message_by_id(messages, shop_item.purchase_message, purchase_text, 0x03)
+
+ world.current_shop_id += 1
+
+ return shop_objs
+
+
+def create_fake_name(item_name):
+ return item_name + "???"
+
+
+def boss_reward_index(world, boss_name):
+ code = world.get_location(boss_name).item.special['item_id']
+ if code >= 0x6C:
+ return code - 0x6C
+ else:
+ return 3 + code - 0x66
+
+
+def configure_dungeon_info(rom, world):
+ mq_enable = (world.mq_dungeons_random or world.mq_dungeons != 0 and world.mq_dungeons != 12)
+ enhance_map_compass = world.enhance_map_compass
+
+ bosses = ['Queen Gohma', 'King Dodongo', 'Barinade', 'Phantom Ganon',
+ 'Volvagia', 'Morpha', 'Twinrova', 'Bongo Bongo']
+ dungeon_rewards = [boss_reward_index(world, boss) for boss in bosses]
+
+ codes = ['Deku Tree', 'Dodongos Cavern', 'Jabu Jabus Belly', 'Forest Temple',
+ 'Fire Temple', 'Water Temple', 'Spirit Temple', 'Shadow Temple',
+ 'Bottom of the Well', 'Ice Cavern', 'Tower (N/A)',
+ 'Gerudo Training Grounds', 'Hideout (N/A)', 'Ganons Castle']
+ dungeon_is_mq = [1 if world.dungeon_mq.get(c) else 0 for c in codes]
+
+ rom.write_int32(rom.sym('cfg_dungeon_info_enable'), 1)
+ rom.write_int32(rom.sym('cfg_dungeon_info_mq_enable'), int(mq_enable))
+ rom.write_int32(rom.sym('cfg_dungeon_info_mq_need_map'), int(enhance_map_compass))
+ rom.write_int32(rom.sym('cfg_dungeon_info_reward_enable'), int(world.misc_hints or enhance_map_compass))
+ rom.write_int32(rom.sym('cfg_dungeon_info_reward_need_compass'), int(enhance_map_compass))
+ rom.write_int32(rom.sym('cfg_dungeon_info_reward_need_altar'), int(not enhance_map_compass))
+ rom.write_bytes(rom.sym('cfg_dungeon_rewards'), dungeon_rewards)
+ rom.write_bytes(rom.sym('cfg_dungeon_is_mq'), dungeon_is_mq)
diff --git a/worlds/oot/Regions.py b/worlds/oot/Regions.py
new file mode 100644
index 00000000..0cfda386
--- /dev/null
+++ b/worlds/oot/Regions.py
@@ -0,0 +1,61 @@
+from enum import unique, Enum
+
+from BaseClasses import Region
+
+
+# copied from OoT-Randomizer/Region.py
+@unique
+class RegionType(Enum):
+
+ Overworld = 1
+ Interior = 2
+ Dungeon = 3
+ Grotto = 4
+
+
+ @property
+ def is_indoors(self):
+ """Shorthand for checking if Interior or Dungeon"""
+ return self in (RegionType.Interior, RegionType.Dungeon, RegionType.Grotto)
+
+# Pretends to be an enum, but when the values are raw ints, it's much faster
+class TimeOfDay(object):
+ NONE = 0
+ DAY = 1
+ DAMPE = 2
+ ALL = DAY | DAMPE
+
+
+
+
+class OOTRegion(Region):
+ game: str = "Ocarina of Time"
+
+ def __init__(self, name: str, type, hint, player: int):
+ super(OOTRegion, self).__init__(name, type, hint, player)
+ self.price = None
+ self.time_passes = False
+ self.provides_time = TimeOfDay.NONE
+ self.scene = None
+ self.dungeon = None
+
+ def get_scene(self):
+ if self.scene:
+ return self.scene
+ elif self.dungeon:
+ return self.dungeon.name
+ else:
+ return None
+
+ def can_reach(self, state):
+ if state.stale[self.player]:
+ stored_age = state.age[self.player]
+ state._oot_update_age_reachable_regions(self.player)
+ state.age[self.player] = stored_age
+ if state.age[self.player] == 'child':
+ return self in state.child_reachable_regions[self.player]
+ elif state.age[self.player] == 'adult':
+ return self in state.adult_reachable_regions[self.player]
+ else: # we don't care about age
+ return self in state.child_reachable_regions[self.player] or self in state.adult_reachable_regions[self.player]
+
diff --git a/worlds/oot/Rom.py b/worlds/oot/Rom.py
new file mode 100644
index 00000000..64a53b61
--- /dev/null
+++ b/worlds/oot/Rom.py
@@ -0,0 +1,321 @@
+import io
+import itertools
+import json
+import logging
+import os
+import platform
+import struct
+import subprocess
+import random
+import copy
+from Utils import local_path, is_frozen
+from .Utils import subprocess_args, data_path, get_version_bytes, __version__
+from .ntype import BigStream, uint32
+from .crc import calculate_crc
+
+DMADATA_START = 0x7430
+
+class Rom(BigStream):
+
+ def __init__(self, file=None):
+ super().__init__([])
+
+ self.original = None
+ self.changed_address = {}
+ self.changed_dma = {}
+ self.force_patch = []
+
+ if file is None:
+ return
+
+ decomp_file = 'ZOOTDEC.z64'
+
+ os.chdir(local_path())
+
+ with open(data_path('generated/symbols.json'), 'r') as stream:
+ symbols = json.load(stream)
+ self.symbols = { name: int(addr, 16) for name, addr in symbols.items() }
+
+ # If decompressed file already exists, read from it
+ if os.path.exists(decomp_file):
+ file = decomp_file
+
+ if file == '':
+ # if not specified, try to read from the previously decompressed rom
+ file = decomp_file
+ try:
+ self.read_rom(file)
+ except FileNotFoundError:
+ # could not find the decompressed rom either
+ raise FileNotFoundError('Must specify path to base ROM')
+ else:
+ self.read_rom(file)
+
+ # decompress rom, or check if it's already decompressed
+ self.decompress_rom_file(file, decomp_file)
+
+ # Add file to maximum size
+ self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
+ self.original = self.copy()
+
+ # Add version number to header.
+ self.write_bytes(0x35, get_version_bytes(__version__))
+ self.force_patch.extend([0x35, 0x36, 0x37])
+
+
+ def copy(self):
+ new_rom = Rom()
+ new_rom.buffer = copy.copy(self.buffer)
+ new_rom.changed_address = copy.copy(self.changed_address)
+ new_rom.changed_dma = copy.copy(self.changed_dma)
+ new_rom.force_patch = copy.copy(self.force_patch)
+ return new_rom
+
+
+ def decompress_rom_file(self, file, decomp_file):
+ validCRC = [
+ [0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
+ [0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed
+ [0x93, 0x52, 0x2E, 0x7B, 0xE5, 0x06, 0xD4, 0x27], # Decompressed
+ ]
+
+ # Validate ROM file
+ file_name = os.path.splitext(file)
+ romCRC = list(self.buffer[0x10:0x18])
+ if romCRC not in validCRC:
+ # Bad CRC validation
+ raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
+ elif len(self.buffer) < 0x2000000 or len(self.buffer) > (0x4000000) or file_name[1].lower() not in ['.z64', '.n64']:
+ # ROM is too big, or too small, or not a bad type
+ raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file)
+ elif len(self.buffer) == 0x2000000:
+ # If Input ROM is compressed, then Decompress it
+ subcall = []
+
+ sub_dir = data_path("Decompress")
+
+ if platform.system() == 'Windows':
+ if 8 * struct.calcsize("P") == 64:
+ subcall = [sub_dir + "\\Decompress.exe", file, decomp_file]
+ else:
+ subcall = [sub_dir + "\\Decompress32.exe", file, decomp_file]
+ elif platform.system() == 'Linux':
+ if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
+ subcall = [sub_dir + "/Decompress_ARM64", file, decomp_file]
+ else:
+ subcall = [sub_dir + "/Decompress", file, decomp_file]
+ elif platform.system() == 'Darwin':
+ subcall = [sub_dir + "/Decompress.out", file, decomp_file]
+ else:
+ raise RuntimeError('Unsupported operating system for decompression. Please supply an already decompressed ROM.')
+
+ if not os.path.exists(subcall[0]):
+ raise RuntimeError(f'Decompressor does not exist! Please place it at {subcall[0]}.')
+ subprocess.call(subcall, **subprocess_args())
+ self.read_rom(decomp_file)
+ else:
+ # ROM file is a valid and already uncompressed
+ pass
+
+
+ def write_byte(self, address, value):
+ super().write_byte(address, value)
+ self.changed_address[self.last_address-1] = value
+
+
+ def write_bytes(self, address, values):
+ super().write_bytes(address, values)
+ self.changed_address.update(zip(range(address, address+len(values)), values))
+
+
+ def restore(self):
+ self.buffer = copy.copy(self.original.buffer)
+ self.changed_address = {}
+ self.changed_dma = {}
+ self.force_patch = []
+ self.last_address = None
+ self.write_bytes(0x35, get_version_bytes(__version__))
+ self.force_patch.extend([0x35, 0x36, 0x37])
+
+
+ def sym(self, symbol_name):
+ return self.symbols.get(symbol_name)
+
+
+ def write_to_file(self, file):
+ self.verify_dmadata()
+ self.update_header()
+ with open(file, 'wb') as outfile:
+ outfile.write(self.buffer)
+
+
+ def update_header(self):
+ crc = calculate_crc(self)
+ self.write_bytes(0x10, crc)
+
+
+ def read_rom(self, file):
+ # "Reads rom into bytearray"
+ try:
+ with open(file, 'rb') as stream:
+ self.buffer = bytearray(stream.read())
+ except FileNotFoundError as ex:
+ raise FileNotFoundError('Invalid path to Base ROM: "' + file + '"')
+
+
+ # dmadata/file management helper functions
+
+ def _get_dmadata_record(self, cur):
+ start = self.read_int32(cur)
+ end = self.read_int32(cur+0x04)
+ size = end-start
+ return start, end, size
+
+
+ def get_dmadata_record_by_key(self, key):
+ cur = DMADATA_START
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+ while True:
+ if dma_start == 0 and dma_end == 0:
+ return None
+ if dma_start == key:
+ return dma_start, dma_end, dma_size
+ cur += 0x10
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+
+
+ def verify_dmadata(self):
+ cur = DMADATA_START
+ overlapping_records = []
+ dma_data = []
+
+ while True:
+ this_start, this_end, this_size = self._get_dmadata_record(cur)
+
+ if this_start == 0 and this_end == 0:
+ break
+
+ dma_data.append((this_start, this_end, this_size))
+ cur += 0x10
+
+ dma_data.sort(key=lambda v: v[0])
+
+ for i in range(0, len(dma_data) - 1):
+ this_start, this_end, this_size = dma_data[i]
+ next_start, next_end, next_size = dma_data[i + 1]
+
+ if this_end > next_start:
+ overlapping_records.append(
+ '0x%08X - 0x%08X (Size: 0x%04X)\n0x%08X - 0x%08X (Size: 0x%04X)' % \
+ (this_start, this_end, this_size, next_start, next_end, next_size)
+ )
+
+ if len(overlapping_records) > 0:
+ raise Exception("Overlapping DMA Data Records!\n%s" % \
+ '\n-------------------------------------\n'.join(overlapping_records))
+
+
+ # update dmadata record with start vrom address "key"
+ # if key is not found, then attempt to add a new dmadata entry
+ def update_dmadata_record(self, key, start, end, from_file=None):
+ cur, dma_data_end = self.get_dma_table_range()
+ dma_index = 0
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+ while dma_start != key:
+ if dma_start == 0 and dma_end == 0:
+ break
+
+ cur += 0x10
+ dma_index += 1
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+
+ if cur >= (dma_data_end - 0x10):
+ raise Exception('dmadata update failed: key {0:x} not found in dmadata and dma table is full.'.format(key))
+ else:
+ self.write_int32s(cur, [start, end, start, 0])
+ if from_file == None:
+ if key == None:
+ from_file = -1
+ else:
+ from_file = key
+ self.changed_dma[dma_index] = (from_file, start, end - start)
+
+
+ def get_dma_table_range(self):
+ cur = DMADATA_START
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+ while True:
+ if dma_start == 0 and dma_end == 0:
+ raise Exception('Bad DMA Table: DMA Table entry missing.')
+
+ if dma_start == DMADATA_START:
+ return (DMADATA_START, dma_end)
+
+ cur += 0x10
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+
+
+ # This will scan for any changes that have been made to the DMA table
+ # This assumes any changes here are new files, so this should only be called
+ # after patching in the new files, but before vanilla files are repointed
+ def scan_dmadata_update(self):
+ cur = DMADATA_START
+ dma_data_end = None
+ dma_index = 0
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+ old_dma_start, old_dma_end, old_dma_size = self.original._get_dmadata_record(cur)
+
+ while True:
+ if (dma_start == 0 and dma_end == 0) and \
+ (old_dma_start == 0 and old_dma_end == 0):
+ break
+
+ # If the entries do not match, the flag the changed entry
+ if not (dma_start == old_dma_start and dma_end == old_dma_end):
+ self.changed_dma[dma_index] = (-1, dma_start, dma_end - dma_start)
+
+ cur += 0x10
+ dma_index += 1
+ dma_start, dma_end, dma_size = self._get_dmadata_record(cur)
+ old_dma_start, old_dma_end, old_dma_size = self.original._get_dmadata_record(cur)
+
+
+ # gets the last used byte of rom defined in the DMA table
+ def free_space(self):
+ cur = DMADATA_START
+ max_end = 0
+
+ while True:
+ this_start, this_end, this_size = self._get_dmadata_record(cur)
+
+ if this_start == 0 and this_end == 0:
+ break
+
+ max_end = max(max_end, this_end)
+ cur += 0x10
+ max_end = ((max_end + 0x0F) >> 4) << 4
+ return max_end
+
+def compress_rom_file(input_file, output_file):
+ subcall = []
+
+ compressor_path = data_path("Compress")
+
+ if platform.system() == 'Windows':
+ if 8 * struct.calcsize("P") == 64:
+ compressor_path += "\\Compress.exe"
+ else:
+ compressor_path += "\\Compress32.exe"
+ elif platform.system() == 'Linux':
+ if platform.uname()[4] == 'aarch64' or platform.uname()[4] == 'arm64':
+ compressor_path += "/Compress_ARM64"
+ else:
+ compressor_path += "/Compress"
+ elif platform.system() == 'Darwin':
+ compressor_path += "/Compress.out"
+ else:
+ raise RuntimeError('Unsupported operating system for compression.')
+
+ if not os.path.exists(compressor_path):
+ raise RuntimeError(f'Compressor does not exist! Please place it at {compressor_path}.')
+ process = subprocess.call([compressor_path, input_file, output_file], **subprocess_args(include_stdout=False))
diff --git a/worlds/oot/RuleParser.py b/worlds/oot/RuleParser.py
new file mode 100644
index 00000000..5223d3c2
--- /dev/null
+++ b/worlds/oot/RuleParser.py
@@ -0,0 +1,507 @@
+import ast
+from collections import defaultdict
+from inspect import signature, _ParameterKind
+import logging
+import re
+
+from .Items import item_table
+from .Location import OOTLocation
+from .Regions import TimeOfDay, OOTRegion
+from BaseClasses import CollectionState as State
+from .Utils import data_path, read_json
+
+from worlds.generic.Rules import set_rule
+
+
+escaped_items = {}
+for item in item_table:
+ escaped_items[re.sub(r'[\'()[\]]', '', item.replace(' ', '_'))] = item
+
+event_name = re.compile(r'\w+')
+# All generated lambdas must accept these keyword args!
+# For evaluation at a certain age (required as all rules are evaluated at a specific age)
+# or at a certain spot (can be omitted in many cases)
+# or at a specific time of day (often unused)
+kwarg_defaults = {
+ # 'age': None,
+ # 'spot': None,
+ # 'tod': TimeOfDay.NONE,
+}
+
+allowed_globals = {'TimeOfDay': TimeOfDay}
+
+rule_aliases = {}
+nonaliases = set()
+
+def load_aliases():
+ j = read_json(data_path('LogicHelpers.json'))
+ for s, repl in j.items():
+ if '(' in s:
+ rule, args = s[:-1].split('(', 1)
+ args = [re.compile(r'\b%s\b' % a.strip()) for a in args.split(',')]
+ else:
+ rule = s
+ args = ()
+ rule_aliases[rule] = (args, repl)
+ nonaliases = escaped_items.keys() - rule_aliases.keys()
+
+
+def isliteral(expr):
+ return isinstance(expr, (ast.Num, ast.Str, ast.Bytes, ast.NameConstant))
+
+
+class Rule_AST_Transformer(ast.NodeTransformer):
+
+ def __init__(self, world, player):
+ self.world = world
+ self.player = player
+ self.events = set()
+ # map Region -> rule ast string -> item name
+ self.replaced_rules = defaultdict(dict)
+ # delayed rules need to keep: region name, ast node, event name
+ self.delayed_rules = []
+ # lazy load aliases
+ if not rule_aliases:
+ load_aliases()
+ # final rule cache
+ self.rule_cache = {}
+ self.kwarg_defaults = kwarg_defaults.copy() # otherwise this gets contaminated between players
+ self.kwarg_defaults['player'] = self.player
+
+
+ def visit_Name(self, node):
+ if node.id in dir(self):
+ return getattr(self, node.id)(node)
+ elif node.id in rule_aliases:
+ args, repl = rule_aliases[node.id]
+ if args:
+ raise Exception('Parse Error: expected %d args for %s, not 0' % (len(args), node.id),
+ self.current_spot.name, ast.dump(node, False))
+ return self.visit(ast.parse(repl, mode='eval').body)
+ elif node.id in escaped_items:
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has',
+ ctx=ast.Load()),
+ args=[ast.Str(escaped_items[node.id]), ast.Constant(self.player)],
+ keywords=[])
+ elif node.id in self.world.__dict__:
+ # Settings are constant
+ return ast.parse('%r' % self.world.__dict__[node.id], mode='eval').body
+ elif node.id in State.__dict__:
+ return self.make_call(node, node.id, [], [])
+ elif node.id in self.kwarg_defaults or node.id in allowed_globals:
+ return node
+ elif event_name.match(node.id):
+ self.events.add(node.id.replace('_', ' '))
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has',
+ ctx=ast.Load()),
+ args=[ast.Str(node.id.replace('_', ' ')), ast.Constant(self.player)],
+ keywords=[])
+ else:
+ raise Exception('Parse Error: invalid node name %s' % node.id, self.current_spot.name, ast.dump(node, False))
+
+ def visit_Str(self, node):
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has',
+ ctx=ast.Load()),
+ args=[ast.Str(node.s), ast.Constant(self.player)],
+ keywords=[])
+
+ # python 3.8 compatibility: ast walking now uses visit_Constant for Constant subclasses
+ # this includes Num, Str, NameConstant, Bytes, and Ellipsis. We only handle Str.
+ def visit_Constant(self, node):
+ if isinstance(node, ast.Str):
+ return self.visit_Str(node)
+ return node
+
+
+ def visit_Tuple(self, node):
+ if len(node.elts) != 2:
+ raise Exception('Parse Error: Tuple must have 2 values', self.current_spot.name, ast.dump(node, False))
+
+ item, count = node.elts
+
+ if not isinstance(item, (ast.Name, ast.Str)):
+ raise Exception('Parse Error: first value must be an item. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
+ iname = item.id if isinstance(item, ast.Name) else item.s
+
+ if not (isinstance(count, ast.Name) or isinstance(count, ast.Num)):
+ raise Exception('Parse Error: second value must be a number. Got %s' % item.__class__.__name__, self.current_spot.name, ast.dump(node, False))
+
+ if isinstance(count, ast.Name):
+ # Must be a settings constant
+ count = ast.parse('%r' % self.world.__dict__[count.id], mode='eval').body
+
+ if iname in escaped_items:
+ iname = escaped_items[iname]
+
+ if iname not in item_table:
+ self.events.add(iname)
+
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has',
+ ctx=ast.Load()),
+ args=[ast.Str(iname), ast.Constant(self.player), count],
+ keywords=[])
+
+
+ def visit_Call(self, node):
+ if not isinstance(node.func, ast.Name):
+ return node
+
+ if node.func.id in dir(self):
+ return getattr(self, node.func.id)(node)
+ elif node.func.id in rule_aliases:
+ args, repl = rule_aliases[node.func.id]
+ if len(args) != len(node.args):
+ raise Exception('Parse Error: expected %d args for %s, not %d' % (len(args), node.func.id, len(node.args)),
+ self.current_spot.name, ast.dump(node, False))
+ # straightforward string manip
+ for arg_re, arg_val in zip(args, node.args):
+ if isinstance(arg_val, ast.Name):
+ val = arg_val.id
+ elif isinstance(arg_val, ast.Constant):
+ val = repr(arg_val.value)
+ elif isinstance(arg_val, ast.Str):
+ val = repr(arg_val.s)
+ else:
+ raise Exception('Parse Error: invalid argument %s' % ast.dump(arg_val, False),
+ self.current_spot.name, ast.dump(node, False))
+ repl = arg_re.sub(val, repl)
+ return self.visit(ast.parse(repl, mode='eval').body)
+
+ new_args = []
+ for child in node.args:
+ if isinstance(child, ast.Name):
+ if child.id in self.world.__dict__:
+ # child = ast.Attribute(
+ # value=ast.Attribute(
+ # value=ast.Name(id='state', ctx=ast.Load()),
+ # attr='world',
+ # ctx=ast.Load()),
+ # attr=child.id,
+ # ctx=ast.Load())
+ child = ast.Constant(getattr(self.world, child.id))
+ elif child.id in rule_aliases:
+ child = self.visit(child)
+ elif child.id in escaped_items:
+ child = ast.Str(escaped_items[child.id])
+ else:
+ child = ast.Str(child.id.replace('_', ' '))
+ elif not isinstance(child, ast.Str):
+ child = self.visit(child)
+ new_args.append(child)
+
+ return self.make_call(node, node.func.id, new_args, node.keywords)
+
+
+ def visit_Subscript(self, node):
+ if isinstance(node.value, ast.Name):
+ s = node.slice if isinstance(node.slice, ast.Name) else node.slice.value
+ return ast.Subscript(
+ value=ast.Attribute(
+ # value=ast.Attribute(
+ # value=ast.Name(id='state', ctx=ast.Load()),
+ # attr='world',
+ # ctx=ast.Load()),
+ value=ast.Subscript(
+ value=ast.Attribute(
+ value=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='world',
+ ctx=ast.Load()),
+ attr='worlds',
+ ctx=ast.Load()),
+ slice=ast.Index(value=ast.Constant(self.player)),
+ ctx=ast.Load()),
+ attr=node.value.id,
+ ctx=ast.Load()),
+ slice=ast.Index(value=ast.Str(s.id.replace('_', ' '))),
+ ctx=node.ctx)
+ else:
+ return node
+
+
+ def visit_Compare(self, node):
+ def escape_or_string(n):
+ if isinstance(n, ast.Name) and n.id in escaped_items:
+ return ast.Str(escaped_items[n.id])
+ elif not isinstance(n, ast.Str):
+ return self.visit(n)
+ return n
+
+ # Fast check for json can_use
+ if (len(node.ops) == 1 and isinstance(node.ops[0], ast.Eq)
+ and isinstance(node.left, ast.Name) and isinstance(node.comparators[0], ast.Name)
+ and node.left.id not in self.world.__dict__ and node.comparators[0].id not in self.world.__dict__):
+ return ast.NameConstant(node.left.id == node.comparators[0].id)
+
+ node.left = escape_or_string(node.left)
+ node.comparators = list(map(escape_or_string, node.comparators))
+ node.ops = list(map(self.visit, node.ops))
+
+ # if all the children are literals now, we can evaluate
+ if isliteral(node.left) and all(map(isliteral, node.comparators)):
+ # either we turn the ops into operator functions to apply (lots of work),
+ # or we compile, eval, and reparse the result
+ try:
+ res = eval(compile(ast.fix_missing_locations(ast.Expression(node)), '', 'eval'))
+ except TypeError as e:
+ raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(node, False))
+ return self.visit(ast.parse('%r' % res, mode='eval').body)
+ return node
+
+
+ def visit_UnaryOp(self, node):
+ # visit the children first
+ self.generic_visit(node)
+ # if all the children are literals now, we can evaluate
+ if isliteral(node.operand):
+ res = eval(compile(ast.Expression(node), '', 'eval'))
+ return ast.parse('%r' % res, mode='eval').body
+ return node
+
+
+ def visit_BinOp(self, node):
+ # visit the children first
+ self.generic_visit(node)
+ # if all the children are literals now, we can evaluate
+ if isliteral(node.left) and isliteral(node.right):
+ res = eval(compile(ast.Expression(node), '', 'eval'))
+ return ast.parse('%r' % res, mode='eval').body
+ return node
+
+
+ def visit_BoolOp(self, node):
+ # Everything else must be visited, then can be removed/reduced to.
+ early_return = isinstance(node.op, ast.Or)
+ groupable = 'has_any' if early_return else 'has_all'
+ items = set()
+ new_values = []
+ # if any elt is True(And)/False(Or), we can omit it
+ # if any is False(And)/True(Or), the whole node can be replaced with it
+ for elt in list(node.values):
+ if isinstance(elt, ast.Str):
+ items.add(elt.s)
+ elif isinstance(elt, ast.Name) and elt.id in nonaliases:
+ items.add(escaped_items[elt.id])
+ else:
+ # It's possible this returns a single item check,
+ # but it's already wrapped in a Call.
+ elt = self.visit(elt)
+ if isinstance(elt, ast.NameConstant):
+ if elt.value == early_return:
+ return elt
+ # else omit it
+ elif (isinstance(elt, ast.Call) and isinstance(elt.func, ast.Attribute)
+ and elt.func.attr in ('has', groupable) and len(elt.args) == 1):
+ args = elt.args[0]
+ if isinstance(args, ast.Str):
+ items.add(args.s)
+ else:
+ items.update(it.s for it in args.elts)
+ elif isinstance(elt, ast.BoolOp) and node.op.__class__ == elt.op.__class__:
+ new_values.extend(elt.values)
+ else:
+ new_values.append(elt)
+
+ # package up the remaining items and values
+ if not items and not new_values:
+ # all values were True(And)/False(Or)
+ return ast.NameConstant(not early_return)
+
+ if items:
+ node.values = [ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has_any' if early_return else 'has_all',
+ ctx=ast.Load()),
+ args=[ast.Tuple(elts=[ast.Str(i) for i in items], ctx=ast.Load()), ast.Constant(self.player)],
+ keywords=[])] + new_values
+ else:
+ node.values = new_values
+ if len(node.values) == 1:
+ return node.values[0]
+ return node
+
+
+ # Generates an ast.Call invoking the given State function 'name',
+ # providing given args and keywords, and adding in additional
+ # keyword args from kwarg_defaults (age, etc.)
+ def make_call(self, node, name, args, keywords):
+ if not hasattr(State, name):
+ raise Exception('Parse Error: No such function State.%s' % name, self.current_spot.name, ast.dump(node, False))
+
+ for (k, v) in self.kwarg_defaults.items():
+ keywords.append(ast.keyword(arg=f'{k}', value=ast.Constant(v)))
+
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr=name,
+ ctx=ast.Load()),
+ args=args,
+ keywords=keywords)
+
+
+ def replace_subrule(self, target, node):
+ rule = ast.dump(node, False)
+ if rule in self.replaced_rules[target]:
+ return self.replaced_rules[target][rule]
+
+ subrule_name = target + ' Subrule %d' % (1 + len(self.replaced_rules[target]))
+ # Save the info to be made into a rule later
+ self.delayed_rules.append((target, node, subrule_name))
+ # Replace the call with a reference to that item
+ item_rule = ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='state', ctx=ast.Load()),
+ attr='has',
+ ctx=ast.Load()),
+ args=[ast.Str(subrule_name), ast.Constant(self.player)],
+ keywords=[])
+ # Cache the subrule for any others in this region
+ # (and reserve the item name in the process)
+ self.replaced_rules[target][rule] = item_rule
+ return item_rule
+
+
+ # Requires the target regions have been defined in the world.
+ def create_delayed_rules(self):
+ for region_name, node, subrule_name in self.delayed_rules:
+ region = self.world.world.get_region(region_name, self.player)
+ event = OOTLocation(self.player, subrule_name, type='Event', parent=region, internal=True)
+ event.show_in_spoiler = False
+
+ self.current_spot = event
+ # This could, in theory, create further subrules.
+ access_rule = self.make_access_rule(self.visit(node))
+ if access_rule is self.rule_cache.get('NameConstant(False)'):
+ event.access_rule = None
+ event.never = True
+ logging.getLogger('').debug('Dropping unreachable delayed event: %s', event.name)
+ else:
+ if access_rule is self.rule_cache.get('NameConstant(True)'):
+ event.always = True
+ set_rule(event, access_rule)
+ region.locations.append(event)
+
+ self.world.make_event_item(subrule_name, event)
+ # Safeguard in case this is called multiple times per world
+ self.delayed_rules.clear()
+
+
+ def make_access_rule(self, body):
+ rule_str = ast.dump(body, False)
+ if rule_str not in self.rule_cache:
+ # requires consistent iteration on dicts
+ kwargs = [ast.arg(arg=k) for k in self.kwarg_defaults.keys()]
+ kwd = list(map(ast.Constant, self.kwarg_defaults.values()))
+ try:
+ self.rule_cache[rule_str] = eval(compile(
+ ast.fix_missing_locations(
+ ast.Expression(ast.Lambda(
+ args=ast.arguments(
+ posonlyargs=[],
+ args=[ast.arg(arg='state')],
+ defaults=[],
+ kwonlyargs=kwargs,
+ kw_defaults=kwd),
+ body=body))),
+ '', 'eval'),
+ # globals/locals. if undefined, everything in the namespace *now* would be allowed
+ allowed_globals)
+ except TypeError as e:
+ raise Exception('Parse Error: %s' % e, self.current_spot.name, ast.dump(body, False))
+ return self.rule_cache[rule_str]
+
+
+ ## Handlers for specific internal functions used in the json logic.
+
+ # at(region_name, rule)
+ # Creates an internal event at the remote region and depends on it.
+ def at(self, node):
+ # Cache this under the target (region) name
+ if len(node.args) < 2 or not isinstance(node.args[0], ast.Str):
+ raise Exception('Parse Error: invalid at() arguments', self.current_spot.name, ast.dump(node, False))
+ return self.replace_subrule(node.args[0].s, node.args[1])
+
+
+ # here(rule)
+ # Creates an internal event in the same region and depends on it.
+ def here(self, node):
+ if not node.args:
+ raise Exception('Parse Error: missing here() argument', self.current_spot.name, ast.dump(node, False))
+ return self.replace_subrule(
+ self.current_spot.parent_region.name,
+ node.args[0])
+
+ ## Handlers for compile-time optimizations (former State functions)
+
+ def at_day(self, node):
+ if self.world.ensure_tod_access:
+ # tod has DAY or (tod == NONE and (ss or find a path from a provider))
+ # parsing is better than constructing this expression by hand
+ return ast.parse("(tod & TimeOfDay.DAY) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAY))", mode='eval').body
+ return ast.NameConstant(True)
+
+ def at_dampe_time(self, node):
+ if self.world.ensure_tod_access:
+ # tod has DAMPE or (tod == NONE and (find a path from a provider))
+ # parsing is better than constructing this expression by hand
+ return ast.parse("(tod & TimeOfDay.DAMPE) if tod else state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE)", mode='eval').body
+ return ast.NameConstant(True)
+
+ def at_night(self, node):
+ if self.current_spot.type == 'GS Token' and self.world.logic_no_night_tokens_without_suns_song:
+ # Using visit here to resolve 'can_play' rule
+ return self.visit(ast.parse('can_play(Suns_Song)', mode='eval').body)
+ if self.world.ensure_tod_access:
+ # tod has DAMPE or (tod == NONE and (ss or find a path from a provider))
+ # parsing is better than constructing this expression by hand
+ return ast.parse("(tod & TimeOfDay.DAMPE) if tod else (state.has_all(('Ocarina', 'Suns Song')) or state.search.can_reach(spot.parent_region, age=age, tod=TimeOfDay.DAMPE))", mode='eval').body
+ return ast.NameConstant(True)
+
+
+ # Parse entry point
+ # If spot is None, here() rules won't work.
+ def parse_rule(self, rule_string, spot=None):
+ self.current_spot = spot
+ return self.make_access_rule(self.visit(ast.parse(rule_string, mode='eval').body))
+
+ def parse_spot_rule(self, spot):
+ rule = spot.rule_string.split('#', 1)[0].strip()
+
+ access_rule = self.parse_rule(rule, spot)
+ set_rule(spot, access_rule)
+ if access_rule is self.rule_cache.get('NameConstant(False)'):
+ spot.never = True
+ elif access_rule is self.rule_cache.get('NameConstant(True)'):
+ spot.always = True
+
+ # Hijacking functions
+ def current_spot_child_access(self, node):
+ r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
+ return ast.parse(f"state._oot_reach_as_age('{r.name}', 'child', {self.player})", mode='eval').body
+
+ def current_spot_adult_access(self, node):
+ r = self.current_spot if type(self.current_spot) == OOTRegion else self.current_spot.parent_region
+ return ast.parse(f"state._oot_reach_as_age('{r.name}', 'adult', {self.player})", mode='eval').body
+
+ def current_spot_starting_age_access(self, node):
+ return self.current_spot_child_access(node) if self.world.starting_age == 'child' else self.current_spot_adult_access(node)
+
+ def has_bottle(self, node):
+ return ast.parse(f"state._oot_has_bottle({self.player})", mode='eval').body
+
+ def can_live_dmg(self, node):
+ return ast.parse(f"state._oot_can_live_dmg({self.player}, {node.args[0].value})", mode='eval').body
diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py
new file mode 100644
index 00000000..75c67d08
--- /dev/null
+++ b/worlds/oot/Rules.py
@@ -0,0 +1,203 @@
+from collections import deque
+import logging
+
+from .SaveContext import SaveContext
+
+from BaseClasses import CollectionState
+from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item, item_in_locations
+from ..AutoWorld import LogicMixin
+
+
+class OOTLogic(LogicMixin):
+
+ def _oot_has_stones(self, count, player):
+ return self.has_group("stones", player, count)
+
+ def _oot_has_medallions(self, count, player):
+ return self.has_group("medallions", player, count)
+
+ def _oot_has_dungeon_rewards(self, count, player):
+ return self.has_group("rewards", player, count)
+
+ def _oot_has_bottle(self, player):
+ return self.has_group("bottles", player)
+
+ # Used for fall damage and other situations where damage is unavoidable
+ def _oot_can_live_dmg(self, player, hearts):
+ mult = self.world.worlds[player].damage_multiplier
+ if hearts*4 >= 3:
+ return mult != 'ohko' and mult != 'quadruple'
+ elif hearts*4 < 3:
+ return mult != 'ohko'
+ else:
+ return True
+
+ # This function operates by assuming different behavior based on the "level of recursion", handled manually.
+ # If it's called while self.age[player] is None, then it will set the age variable and then attempt to reach the region.
+ # If self.age[player] is not None, then it will compare it to the 'age' parameter, and return True iff they are equal.
+ # This lets us fake the OOT accessibility check that cares about age. Unfortunately it's still tied to the ground region.
+ def _oot_reach_as_age(self, regionname, age, player):
+ if self.age[player] is None:
+ self.age[player] = age
+ can_reach = self.world.get_region(regionname, player).can_reach(self)
+ self.age[player] = None
+ return can_reach
+ return self.age[player] == age
+
+ # Store the age before calling this!
+ def _oot_update_age_reachable_regions(self, player):
+ self.stale[player] = False
+ for age in ['child', 'adult']:
+ self.age[player] = age
+ rrp = getattr(self, f'{age}_reachable_regions')[player]
+ bc = getattr(self, f'{age}_blocked_connections')[player]
+ queue = deque(getattr(self, f'{age}_blocked_connections')[player])
+ start = self.world.get_region('Menu', player)
+
+ # init on first call - this can't be done on construction since the regions don't exist yet
+ if not start in rrp:
+ rrp.add(start)
+ bc.update(start.exits)
+ queue.extend(start.exits)
+
+ # run BFS on all connections, and keep track of those blocked by missing items
+ while queue:
+ connection = queue.popleft()
+ new_region = connection.connected_region
+ if new_region in rrp:
+ bc.remove(connection)
+ elif connection.can_reach(self):
+ rrp.add(new_region)
+ bc.remove(connection)
+ bc.update(new_region.exits)
+ queue.extend(new_region.exits)
+ self.path[new_region] = (new_region.name, self.path.get(connection, None))
+
+
+def set_rules(ootworld):
+ logger = logging.getLogger('')
+
+ world = ootworld.world
+ player = ootworld.player
+
+ if ootworld.logic_rules != 'no_logic':
+ if ootworld.triforce_hunt:
+ world.completion_condition[player] = lambda state: state.has('Triforce Piece', player, ootworld.triforce_goal)
+ else:
+ world.completion_condition[player] = lambda state: state.has('Triforce', player)
+
+ # ganon can only carry triforce
+ world.get_location('Ganon', player).item_rule = lambda item: item.name == 'Triforce'
+
+ # is_child = ootworld.parser.parse_rule('is_child')
+ # guarantee_hint = ootworld.parser.parse_rule('guarantee_hint')
+
+ for location in ootworld.get_locations():
+ if ootworld.shuffle_song_items == 'song':
+ if location.type == 'Song':
+ # must be a song, or there are songs in starting items; then it can be anything
+ add_item_rule(location, lambda item:
+ (ootworld.starting_songs and item.type != 'Song')
+ or (item.type == 'Song' and item.player == location.player))
+ else:
+ add_item_rule(location, lambda item: item.type != 'Song')
+
+ if location.type == 'Shop':
+ if location.name in ootworld.shop_prices:
+ add_item_rule(location, lambda item: item.type != 'Shop')
+ location.price = ootworld.shop_prices[location.name]
+ add_rule(location, create_shop_rule(location, ootworld.parser))
+ else:
+ add_item_rule(location, lambda item: item.type == 'Shop' and item.player == location.player)
+ elif 'Deku Scrub' in location.name:
+ add_rule(location, create_shop_rule(location, ootworld.parser))
+ else:
+ add_item_rule(location, lambda item: item.type != 'Shop')
+
+ if ootworld.skip_child_zelda and location.name == 'Song from Impa':
+ limit_to_itemset(location, SaveContext.giveable_items)
+ add_item_rule(location, lambda item: item.player == location.player)
+
+ if location.name == 'Forest Temple MQ First Room Chest' and ootworld.shuffle_bosskeys == 'dungeon' and ootworld.shuffle_smallkeys == 'dungeon' and ootworld.tokensanity == 'off':
+ # This location needs to be a small key. Make sure the boss key isn't placed here.
+ forbid_item(location, 'Boss Key (Forest Temple)', ootworld.player)
+
+ # TODO: re-add hints once they are working
+ # if location.type == 'HintStone' and ootworld.hints == 'mask':
+ # location.add_rule(is_child)
+
+ # if location.name in ootworld.always_hints:
+ # location.add_rule(guarantee_hint)
+
+
+def create_shop_rule(location, parser):
+ def required_wallets(price):
+ if price > 500:
+ return 3
+ if price > 200:
+ return 2
+ if price > 99:
+ return 1
+ return 0
+ return parser.parse_rule('(Progressive_Wallet, %d)' % required_wallets(location.price))
+
+
+def limit_to_itemset(location, itemset):
+ old_rule = location.item_rule
+ location.item_rule = lambda item: item.name in itemset and old_rule(item)
+
+
+# This function should be run once after the shop items are placed in the world.
+# It should be run before other items are placed in the world so that logic has
+# the correct checks for them. This is safe to do since every shop is still
+# accessible when all items are obtained and every shop item is not.
+# This function should also be called when a world is copied if the original world
+# had called this function because the world.copy does not copy the rules
+def set_shop_rules(ootworld):
+ found_bombchus = ootworld.parser.parse_rule('found_bombchus')
+ wallet = ootworld.parser.parse_rule('Progressive_Wallet')
+ wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)')
+ for location in ootworld.world.get_filled_locations():
+ if location.player == ootworld.player and location.item.type == 'Shop':
+ # Add wallet requirements
+ if location.item.name in ['Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)']:
+ add_rule(location, wallet)
+ elif location.item.name in ['Buy Zora Tunic', 'Buy Blue Fire']:
+ add_rule(location, wallet2)
+
+ # Add adult only checks
+ if location.item.name in ['Buy Goron Tunic', 'Buy Zora Tunic']:
+ is_adult = ootworld.parser.parse_rule('is_adult', location)
+ add_rule(location, is_adult)
+
+ # Add item prerequisite checks
+ if location.item.name in ['Buy Blue Fire',
+ 'Buy Blue Potion',
+ 'Buy Bottle Bug',
+ 'Buy Fish',
+ 'Buy Green Potion',
+ 'Buy Poe',
+ 'Buy Red Potion [30]',
+ 'Buy Red Potion [40]',
+ 'Buy Red Potion [50]',
+ 'Buy Fairy\'s Spirit']:
+ add_rule(location, lambda state: CollectionState._oot_has_bottle(state, ootworld.player))
+ if location.item.name in ['Buy Bombchu (10)', 'Buy Bombchu (20)', 'Buy Bombchu (5)']:
+ add_rule(location, found_bombchus)
+
+
+# This function should be ran once after setting up entrances and before placing items
+# The goal is to automatically set item rules based on age requirements in case entrances were shuffled
+def set_entrances_based_rules(ootworld):
+
+ if ootworld.world.accessibility == 'beatable':
+ return
+
+ all_state = ootworld.state_with_items(ootworld.itempool)
+
+ for location in ootworld.get_locations():
+ # If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
+ if location.type == 'Shop' and not all_state._oot_reach_as_age(location.parent_region.name, 'adult', ootworld.player):
+ forbid_item(location, 'Buy Goron Tunic', ootworld.player)
+ forbid_item(location, 'Buy Zora Tunic', ootworld.player)
+
diff --git a/worlds/oot/SaveContext.py b/worlds/oot/SaveContext.py
new file mode 100644
index 00000000..27156b27
--- /dev/null
+++ b/worlds/oot/SaveContext.py
@@ -0,0 +1,1000 @@
+from itertools import chain
+
+class Address():
+ prev_address = None
+
+ def __init__(self, address=None, size=4, mask=0xFFFFFFFF, max=None, choices=None, value=None):
+ if address is None:
+ self.address = Address.prev_address
+ else:
+ self.address = address
+ self.value = value
+ self.size = size
+ self.choices = choices
+ self.mask = mask
+
+ Address.prev_address = self.address + self.size
+
+ self.bit_offset = 0
+ while mask & 1 == 0:
+ mask = mask >> 1
+ self.bit_offset += 1
+
+ if max is None:
+ self.max = mask
+ else:
+ self.max = max
+
+
+ def get_value(self, default=0):
+ if self.value is None:
+ return default
+ return self.value
+
+
+ def get_value_raw(self):
+ if self.value is None:
+ return None
+
+ value = self.value
+ if self.choices is not None:
+ value = self.choices[value]
+ if not isinstance(value, int):
+ raise ValueError("Invalid value type '%s'" % str(value))
+
+ if isinstance(value, bool):
+ value = 1 if value else 0
+ if value > self.max:
+ value = self.max
+
+ value = (value << self.bit_offset) & self.mask
+ return value
+
+
+ def set_value_raw(self, value):
+ if value is None:
+ self.value = None
+ return
+
+ if not isinstance(value, int):
+ raise ValueError("Invalid value type '%s'" % str(value))
+
+ value = (value & self.mask) >> self.bit_offset
+ if value > self.max:
+ value = self.max
+
+ if self.choices is not None:
+ for choice_name, choice_value in self.choices.items():
+ if choice_value == value:
+ value = choice_name
+ break
+
+ self.value = value
+
+
+ def get_writes(self, save_context):
+ if self.value is None:
+ return
+
+ value = self.get_value_raw()
+ if value is None:
+ return
+
+ values = zip(Address.to_bytes(value, self.size),
+ Address.to_bytes(self.mask, self.size))
+
+ for i, (byte, mask) in enumerate(values):
+ if mask == 0:
+ continue
+ if mask == 0xFF:
+ save_context.write_byte(self.address + i, byte)
+ else:
+ save_context.write_bits(self.address + i, byte, mask=mask)
+
+
+ def to_bytes(value, size):
+ ret = []
+ for _ in range(size):
+ ret.insert(0, value & 0xFF)
+ value = value >> 8
+ return ret
+
+
+class SaveContext():
+ def __init__(self):
+ self.save_bits = {}
+ self.save_bytes = {}
+ self.addresses = self.get_save_context_addresses()
+
+
+ # will set the bits of value to the offset in the save (or'ing them with what is already there)
+ def write_bits(self, address, value, mask=None, predicate=None):
+ if predicate and not predicate(value):
+ return
+
+ if mask is not None:
+ value = value & mask
+
+ if address in self.save_bytes:
+ old_val = self.save_bytes[address]
+ if mask is not None:
+ old_val &= ~mask
+ value = old_val | value
+ self.write_byte(address, value, predicate)
+ elif address in self.save_bits:
+ if mask is not None:
+ self.save_bits[address] &= ~mask
+ self.save_bits[address] |= value
+ else:
+ self.save_bits[address] = value
+
+
+ # will overwrite the byte at offset with the given value
+ def write_byte(self, address, value, predicate=None):
+ if predicate and not predicate(value):
+ return
+
+ if address in self.save_bits:
+ del self.save_bits[address]
+
+ self.save_bytes[address] = value
+
+
+ # will overwrite the byte at offset with the given value
+ def write_bytes(self, address, bytes, predicate=None):
+ for i, value in enumerate(bytes):
+ self.write_byte(address + i, value, predicate)
+
+
+ def write_save_entry(self, address):
+ if isinstance(address, dict):
+ for name, sub_address in address.items():
+ self.write_save_entry(sub_address)
+ elif isinstance(address, list):
+ for sub_address in address:
+ self.write_save_entry(sub_address)
+ else:
+ address.get_writes(self)
+
+
+ def set_ammo_max(self):
+ ammo_maxes = {
+ 'stick' : ('stick_upgrade', [10, 10, 20, 30]),
+ 'nut' : ('nut_upgrade', [20, 20, 30, 40]),
+ 'bomb' : ('bomb_bag', [00, 20, 30, 40]),
+ 'bow' : ('quiver', [00, 30, 40, 50]),
+ 'slingshot' : ('bullet_bag', [00, 30, 40, 50]),
+ 'rupees' : ('wallet', [99, 200, 500, 999]),
+ }
+
+ for ammo, (upgrade, maxes) in ammo_maxes.items():
+ upgrade_count = self.addresses['upgrades'][upgrade].get_value()
+ try:
+ ammo_max = maxes[upgrade_count]
+ except IndexError:
+ ammo_max = maxes[-1]
+ if ammo == 'rupees':
+ self.addresses[ammo].max = ammo_max
+ else:
+ self.addresses['ammo'][ammo].max = ammo_max
+
+
+ # will overwrite the byte at offset with the given value
+ def write_save_table(self, rom):
+ self.set_ammo_max()
+ for name, address in self.addresses.items():
+ self.write_save_entry(address)
+
+ save_table = []
+ for address, value in self.save_bits.items():
+ if value != 0:
+ save_table += [(address & 0xFF00) >> 8, address & 0xFF, 0x00, value]
+ for address, value in self.save_bytes.items():
+ save_table += [(address & 0xFF00) >> 8, address & 0xFF, 0x01, value]
+ save_table += [0x00,0x00,0x00,0x00]
+
+ table_len = len(save_table)
+ if table_len > 0x400:
+ raise Exception("The Initial Save Table has exceeded its maximum capacity: 0x%03X/0x400" % table_len)
+ rom.write_bytes(rom.sym('INITIAL_SAVE_DATA'), save_table)
+
+
+ def give_bottle(self, item, count):
+ for bottle_id in range(4):
+ item_slot = 'bottle_%d' % (bottle_id + 1)
+ if self.addresses['item_slot'][item_slot].get_value(0xFF) != 0xFF:
+ continue
+
+ self.addresses['item_slot'][item_slot].value = SaveContext.bottle_types[item]
+ count -= 1
+
+ if count == 0:
+ return
+
+
+ def give_health(self, health):
+ health += self.addresses['health_capacity'].get_value(0x30) / 0x10
+ health += self.addresses['quest']['heart_pieces'].get_value() / 4
+
+ self.addresses['health_capacity'].value = int(health) * 0x10
+ self.addresses['health'].value = int(health) * 0x10
+ self.addresses['quest']['heart_pieces'].value = int((health % 1) * 4)
+
+
+ def give_raw_item(self, item):
+ if item.endswith(')'):
+ item_base, count = item[:-1].split(' (', 1)
+ if count.isdigit():
+ return self.give_item(item_base, count=int(count))
+ return self.give_item(item)
+
+
+ def give_item(self, item, count=1):
+ if item in SaveContext.bottle_types:
+ self.give_bottle(item, count)
+ elif item in ["Piece of Heart", "Piece of Heart (Treasure Chest Game)"]:
+ self.give_health(count / 4)
+ elif item == "Heart Container":
+ self.give_health(count)
+ elif item == "Bombchu Item":
+ self.give_bombchu_item()
+ elif item in SaveContext.save_writes_table:
+ for address, value in SaveContext.save_writes_table[item].items():
+ if value is None:
+ value = count
+ elif isinstance(value, list):
+ value = value[min(len(value), count) - 1]
+ elif isinstance(value, bool):
+ value = 1 if value else 0
+
+ address_value = self.addresses
+ prev_sub_address = 'Save Context'
+ for sub_address in address.split('.'):
+ if sub_address not in address_value:
+ raise ValueError('Unknown key %s in %s of SaveContext' % (sub_address, prev_sub_address))
+
+ if isinstance(address_value, list):
+ sub_address = int(sub_address)
+
+ address_value = address_value[sub_address]
+ prev_sub_address = sub_address
+ if not isinstance(address_value, Address):
+ raise ValueError('%s does not resolve to an Address in SaveContext' % (sub_address))
+
+ if isinstance(value, int) and value < address_value.get_value():
+ continue
+
+ address_value.value = value
+ else:
+ raise ValueError("Cannot give unknown starting item %s" % item)
+
+
+ def give_bombchu_item(self):
+ self.give_item("Bombchus", 0)
+
+
+ def equip_default_items(self, age):
+ self.equip_items(age, 'equips_' + age)
+
+
+ def equip_current_items(self, age):
+ self.equip_items(age, 'equips')
+
+
+ def equip_items(self, age, equip_type):
+ if age not in ['child', 'adult']:
+ raise ValueError("Age must be 'adult' or 'child', not %s" % age)
+
+ if equip_type not in ['equips', 'equips_child', 'equips_adult']:
+ raise ValueError("Equip type must be 'equips', 'equips_child' or 'equips_adult', not %s" % equip_type)
+
+ age = 'equips_' + age
+ c_buttons = list(self.addresses[age]['button_slots'].keys())
+ for item_slot in SaveContext.equipable_items[age]['items']:
+ item = self.addresses['item_slot'][item_slot].get_value('none')
+ if item != 'none':
+ c_button = c_buttons.pop()
+ self.addresses[equip_type]['button_slots'][c_button].value = item_slot
+ self.addresses[equip_type]['button_items'][c_button].value = item
+ if not c_buttons:
+ break
+
+ for equip_item, equip_addresses in self.addresses[age]['equips'].items():
+ for item in SaveContext.equipable_items[age][equip_item]:
+ if self.addresses['equip_items'][item].get_value():
+ item_value = self.addresses['equip_items'][item].get_value_raw()
+ self.addresses[equip_type]['equips'][equip_item].set_value_raw(item_value)
+ if equip_item == 'tunic':
+ self.addresses[equip_type]['equips'][equip_item].value = 1
+ if equip_item == 'sword':
+ self.addresses[equip_type]['button_items']['b'].value = item
+ break
+
+
+ def get_save_context_addresses(self):
+ return {
+ 'entrance_index' : Address(0x0000, size=4),
+ 'link_age' : Address(size=4, max=1),
+ 'unk_00' : Address(size=2),
+ 'cutscene_index' : Address(size=2),
+ 'time_of_day' : Address(size=2),
+ 'unk_01' : Address(size=2),
+ 'night_flag' : Address(size=4, max=1),
+ 'unk_02' : Address(size=8),
+ 'id' : Address(size=6),
+ 'deaths' : Address(size=2),
+ 'file_name' : Address(size=8),
+ 'n64dd_flag' : Address(size=2),
+ 'health_capacity' : Address(size=2, max=0x140),
+ 'health' : Address(size=2, max=0x140),
+ 'magic_level' : Address(size=1, max=2),
+ 'magic' : Address(size=1, max=0x60),
+ 'rupees' : Address(size=2),
+ 'bgs_hits_left' : Address(size=2),
+ 'navi_timer' : Address(size=2),
+ 'magic_acquired' : Address(size=1, max=1),
+ 'unk_03' : Address(size=1),
+ 'double_magic' : Address(size=1, max=1),
+ 'double_defense' : Address(size=1, max=1),
+ 'bgs_flag' : Address(size=1, max=1),
+ 'unk_05' : Address(size=1),
+
+ # Equiped Items
+ 'equips_child' : {
+ 'button_items' : {
+ 'b' : Address(size=1, choices=SaveContext.item_id_map),
+ 'left' : Address(size=1, choices=SaveContext.item_id_map),
+ 'down' : Address(size=1, choices=SaveContext.item_id_map),
+ 'right' : Address(size=1, choices=SaveContext.item_id_map),
+ },
+ 'button_slots' : {
+ 'left' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'down' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'right' : Address(size=1, choices=SaveContext.slot_id_map),
+ },
+ 'equips' : {
+ 'sword' : Address(0x0048, size=2, mask=0x000F),
+ 'shield' : Address(0x0048, size=2, mask=0x00F0),
+ 'tunic' : Address(0x0048, size=2, mask=0x0F00),
+ 'boots' : Address(0x0048, size=2, mask=0xF000),
+ },
+ },
+ 'equips_adult' : {
+ 'button_items' : {
+ 'b' : Address(size=1, choices=SaveContext.item_id_map),
+ 'left' : Address(size=1, choices=SaveContext.item_id_map),
+ 'down' : Address(size=1, choices=SaveContext.item_id_map),
+ 'right' : Address(size=1, choices=SaveContext.item_id_map),
+ },
+ 'button_slots' : {
+ 'left' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'down' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'right' : Address(size=1, choices=SaveContext.slot_id_map),
+ },
+ 'equips' : {
+ 'sword' : Address(0x0052, size=2, mask=0x000F),
+ 'shield' : Address(0x0052, size=2, mask=0x00F0),
+ 'tunic' : Address(0x0052, size=2, mask=0x0F00),
+ 'boots' : Address(0x0052, size=2, mask=0xF000),
+ },
+ },
+ 'unk_06' : Address(size=0x12),
+ 'scene_index' : Address(size=2),
+
+ 'equips' : {
+ 'button_items' : {
+ 'b' : Address(size=1, choices=SaveContext.item_id_map),
+ 'left' : Address(size=1, choices=SaveContext.item_id_map),
+ 'down' : Address(size=1, choices=SaveContext.item_id_map),
+ 'right' : Address(size=1, choices=SaveContext.item_id_map),
+ },
+ 'button_slots' : {
+ 'left' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'down' : Address(size=1, choices=SaveContext.slot_id_map),
+ 'right' : Address(size=1, choices=SaveContext.slot_id_map),
+ },
+ 'equips' : {
+ 'sword' : Address(0x0070, size=2, mask=0x000F, max=3),
+ 'shield' : Address(0x0070, size=2, mask=0x00F0, max=3),
+ 'tunic' : Address(0x0070, size=2, mask=0x0F00, max=3),
+ 'boots' : Address(0x0070, size=2, mask=0xF000, max=3),
+ },
+ },
+ 'unk_07' : Address(size=2),
+
+ # Item Slots
+ 'item_slot' : {
+ 'stick' : Address(size=1, choices=SaveContext.item_id_map),
+ 'nut' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bomb' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bow' : Address(size=1, choices=SaveContext.item_id_map),
+ 'fire_arrow' : Address(size=1, choices=SaveContext.item_id_map),
+ 'dins_fire' : Address(size=1, choices=SaveContext.item_id_map),
+ 'slingshot' : Address(size=1, choices=SaveContext.item_id_map),
+ 'ocarina' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bombchu' : Address(size=1, choices=SaveContext.item_id_map),
+ 'hookshot' : Address(size=1, choices=SaveContext.item_id_map),
+ 'ice_arrow' : Address(size=1, choices=SaveContext.item_id_map),
+ 'farores_wind' : Address(size=1, choices=SaveContext.item_id_map),
+ 'boomerang' : Address(size=1, choices=SaveContext.item_id_map),
+ 'lens' : Address(size=1, choices=SaveContext.item_id_map),
+ 'beans' : Address(size=1, choices=SaveContext.item_id_map),
+ 'hammer' : Address(size=1, choices=SaveContext.item_id_map),
+ 'light_arrow' : Address(size=1, choices=SaveContext.item_id_map),
+ 'nayrus_love' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bottle_1' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bottle_2' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bottle_3' : Address(size=1, choices=SaveContext.item_id_map),
+ 'bottle_4' : Address(size=1, choices=SaveContext.item_id_map),
+ 'adult_trade' : Address(size=1, choices=SaveContext.item_id_map),
+ 'child_trade' : Address(size=1, choices=SaveContext.item_id_map),
+ },
+
+ # Item Ammo
+ 'ammo' : {
+ 'stick' : Address(size=1),
+ 'nut' : Address(size=1),
+ 'bomb' : Address(size=1),
+ 'bow' : Address(size=1),
+ 'fire_arrow' : Address(size=1, max=0),
+ 'dins_fire' : Address(size=1, max=0),
+ 'slingshot' : Address(size=1),
+ 'ocarina' : Address(size=1, max=0),
+ 'bombchu' : Address(size=1, max=50),
+ 'hookshot' : Address(size=1, max=0),
+ 'ice_arrow' : Address(size=1, max=0),
+ 'farores_wind' : Address(size=1, max=0),
+ 'boomerang' : Address(size=1, max=0),
+ 'lens' : Address(size=1, max=0),
+ 'beans' : Address(size=1, max=10),
+ },
+ 'magic_beans_sold' : Address(size=1, max=10),
+
+ # Equipment
+ 'equip_items' : {
+ 'kokiri_sword' : Address(0x009C, size=2, mask=0x0001),
+ 'master_sword' : Address(0x009C, size=2, mask=0x0002),
+ 'biggoron_sword' : Address(0x009C, size=2, mask=0x0004),
+ 'broken_knife' : Address(0x009C, size=2, mask=0x0008),
+ 'deku_shield' : Address(0x009C, size=2, mask=0x0010),
+ 'hylian_shield' : Address(0x009C, size=2, mask=0x0020),
+ 'mirror_shield' : Address(0x009C, size=2, mask=0x0040),
+ 'kokiri_tunic' : Address(0x009C, size=2, mask=0x0100),
+ 'goron_tunic' : Address(0x009C, size=2, mask=0x0200),
+ 'zora_tunic' : Address(0x009C, size=2, mask=0x0400),
+ 'kokiri_boots' : Address(0x009C, size=2, mask=0x1000),
+ 'iron_boots' : Address(0x009C, size=2, mask=0x2000),
+ 'hover_boots' : Address(0x009C, size=2, mask=0x4000),
+ },
+
+ 'unk_08' : Address(size=2),
+
+ # Upgrades
+ 'upgrades' : {
+ 'quiver' : Address(0x00A0, mask=0x00000007, max=3),
+ 'bomb_bag' : Address(0x00A0, mask=0x00000038, max=3),
+ 'strength_upgrade' : Address(0x00A0, mask=0x000001C0, max=3),
+ 'diving_upgrade' : Address(0x00A0, mask=0x00000E00, max=2),
+ 'wallet' : Address(0x00A0, mask=0x00003000, max=3),
+ 'bullet_bag' : Address(0x00A0, mask=0x0001C000, max=3),
+ 'stick_upgrade' : Address(0x00A0, mask=0x000E0000, max=3),
+ 'nut_upgrade' : Address(0x00A0, mask=0x00700000, max=3),
+ },
+
+ # Medallions
+ 'quest' : {
+ 'medallions' : {
+ 'forest' : Address(0x00A4, mask=0x00000001),
+ 'fire' : Address(0x00A4, mask=0x00000002),
+ 'water' : Address(0x00A4, mask=0x00000004),
+ 'spirit' : Address(0x00A4, mask=0x00000008),
+ 'shadow' : Address(0x00A4, mask=0x00000010),
+ 'light' : Address(0x00A4, mask=0x00000020),
+
+ },
+ 'songs' : {
+ 'minuet_of_forest' : Address(0x00A4, mask=0x00000040),
+ 'bolero_of_fire' : Address(0x00A4, mask=0x00000080),
+ 'serenade_of_water' : Address(0x00A4, mask=0x00000100),
+ 'requiem_of_spirit' : Address(0x00A4, mask=0x00000200),
+ 'nocturne_of_shadow' : Address(0x00A4, mask=0x00000400),
+ 'prelude_of_light' : Address(0x00A4, mask=0x00000800),
+ 'zeldas_lullaby' : Address(0x00A4, mask=0x00001000),
+ 'eponas_song' : Address(0x00A4, mask=0x00002000),
+ 'sarias_song' : Address(0x00A4, mask=0x00004000),
+ 'suns_song' : Address(0x00A4, mask=0x00008000),
+ 'song_of_time' : Address(0x00A4, mask=0x00010000),
+ 'song_of_storms' : Address(0x00A4, mask=0x00020000),
+ },
+ 'stones' : {
+ 'kokiris_emerald' : Address(0x00A4, mask=0x00040000),
+ 'gorons_ruby' : Address(0x00A4, mask=0x00080000),
+ 'zoras_sapphire' : Address(0x00A4, mask=0x00100000),
+ },
+ 'stone_of_agony' : Address(0x00A4, mask=0x00200000),
+ 'gerudos_card' : Address(0x00A4, mask=0x00400000),
+ 'gold_skulltula' : Address(0x00A4, mask=0x00800000),
+ 'heart_pieces' : Address(0x00A4, mask=0xFF000000),
+ },
+
+ # Dungeon Items
+ 'dungeon_items' : {
+ 'deku' : {
+ 'boss_key' : Address(0x00A8, size=1, mask=0x01),
+ 'compass' : Address(0x00A8, size=1, mask=0x02),
+ 'map' : Address(0x00A8, size=1, mask=0x04),
+ },
+ 'dodongo' : {
+ 'boss_key' : Address(0x00A9, size=1, mask=0x01),
+ 'compass' : Address(0x00A9, size=1, mask=0x02),
+ 'map' : Address(0x00A9, size=1, mask=0x04),
+ },
+ 'jabu' : {
+ 'boss_key' : Address(0x00AA, size=1, mask=0x01),
+ 'compass' : Address(0x00AA, size=1, mask=0x02),
+ 'map' : Address(0x00AA, size=1, mask=0x04),
+ },
+ 'forest' : {
+ 'boss_key' : Address(0x00AB, size=1, mask=0x01),
+ 'compass' : Address(0x00AB, size=1, mask=0x02),
+ 'map' : Address(0x00AB, size=1, mask=0x04),
+ },
+ 'fire' : {
+ 'boss_key' : Address(0x00AC, size=1, mask=0x01),
+ 'compass' : Address(0x00AC, size=1, mask=0x02),
+ 'map' : Address(0x00AC, size=1, mask=0x04),
+ },
+ 'water' : {
+ 'boss_key' : Address(0x00AD, size=1, mask=0x01),
+ 'compass' : Address(0x00AD, size=1, mask=0x02),
+ 'map' : Address(0x00AD, size=1, mask=0x04),
+ },
+ 'spirit' : {
+ 'boss_key' : Address(0x00AE, size=1, mask=0x01),
+ 'compass' : Address(0x00AE, size=1, mask=0x02),
+ 'map' : Address(0x00AE, size=1, mask=0x04),
+ },
+ 'shadow' : {
+ 'boss_key' : Address(0x00AF, size=1, mask=0x01),
+ 'compass' : Address(0x00AF, size=1, mask=0x02),
+ 'map' : Address(0x00AF, size=1, mask=0x04),
+ },
+ 'botw' : {
+ 'boss_key' : Address(0x00B0, size=1, mask=0x01),
+ 'compass' : Address(0x00B0, size=1, mask=0x02),
+ 'map' : Address(0x00B0, size=1, mask=0x04),
+ },
+ 'ice' : {
+ 'boss_key' : Address(0x00B1, size=1, mask=0x01),
+ 'compass' : Address(0x00B1, size=1, mask=0x02),
+ 'map' : Address(0x00B1, size=1, mask=0x04),
+ },
+ 'gt' : {
+ 'boss_key' : Address(0x00B2, size=1, mask=0x01),
+ 'compass' : Address(0x00B2, size=1, mask=0x02),
+ 'map' : Address(0x00B2, size=1, mask=0x04),
+ },
+ 'gtg' : {
+ 'boss_key' : Address(0x00B3, size=1, mask=0x01),
+ 'compass' : Address(0x00B3, size=1, mask=0x02),
+ 'map' : Address(0x00B3, size=1, mask=0x04),
+ },
+ 'fortress' : {
+ 'boss_key' : Address(0x00B4, size=1, mask=0x01),
+ 'compass' : Address(0x00B4, size=1, mask=0x02),
+ 'map' : Address(0x00B4, size=1, mask=0x04),
+ },
+ 'gc' : {
+ 'boss_key' : Address(0x00B5, size=1, mask=0x01),
+ 'compass' : Address(0x00B5, size=1, mask=0x02),
+ 'map' : Address(0x00B5, size=1, mask=0x04),
+ },
+ 'unused' : Address(size=6),
+ },
+ 'keys' : {
+ 'deku' : Address(size=1),
+ 'dodongo' : Address(size=1),
+ 'jabu' : Address(size=1),
+ 'forest' : Address(size=1),
+ 'fire' : Address(size=1),
+ 'water' : Address(size=1),
+ 'spirit' : Address(size=1),
+ 'shadow' : Address(size=1),
+ 'botw' : Address(size=1),
+ 'ice' : Address(size=1),
+ 'gt' : Address(size=1),
+ 'gtg' : Address(size=1),
+ 'fortress' : Address(size=1),
+ 'gc' : Address(size=1),
+ 'unused' : Address(size=5),
+ },
+ 'defense_hearts' : Address(size=1, max=20),
+ 'gs_tokens' : Address(size=2, max=100),
+ 'triforce_pieces' : Address(0xD4 + 0x1C * 0x48 + 0x10, size=4), # Unused word in scene x48
+ }
+
+
+ item_id_map = {
+ 'none' : 0xFF,
+ 'stick' : 0x00,
+ 'nut' : 0x01,
+ 'bomb' : 0x02,
+ 'bow' : 0x03,
+ 'fire_arrow' : 0x04,
+ 'dins_fire' : 0x05,
+ 'slingshot' : 0x06,
+ 'fairy_ocarina' : 0x07,
+ 'ocarina_of_time' : 0x08,
+ 'bombchu' : 0x09,
+ 'hookshot' : 0x0A,
+ 'longshot' : 0x0B,
+ 'ice_arrow' : 0x0C,
+ 'farores_wind' : 0x0D,
+ 'boomerang' : 0x0E,
+ 'lens' : 0x0F,
+ 'beans' : 0x10,
+ 'hammer' : 0x11,
+ 'light_arrow' : 0x12,
+ 'nayrus_love' : 0x13,
+ 'bottle' : 0x14,
+ 'red_potion' : 0x15,
+ 'green_potion' : 0x16,
+ 'blue_potion' : 0x17,
+ 'fairy' : 0x18,
+ 'fish' : 0x19,
+ 'milk' : 0x1A,
+ 'letter' : 0x1B,
+ 'blue_fire' : 0x1C,
+ 'bug' : 0x1D,
+ 'big_poe' : 0x1E,
+ 'half_milk' : 0x1F,
+ 'poe' : 0x20,
+ 'weird_egg' : 0x21,
+ 'chicken' : 0x22,
+ 'zeldas_letter' : 0x23,
+ 'keaton_mask' : 0x24,
+ 'skull_mask' : 0x25,
+ 'spooky_mask' : 0x26,
+ 'bunny_hood' : 0x27,
+ 'goron_mask' : 0x28,
+ 'zora_mask' : 0x29,
+ 'gerudo_mask' : 0x2A,
+ 'mask_of_truth' : 0x2B,
+ 'sold_out' : 0x2C,
+ 'pocket_egg' : 0x2D,
+ 'pocket_cucco' : 0x2E,
+ 'cojiro' : 0x2F,
+ 'odd_mushroom' : 0x30,
+ 'odd_potion' : 0x31,
+ 'poachers_saw' : 0x32,
+ 'broken_gorons_sword' : 0x33,
+ 'prescription' : 0x34,
+ 'eyeball_frog' : 0x35,
+ 'eye_drops' : 0x36,
+ 'claim_check' : 0x37,
+ 'bow_fire_arrow' : 0x38,
+ 'bow_ice_arrow' : 0x39,
+ 'bow_light_arrow' : 0x3A,
+ 'kokiri_sword' : 0x3B,
+ 'master_sword' : 0x3C,
+ 'biggoron_sword' : 0x3D,
+ 'deku_shield' : 0x3E,
+ 'hylian_shield' : 0x3F,
+ 'mirror_shield' : 0x40,
+ 'kokiri_tunic' : 0x41,
+ 'goron_tunic' : 0x42,
+ 'zora_tunic' : 0x43,
+ 'kokiri_boots' : 0x44,
+ 'iron_boots' : 0x45,
+ 'hover_boots' : 0x46,
+ 'bullet_bag_30' : 0x47,
+ 'bullet_bag_40' : 0x48,
+ 'bullet_bag_50' : 0x49,
+ 'quiver_30' : 0x4A,
+ 'quiver_40' : 0x4B,
+ 'quiver_50' : 0x4C,
+ 'bomb_bag_20' : 0x4D,
+ 'bomb_bag_30' : 0x4E,
+ 'bomb_bag_40' : 0x4F,
+ 'gorons_bracelet' : 0x40,
+ 'silver_gauntlets' : 0x41,
+ 'golden_gauntlets' : 0x42,
+ 'silver_scale' : 0x43,
+ 'golden_scale' : 0x44,
+ 'broken_giants_knife' : 0x45,
+ 'adults_wallet' : 0x46,
+ 'giants_wallet' : 0x47,
+ 'deku_seeds' : 0x48,
+ 'fishing_pole' : 0x49,
+ 'minuet' : 0x4A,
+ 'bolero' : 0x4B,
+ 'serenade' : 0x4C,
+ 'requiem' : 0x4D,
+ 'nocturne' : 0x4E,
+ 'prelude' : 0x4F,
+ 'zeldas_lullaby' : 0x50,
+ 'eponas_song' : 0x51,
+ 'sarias_song' : 0x52,
+ 'suns_song' : 0x53,
+ 'song_of_time' : 0x54,
+ 'song_of_storms' : 0x55,
+ 'forest_medallion' : 0x56,
+ 'fire_medallion' : 0x57,
+ 'water_medallion' : 0x58,
+ 'spirit_medallion' : 0x59,
+ 'shadow_medallion' : 0x5A,
+ 'light_medallion' : 0x5B,
+ 'kokiris_emerald' : 0x5C,
+ 'gorons_ruby' : 0x5D,
+ 'zoras_sapphire' : 0x5E,
+ 'stone_of_agony' : 0x5F,
+ 'gerudos_card' : 0x60,
+ 'gold_skulltula' : 0x61,
+ 'heart_container' : 0x62,
+ 'piece_of_heart' : 0x63,
+ 'boss_key' : 0x64,
+ 'compass' : 0x65,
+ 'dungeon_map' : 0x66,
+ 'small_key' : 0x67,
+ }
+
+
+ slot_id_map = {
+ 'stick' : 0x00,
+ 'nut' : 0x01,
+ 'bomb' : 0x02,
+ 'bow' : 0x03,
+ 'fire_arrow' : 0x04,
+ 'dins_fire' : 0x05,
+ 'slingshot' : 0x06,
+ 'ocarina' : 0x07,
+ 'bombchu' : 0x08,
+ 'hookshot' : 0x09,
+ 'ice_arrow' : 0x0A,
+ 'farores_wind' : 0x0B,
+ 'boomerang' : 0x0C,
+ 'lens' : 0x0D,
+ 'beans' : 0x0E,
+ 'hammer' : 0x0F,
+ 'light_arrow' : 0x10,
+ 'nayrus_love' : 0x11,
+ 'bottle_1' : 0x12,
+ 'bottle_2' : 0x13,
+ 'bottle_3' : 0x14,
+ 'bottle_4' : 0x15,
+ 'adult_trade' : 0x16,
+ 'child_trade' : 0x17,
+ }
+
+
+ bottle_types = {
+ "Bottle" : 'bottle',
+ "Bottle with Red Potion" : 'red_potion',
+ "Bottle with Green Potion" : 'green_potion',
+ "Bottle with Blue Potion" : 'blue_potion',
+ "Bottle with Fairy" : 'fairy',
+ "Bottle with Fish" : 'fish',
+ "Bottle with Milk" : 'milk',
+ "Rutos Letter" : 'letter',
+ "Bottle with Blue Fire" : 'blue_fire',
+ "Bottle with Bugs" : 'bug',
+ "Bottle with Big Poe" : 'big_poe',
+ "Bottle with Milk (Half)" : 'half_milk',
+ "Bottle with Poe" : 'poe',
+ }
+
+
+ save_writes_table = {
+ "Deku Stick Capacity": {
+ 'item_slot.stick' : 'stick',
+ 'upgrades.stick_upgrade' : [2,3],
+ },
+ "Deku Sticks": {
+ 'item_slot.stick' : 'stick',
+ 'upgrades.stick_upgrade' : 1,
+ 'ammo.stick' : None,
+ },
+ "Deku Nut Capacity": {
+ 'item_slot.nut' : 'nut',
+ 'upgrades.nut_upgrade' : [2,3],
+ },
+ "Deku Nuts": {
+ 'item_slot.nut' : 'nut',
+ 'upgrades.nut_upgrade' : 1,
+ 'ammo.nut' : None,
+ },
+ "Bomb Bag": {
+ 'item_slot.bomb' : 'bomb',
+ 'upgrades.bomb_bag' : None,
+ },
+ "Bombs" : {
+ 'ammo.bomb' : None,
+ },
+ "Bombchus" : {
+ 'item_slot.bombchu' : 'bombchu',
+ 'ammo.bombchu' : None,
+ },
+ "Bow" : {
+ 'item_slot.bow' : 'bow',
+ 'upgrades.quiver' : None,
+ },
+ "Arrows" : {
+ 'ammo.bow' : None,
+ },
+ "Slingshot" : {
+ 'item_slot.slingshot' : 'slingshot',
+ 'upgrades.bullet_bag' : None,
+ },
+ "Deku Seeds" : {
+ 'ammo.slingshot' : None,
+ },
+ "Magic Bean" : {
+ 'item_slot.beans' : 'beans',
+ 'ammo.beans' : None,
+ 'magic_beans_sold' : None,
+ },
+ "Fire Arrows" : {'item_slot.fire_arrow' : 'fire_arrow'},
+ "Ice Arrows" : {'item_slot.ice_arrow' : 'ice_arrow'},
+ "Light Arrows" : {'item_slot.light_arrow' : 'light_arrow'},
+ "Dins Fire" : {'item_slot.dins_fire' : 'dins_fire'},
+ "Farores Wind" : {'item_slot.farores_wind' : 'farores_wind'},
+ "Nayrus Love" : {'item_slot.nayrus_love' : 'nayrus_love'},
+ "Ocarina" : {'item_slot.ocarina' : ['fairy_ocarina', 'ocarina_of_time']},
+ "Progressive Hookshot" : {'item_slot.hookshot' : ['hookshot', 'longshot']},
+ "Boomerang" : {'item_slot.boomerang' : 'boomerang'},
+ "Lens of Truth" : {'item_slot.lens' : 'lens'},
+ "Megaton Hammer" : {'item_slot.hammer' : 'hammer'},
+ "Pocket Egg" : {'item_slot.adult_trade' : 'pocket_egg'},
+ "Pocket Cucco" : {'item_slot.adult_trade' : 'pocket_cucco'},
+ "Cojiro" : {'item_slot.adult_trade' : 'cojiro'},
+ "Odd Mushroom" : {'item_slot.adult_trade' : 'odd_mushroom'},
+ "Poachers Saw" : {'item_slot.adult_trade' : 'poachers_saw'},
+ "Broken Sword" : {'item_slot.adult_trade' : 'broken_knife'},
+ "Prescription" : {'item_slot.adult_trade' : 'prescription'},
+ "Eyeball Frog" : {'item_slot.adult_trade' : 'eyeball_frog'},
+ "Eyedrops" : {'item_slot.adult_trade' : 'eye_drops'},
+ "Claim Check" : {'item_slot.adult_trade' : 'claim_check'},
+ "Weird Egg" : {'item_slot.child_trade' : 'weird_egg'},
+ "Chicken" : {'item_slot.child_trade' : 'chicken'},
+ "Zeldas Letter" : {'item_slot.child_trade' : 'zeldas_letter'},
+ "Goron Tunic" : {'equip_items.goron_tunic' : True},
+ "Zora Tunic" : {'equip_items.zora_tunic' : True},
+ "Iron Boots" : {'equip_items.iron_boots' : True},
+ "Hover Boots" : {'equip_items.hover_boots' : True},
+ "Deku Shield" : {'equip_items.deku_shield' : True},
+ "Hylian Shield" : {'equip_items.hylian_shield' : True},
+ "Mirror Shield" : {'equip_items.mirror_shield' : True},
+ "Kokiri Sword" : {'equip_items.kokiri_sword' : True},
+ "Master Sword" : {'equip_items.master_sword' : True},
+ "Giants Knife" : {
+ 'equip_items.biggoron_sword' : True,
+ 'bgs_hits_left' : 8,
+ },
+ "Biggoron Sword" : {
+ 'equip_items.biggoron_sword' : True,
+ 'bgs_flag' : True,
+ 'bgs_hits_left' : 1,
+ },
+ "Gerudo Membership Card" : {'quest.gerudos_card' : True},
+ "Stone of Agony" : {'quest.stone_of_agony' : True},
+ "Zeldas Lullaby" : {'quest.songs.zeldas_lullaby' : True},
+ "Eponas Song" : {'quest.songs.eponas_song' : True},
+ "Sarias Song" : {'quest.songs.sarias_song' : True},
+ "Suns Song" : {'quest.songs.suns_song' : True},
+ "Song of Time" : {'quest.songs.song_of_time' : True},
+ "Song of Storms" : {'quest.songs.song_of_storms' : True},
+ "Minuet of Forest" : {'quest.songs.minuet_of_forest' : True},
+ "Bolero of Fire" : {'quest.songs.bolero_of_fire' : True},
+ "Serenade of Water" : {'quest.songs.serenade_of_water' : True},
+ "Requiem of Spirit" : {'quest.songs.requiem_of_spirit' : True},
+ "Nocturne of Shadow" : {'quest.songs.nocturne_of_shadow' : True},
+ "Prelude of Light" : {'quest.songs.prelude_of_light' : True},
+ "Kokiri Emerald" : {'quest.stones.kokiris_emerald' : True},
+ "Goron Ruby" : {'quest.stones.gorons_ruby' : True},
+ "Zora Sapphire" : {'quest.stones.zoras_sapphire' : True},
+ "Light Medallion" : {'quest.medallions.light' : True},
+ "Forest Medallion" : {'quest.medallions.forest' : True},
+ "Fire Medallion" : {'quest.medallions.fire' : True},
+ "Water Medallion" : {'quest.medallions.water' : True},
+ "Spirit Medallion" : {'quest.medallions.spirit' : True},
+ "Shadow Medallion" : {'quest.medallions.shadow' : True},
+ "Progressive Strength Upgrade" : {'upgrades.strength_upgrade' : None},
+ "Progressive Scale" : {'upgrades.diving_upgrade' : None},
+ "Progressive Wallet" : {'upgrades.wallet' : None},
+ "Gold Skulltula Token" : {
+ 'quest.gold_skulltula' : True,
+ 'gs_tokens' : None,
+ },
+ "Double Defense" : {
+ 'double_defense' : True,
+ 'defense_hearts' : 20,
+ },
+ "Magic Meter" : {
+ 'magic_acquired' : True,
+ 'magic' : [0x30, 0x60],
+ 'magic_level' : None,
+ 'double_magic' : [False, True],
+ },
+ "Rupee" : {'rupees' : None},
+ "Rupees" : {'rupees' : None},
+ "Magic Bean Pack" : {
+ 'item_slot.beans' : 'beans',
+ 'ammo.beans' : 10
+ },
+ "Triforce Piece" : {'triforce_pieces': None},
+ }
+
+ giveable_items = set(chain(save_writes_table.keys(), bottle_types.keys(),
+ ["Piece of Heart", "Piece of Heart (Treasure Chest Game)", "Heart Container", "Rupee (1)"]))
+
+
+ equipable_items = {
+ 'equips_adult' : {
+ 'items': [
+ 'hookshot',
+ 'hammer',
+ 'bomb',
+ 'bow',
+ 'nut',
+ 'lens',
+ 'farores_wind',
+ 'dins_fire',
+ 'bombchu',
+ 'nayrus_love',
+ 'adult_trade',
+ 'bottle_1',
+ 'bottle_2',
+ 'bottle_3',
+ 'bottle_4',
+ ],
+ 'sword' : [
+ 'biggoron_sword',
+ 'master_sword',
+ ],
+ 'shield' : [
+ 'mirror_shield',
+ 'hylian_shield',
+ ],
+ 'tunic' : [
+ 'goron_tunic',
+ 'zora_tunic',
+ 'kokiri_tunic',
+ ],
+ 'boots' : [
+ 'kokiri_boots'
+ ],
+ },
+ 'equips_child' : {
+ 'items': [
+ 'bomb',
+ 'boomerang',
+ 'slingshot',
+ 'stick',
+ 'nut',
+ 'lens',
+ 'farores_wind',
+ 'dins_fire',
+ 'bombchu',
+ 'nayrus_love',
+ 'beans',
+ 'child_trade',
+ 'bottle_1',
+ 'bottle_2',
+ 'bottle_3',
+ 'bottle_4',
+ ],
+ 'sword' : [
+ 'kokiri_sword',
+ ],
+ 'shield' : [
+ 'deku_shield',
+ 'hylian_shield',
+ ],
+ 'tunic' : [
+ 'kokiri_tunic',
+ ],
+ 'boots' : [
+ 'kokiri_boots',
+ ],
+ }
+ }
diff --git a/worlds/oot/Sounds.py b/worlds/oot/Sounds.py
new file mode 100644
index 00000000..685e39c5
--- /dev/null
+++ b/worlds/oot/Sounds.py
@@ -0,0 +1,212 @@
+# SOUNDS.PY
+#
+# A data-oriented module created to avoid cluttering (and entangling) other,
+# more important modules with sound data.
+#
+# Tags
+# To easily fetch related sounds by their properties. This seems generally
+# better than the alternative of defining long lists by hand. You still can, of
+# course. Categorizing sounds with more useful tags will require some work. Do
+# this as needed.
+#
+# Sounds
+# These are a collection of data structures relating to sounds. Already I'm sure
+# you get the picture.
+#
+# Sound Pools
+# These are just groups of sounds, to be referenced by sfx settings. Could
+# potentially merit enumerating later on. ¯\_(ツ)_/¯
+#
+# Sound Hooks
+# These are intended to gear themselves toward configurable settings, rather
+# than to document every location where a particular sound is used. For example,
+# suppose we want a setting to override all of Link's vocalizations. The sound
+# hook would contain a bunch of addresses, whether they share the same default
+# value or not.
+
+from enum import Enum
+from collections import namedtuple
+
+
+class Tags(Enum):
+ LOOPED = 0
+ QUIET = 1
+ IMMEDIATE = 2 # Delayed sounds are commonly undesirable
+ BRIEF = 3 # Punchy sounds, good for rapid fire
+ NEW = 4
+ PAINFUL = 5 # Eardrum-piercing sounds
+ NAVI = 6 # Navi sounds (hand chosen)
+ HPLOW = 7 # Low HP sounds (hand chosen)
+ HOVERBOOT = 8 # Hover boot sounds (hand chosen)
+ NIGHTFALL = 9 # Nightfall sounds (hand chosen)
+ MENUSELECT = 10 # Menu selection sounds (hand chosen, could use some more)
+ MENUMOVE = 11 # Menu movement sounds (hand chosen, could use some more)
+ HORSE = 12 # Horse neigh sounds (hand chosen)
+ INC_NE = 20 # Incompatible with NAVI_ENEMY? (Verify)
+ # I'm now thinking it has to do with a limit of concurrent sounds)
+
+Sound = namedtuple('Sound', 'id keyword label tags')
+class Sounds(Enum):
+ NONE = Sound(0x0000, 'none', 'None', [Tags.NAVI, Tags.HPLOW])
+ ARMOS_GROAN = Sound(0x3848, 'armos', 'Armos', [Tags.HORSE, Tags.PAINFUL])
+ BARK = Sound(0x28D8, 'bark', 'Bark', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW, Tags.HOVERBOOT])
+ BOMB_BOUNCE = Sound(0x282F, 'bomb-bounce', 'Bomb Bounce', [Tags.QUIET, Tags.HPLOW])
+ BONGO_HIGH = Sound(0x3951, 'bongo-bongo-high', 'Bongo Bongo High', [Tags.MENUSELECT])
+ BONGO_LOW = Sound(0x3950, 'bongo-bongo-low', 'Bongo Bongo Low', [Tags.QUIET, Tags.HPLOW, Tags.MENUMOVE])
+ BOTTLE_CORK = Sound(0x286C, 'bottle-cork', 'Bottle Cork', [Tags.IMMEDIATE, Tags.BRIEF, Tags.QUIET])
+ BOW_TWANG = Sound(0x1830, 'bow-twang', 'Bow Twang', [Tags.HPLOW, Tags.MENUMOVE])
+ BUBBLE_LOL = Sound(0x38CA, 'bubble-laugh', 'Bubble Laugh', [])
+ BUSINESS_SCRUB = Sound(0x3882, 'business-scrub', 'Business Scrub', [Tags.PAINFUL, Tags.NAVI, Tags.HPLOW])
+ CARROT_REFILL = Sound(0x4845, 'carrot-refill', 'Carrot Refill', [Tags.NAVI, Tags.HPLOW])
+ CARTOON_FALL = Sound(0x28A0, 'cartoon-fall', 'Cartoon Fall', [Tags.PAINFUL, Tags.HOVERBOOT])
+ CHANGE_ITEM = Sound(0x0835, 'change-item', 'Change Item', [Tags.IMMEDIATE, Tags.BRIEF, Tags.MENUSELECT])
+ CHEST_OPEN = Sound(0x2820, 'chest-open', 'Chest Open', [Tags.PAINFUL])
+ CHILD_CRINGE = Sound(0x683A, 'child-cringe', 'Child Cringe', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.MENUSELECT])
+ CHILD_GASP = Sound(0x6836, 'child-gasp', 'Child Gasp', [Tags.PAINFUL])
+ CHILD_HURT = Sound(0x6825, 'child-hurt', 'Child Hurt', [Tags.PAINFUL])
+ CHILD_OWO = Sound(0x6823, 'child-owo', 'Child owo', [Tags.PAINFUL])
+ CHILD_PANT = Sound(0x6829, 'child-pant', 'Child Pant', [Tags.IMMEDIATE])
+ CHILD_SCREAM = Sound(0x6828, 'child-scream', 'Child Scream', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.MENUSELECT, Tags.HORSE])
+ CUCCO_CLUCK = Sound(0x2812, 'cluck', 'Cluck', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW])
+ CUCCO_CROW = Sound(0x2813, 'cockadoodledoo', 'Cockadoodledoo', [Tags.PAINFUL, Tags.NAVI, Tags.NIGHTFALL])
+ CURSED_ATTACK = Sound(0x6868, 'cursed-attack', 'Cursed Attack', [Tags.PAINFUL, Tags.IMMEDIATE])
+ CURSED_SCREAM = Sound(0x6867, 'cursed-scream', 'Cursed Scream', [Tags.PAINFUL])
+ DEKU_BABA_CHATTER = Sound(0x3860, 'deku-baba', 'Deku Baba', [Tags.MENUMOVE])
+ DRAWBRIDGE_SET = Sound(0x280E, 'drawbridge-set', 'Drawbridge Set', [Tags.HPLOW])
+ DUSK_HOWL = Sound(0x28AE, 'dusk-howl', 'Dusk Howl', [Tags.NAVI])
+ EPONA_CHILD = Sound(0x2844, 'baby-epona', 'Epona (Baby)', [Tags.PAINFUL])
+ EXPLODE_CRATE = Sound(0x2839, 'exploding-crate', 'Exploding Crate', [Tags.PAINFUL, Tags.NAVI])
+ EXPLOSION = Sound(0x180E, 'explosion', 'Explosion', [Tags.PAINFUL, Tags.NAVI])
+ FANFARE_SMALL = Sound(0x4824, 'fanfare-light', 'Fanfare (Light)', [])
+ FANFARE_MED = Sound(0x4831, 'fanfare-medium', 'Fanfare (Medium)', [])
+ FIELD_SHRUB = Sound(0x2877, 'field-shrub', 'Field Shrub', [])
+ FLARE_BOSS_LOL = Sound(0x3981, 'flare-dancer-laugh', 'Flare Dancer Laugh', [Tags.PAINFUL, Tags.IMMEDIATE, Tags.HOVERBOOT])
+ FLARE_BOSS_STARTLE = Sound(0x398B, 'flare-dancer-startled', 'Flare Dancer Startled', [])
+ GANON_TENNIS = Sound(0x39CA, 'ganondorf-teh', 'Ganondorf "Teh!"', [])
+ GOHMA_LARVA_CROAK = Sound(0x395D, 'gohma-larva-croak', 'Gohma Larva Croak', [])
+ GOLD_SKULL_TOKEN = Sound(0x4843, 'gold-skull-token', 'Gold Skull Token', [Tags.NIGHTFALL])
+ GORON_WAKE = Sound(0x38FC, 'goron-wake', 'Goron Wake', [])
+ GREAT_FAIRY = Sound(0x6858, 'great-fairy', 'Great Fairy', [Tags.PAINFUL, Tags.NAVI, Tags.NIGHTFALL, Tags.HORSE])
+ GUAY = Sound(0x38B6, 'guay', 'Guay', [Tags.BRIEF, Tags.NAVI, Tags.HPLOW])
+ GUNSHOT = Sound(0x4835, 'gunshot', 'Gunshot', [])
+ HAMMER_BONK = Sound(0x180A, 'hammer-bonk', 'Hammer Bonk', [])
+ HORSE_NEIGH = Sound(0x2805, 'horse-neigh', 'Horse Neigh', [Tags.PAINFUL, Tags.NAVI])
+ HORSE_TROT = Sound(0x2804, 'horse-trot', 'Horse Trot', [Tags.HPLOW])
+ HOVER_BOOTS = Sound(0x08C9, 'hover-boots', 'Hover Boots', [Tags.LOOPED, Tags.PAINFUL])
+ HP_LOW = Sound(0x481B, 'low-health', 'HP Low', [Tags.INC_NE, Tags.NAVI])
+ HP_RECOVER = Sound(0x480B, 'recover-health', 'HP Recover', [Tags.NAVI, Tags.HPLOW])
+ ICE_SHATTER = Sound(0x0875, 'shattering-ice', 'Ice Shattering', [Tags.PAINFUL, Tags.NAVI])
+ INGO_WOOAH = Sound(0x6854, 'ingo-wooah', 'Ingo "Wooah!"', [Tags.PAINFUL])
+ IRON_BOOTS = Sound(0x080D, 'iron-boots', 'Iron Boots', [Tags.BRIEF, Tags.HPLOW, Tags.QUIET])
+ IRON_KNUCKLE = Sound(0x3929, 'iron-knuckle', 'Iron Knuckle', [])
+ INGO_KAAH = Sound(0x6855, 'kaah', 'Kaah!', [Tags.PAINFUL])
+ MOBLIN_CLUB_GROUND = Sound(0x38E1, 'moblin-club-ground', 'Moblin Club Ground', [Tags.PAINFUL])
+ MOBLIN_CLUB_SWING = Sound(0x39EF, 'moblin-club-swing', 'Moblin Club Swing', [Tags.PAINFUL])
+ MOO = Sound(0x28DF, 'moo', 'Moo', [Tags.NAVI, Tags.NIGHTFALL, Tags.HORSE, Tags.HPLOW])
+ MWEEP = Sound(0x687A, 'mweep', 'Mweep!', [Tags.BRIEF, Tags.NAVI, Tags.MENUMOVE, Tags.MENUSELECT, Tags.NIGHTFALL, Tags.HPLOW, Tags.HORSE, Tags.HOVERBOOT])
+ NAVI_HELLO = Sound(0x6844, 'navi-hello', 'Navi "Hello!"', [Tags.PAINFUL, Tags.NAVI])
+ NAVI_HEY = Sound(0x685F, 'navi-hey', 'Navi "Hey!"', [Tags.PAINFUL, Tags.HPLOW])
+ NAVI_RANDOM = Sound(0x6843, 'navi-random', 'Navi Random', [Tags.PAINFUL, Tags.HPLOW])
+ NOTIFICATION = Sound(0x4820, 'notification', 'Notification', [Tags.NAVI, Tags.HPLOW])
+ PHANTOM_GANON_LOL = Sound(0x38B0, 'phantom-ganon-laugh', 'Phantom Ganon Laugh', [])
+ PLANT_EXPLODE = Sound(0x284E, 'plant-explode', 'Plant Explode', [])
+ POE = Sound(0x38EC, 'poe', 'Poe', [Tags.PAINFUL, Tags.NAVI])
+ POT_SHATTER = Sound(0x2887, 'shattering-pot', 'Pot Shattering', [Tags.NAVI, Tags.HPLOW])
+ REDEAD_MOAN = Sound(0x38E4, 'redead-moan', 'Redead Moan', [Tags.NIGHTFALL])
+ REDEAD_SCREAM = Sound(0x38E5, 'redead-scream', 'Redead Scream', [Tags.PAINFUL, Tags.NAVI, Tags.HORSE])
+ RIBBIT = Sound(0x28B1, 'ribbit', 'Ribbit', [Tags.NAVI, Tags.HPLOW])
+ RUPEE = Sound(0x4803, 'rupee', 'Rupee', [])
+ RUPEE_SILVER = Sound(0x28E8, 'silver-rupee', 'Rupee (Silver)', [Tags.HPLOW])
+ RUTO_CHILD_CRASH = Sound(0x6860, 'ruto-crash', 'Ruto Crash', [])
+ RUTO_CHILD_EXCITED = Sound(0x6861, 'ruto-excited', 'Ruto Excited', [Tags.PAINFUL])
+ RUTO_CHILD_GIGGLE = Sound(0x6863, 'ruto-giggle', 'Ruto Giggle', [Tags.PAINFUL, Tags.NAVI])
+ RUTO_CHILD_LIFT = Sound(0x6864, 'ruto-lift', 'Ruto Lift', [])
+ RUTO_CHILD_THROWN = Sound(0x6865, 'ruto-thrown', 'Ruto Thrown', [])
+ RUTO_CHILD_WIGGLE = Sound(0x6866, 'ruto-wiggle', 'Ruto Wiggle', [Tags.PAINFUL, Tags.HORSE])
+ SCRUB_NUTS_UP = Sound(0x387C, 'scrub-emerge', 'Scrub Emerge', [])
+ SHABOM_BOUNCE = Sound(0x3948, 'shabom-bounce', 'Shabom Bounce', [Tags.IMMEDIATE])
+ SHABOM_POP = Sound(0x3949, 'shabom-pop', 'Shabom Pop', [Tags.IMMEDIATE, Tags.BRIEF, Tags.HOVERBOOT])
+ SHELLBLADE = Sound(0x3849, 'shellblade', 'Shellblade', [])
+ SKULLTULA = Sound(0x39DA, 'skulltula', 'Skulltula', [Tags.BRIEF, Tags.NAVI])
+ SOFT_BEEP = Sound(0x4804, 'soft-beep', 'Soft Beep', [Tags.NAVI, Tags.HPLOW])
+ SPIKE_TRAP = Sound(0x38E9, 'spike-trap', 'Spike Trap', [Tags.LOOPED, Tags.PAINFUL])
+ SPIT_NUT = Sound(0x387E, 'spit-nut', 'Spit Nut', [Tags.IMMEDIATE, Tags.BRIEF])
+ STALCHILD_ATTACK = Sound(0x3831, 'stalchild-attack', 'Stalchild Attack', [Tags.PAINFUL, Tags.HORSE])
+ STINGER_CRY = Sound(0x39A3, 'stinger-squeak', 'Stinger Squeak', [Tags.PAINFUL])
+ SWITCH = Sound(0x2815, 'switch', 'Switch', [Tags.HPLOW])
+ SWORD_BONK = Sound(0x181A, 'sword-bonk', 'Sword Bonk', [Tags.HPLOW])
+ TALON_CRY = Sound(0x6853, 'talon-cry', 'Talon Cry', [Tags.PAINFUL])
+ TALON_HMM = Sound(0x6852, 'talon-hmm', 'Talon "Hmm"', [])
+ TALON_SNORE = Sound(0x6850, 'talon-snore', 'Talon Snore', [Tags.NIGHTFALL])
+ TALON_WTF = Sound(0x6851, 'talon-wtf', 'Talon Wtf', [])
+ TAMBOURINE = Sound(0x4842, 'tambourine', 'Tambourine', [Tags.QUIET, Tags.NAVI, Tags.HPLOW, Tags.HOVERBOOT])
+ TARGETING_ENEMY = Sound(0x4830, 'target-enemy', 'Target Enemy', [])
+ TARGETING_NEUTRAL = Sound(0x480C, 'target-neutral', 'Target Neutral', [])
+ THUNDER = Sound(0x282E, 'thunder', 'Thunder', [Tags.NIGHTFALL])
+ TIMER = Sound(0x481A, 'timer', 'Timer', [Tags.INC_NE, Tags.NAVI, Tags.HPLOW])
+ TWINROVA_BICKER = Sound(0x39E7, 'twinrova-bicker', 'Twinrova Bicker', [Tags.LOOPED, Tags.PAINFUL])
+ WOLFOS_HOWL = Sound(0x383C, 'wolfos-howl', 'Wolfos Howl', [Tags.PAINFUL])
+ ZELDA_ADULT_GASP = Sound(0x6879, 'adult-zelda-gasp', 'Zelda Gasp (Adult)', [Tags.NAVI, Tags.HPLOW])
+
+
+# Sound pools
+standard = [s for s in Sounds if Tags.LOOPED not in s.value.tags]
+looping = [s for s in Sounds if Tags.LOOPED in s.value.tags]
+no_painful = [s for s in standard if Tags.PAINFUL not in s.value.tags]
+navi = [s for s in Sounds if Tags.NAVI in s.value.tags]
+hp_low = [s for s in Sounds if Tags.HPLOW in s.value.tags]
+hover_boots = [s for s in Sounds if Tags.HOVERBOOT in s.value.tags]
+nightfall = [s for s in Sounds if Tags.NIGHTFALL in s.value.tags]
+menu_select = [s for s in Sounds if Tags.MENUSELECT in s.value.tags]
+menu_cursor = [s for s in Sounds if Tags.MENUMOVE in s.value.tags]
+horse_neigh = [s for s in Sounds if Tags.HORSE in s.value.tags]
+
+
+SoundHook = namedtuple('SoundHook', 'name pool locations')
+class SoundHooks(Enum):
+ NAVI_OVERWORLD = SoundHook('Navi - Overworld', navi, [0xAE7EF2, 0xC26C7E])
+ NAVI_ENEMY = SoundHook('Navi - Enemy', navi, [0xAE7EC6])
+ HP_LOW = SoundHook('Low Health', hp_low, [0xADBA1A])
+ BOOTS_HOVER = SoundHook('Hover Boots', hover_boots, [0xBDBD8A])
+ NIGHTFALL = SoundHook('Nightfall', nightfall, [0xAD3466, 0xAD7A2E])
+ MENU_SELECT = SoundHook('Menu Select', no_painful + menu_select, [
+ 0xBA1BBE, 0xBA23CE, 0xBA2956, 0xBA321A, 0xBA72F6, 0xBA8106, 0xBA82EE,
+ 0xBA9DAE, 0xBA9EAE, 0xBA9FD2, 0xBAE6D6])
+ MENU_CURSOR = SoundHook('Menu Cursor', no_painful + menu_cursor, [
+ 0xBA165E, 0xBA1C1A, 0xBA2406, 0xBA327E, 0xBA3936, 0xBA77C2, 0xBA7886,
+ 0xBA7A06, 0xBA7A6E, 0xBA7AE6, 0xBA7D6A, 0xBA8186, 0xBA822E, 0xBA82A2,
+ 0xBAA11E, 0xBAE7C6])
+ HORSE_NEIGH = SoundHook('Horse Neigh', horse_neigh, [
+ 0xC18832, 0xC18C32, 0xC19A7E, 0xC19CBE, 0xC1A1F2, 0xC1A3B6, 0xC1B08A,
+ 0xC1B556, 0xC1C28A, 0xC1CC36, 0xC1EB4A, 0xC1F18E, 0xC6B136, 0xC6BBA2,
+ 0xC1E93A, 0XC6B366, 0XC6B562])
+
+
+# # Some enemies have a different cutting sound, making this a bit weird
+# SWORD_SLASH = SoundHook('Sword Slash', standard, [0xAC2942])
+
+
+def get_patch_dict():
+ return {s.value.keyword: s.value.id for s in Sounds}
+
+
+def get_hook_pool(sound_hook, earsafeonly = "FALSE"):
+ if earsafeonly == "TRUE":
+ list = [s for s in sound_hook.value.pool if Tags.PAINFUL not in s.value.tags]
+ return list
+ else:
+ return sound_hook.value.pool
+
+
+def get_setting_choices(sound_hook):
+ pool = sound_hook.value.pool
+ choices = {s.value.keyword: s.value.label for s in sorted(pool, key=lambda s: s.value.label)}
+ result = {
+ 'default': 'Default',
+ 'completely-random': 'Completely Random',
+ 'random-ear-safe': 'Random Ear-Safe',
+ 'random-choice': 'Random Choice',
+ 'none': 'None',
+ **choices,
+ }
+ return result
diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py
new file mode 100644
index 00000000..4ea99757
--- /dev/null
+++ b/worlds/oot/TextBox.py
@@ -0,0 +1,369 @@
+import worlds.oot.Messages as Messages
+
+# Least common multiple of all possible character widths. A line wrap must occur when the combined widths of all of the
+# characters on a line reach this value.
+NORMAL_LINE_WIDTH = 1801800
+
+# Attempting to display more lines in a single text box will cause additional lines to bleed past the bottom of the box.
+LINES_PER_BOX = 4
+
+# Attempting to display more characters in a single text box will cause buffer overflows. First, visual artifacts will
+# appear in lower areas of the text box. Eventually, the text box will become uncloseable.
+MAX_CHARACTERS_PER_BOX = 200
+
+CONTROL_CHARS = {
+ 'LINE_BREAK': ['&', '\x01'],
+ 'BOX_BREAK': ['^', '\x04'],
+ 'NAME': ['@', '\x0F'],
+ 'COLOR': ['#', '\x05\x00'],
+}
+TEXT_END = '\x02'
+
+
+def line_wrap(text, strip_existing_lines=False, strip_existing_boxes=False, replace_control_chars=True):
+ # Replace stand-in characters with their actual control code.
+ if replace_control_chars:
+ for char in CONTROL_CHARS.values():
+ text = text.replace(char[0], char[1])
+
+ # Parse the text into a list of control codes.
+ text_codes = Messages.parse_control_codes(text)
+
+ # Existing line/box break codes to strip.
+ strip_codes = []
+ if strip_existing_boxes:
+ strip_codes.append(0x04)
+ if strip_existing_lines:
+ strip_codes.append(0x01)
+
+ # Replace stripped codes with a space.
+ if strip_codes:
+ index = 0
+ while index < len(text_codes):
+ text_code = text_codes[index]
+ if text_code.code in strip_codes:
+ # Check for existing whitespace near this control code.
+ # If one is found, simply remove this text code.
+ if index > 0 and text_codes[index-1].code == 0x20:
+ text_codes.pop(index)
+ continue
+ if index + 1 < len(text_codes) and text_codes[index+1].code == 0x20:
+ text_codes.pop(index)
+ continue
+ # Replace this text code with a space.
+ text_codes[index] = Messages.Text_Code(0x20, 0)
+ index += 1
+
+ # Split the text codes by current box breaks.
+ boxes = []
+ start_index = 0
+ end_index = 0
+ for text_code in text_codes:
+ end_index += 1
+ if text_code.code == 0x04:
+ boxes.append(text_codes[start_index:end_index])
+ start_index = end_index
+ boxes.append(text_codes[start_index:end_index])
+
+ # Split the boxes into lines and words.
+ processed_boxes = []
+ for box_codes in boxes:
+ line_width = NORMAL_LINE_WIDTH
+ icon_code = None
+ words = []
+
+ # Group the text codes into words.
+ index = 0
+ while index < len(box_codes):
+ text_code = box_codes[index]
+ index += 1
+
+ # Check for an icon code and lower the width of this box if one is found.
+ if text_code.code == 0x13:
+ line_width = 1441440
+ icon_code = text_code
+
+ # Find us a whole word.
+ if text_code.code in [0x01, 0x04, 0x20]:
+ if index > 1:
+ words.append(box_codes[0:index-1])
+ if text_code.code in [0x01, 0x04]:
+ # If we have ran into a line or box break, add it as a "word" as well.
+ words.append([box_codes[index-1]])
+ box_codes = box_codes[index:]
+ index = 0
+ if index > 0 and index == len(box_codes):
+ words.append(box_codes)
+ box_codes = []
+
+ # Arrange our words into lines.
+ lines = []
+ start_index = 0
+ end_index = 0
+ box_count = 1
+ while end_index < len(words):
+ # Our current confirmed line.
+ end_index += 1
+ line = words[start_index:end_index]
+
+ # If this word is a line/box break, trim our line back a word and deal with it later.
+ break_char = False
+ if words[end_index-1][0].code in [0x01, 0x04]:
+ line = words[start_index:end_index-1]
+ break_char = True
+
+ # Check the width of the line after adding one more word.
+ if end_index == len(words) or break_char or calculate_width(words[start_index:end_index+1]) > line_width:
+ if line or lines:
+ lines.append(line)
+ start_index = end_index
+
+ # If we've reached the end of the box, finalize it.
+ if end_index == len(words) or words[end_index-1][0].code == 0x04 or len(lines) == LINES_PER_BOX:
+ # Append the same icon to any wrapped boxes.
+ if icon_code and box_count > 1:
+ lines[0][0] = [icon_code] + lines[0][0]
+ processed_boxes.append(lines)
+ lines = []
+ box_count += 1
+
+ # Construct our final string.
+ # This is a hideous level of list comprehension. Sorry.
+ return '\x04'.join('\x01'.join(' '.join(''.join(code.get_string() for code in word) for word in line) for line in box) for box in processed_boxes)
+
+
+def calculate_width(words):
+ words_width = 0
+ for word in words:
+ index = 0
+ while index < len(word):
+ character = word[index]
+ index += 1
+ if character.code in Messages.CONTROL_CODES:
+ if character.code == 0x06:
+ words_width += character.data
+ words_width += get_character_width(chr(character.code))
+ spaces_width = get_character_width(' ') * (len(words) - 1)
+
+ return words_width + spaces_width
+
+
+def get_character_width(character):
+ try:
+ return character_table[character]
+ except KeyError:
+ if ord(character) < 0x20:
+ if character in control_code_width:
+ return sum([character_table[c] for c in control_code_width[character]])
+ else:
+ return 0
+ else:
+ # A sane default with the most common character width
+ return character_table[' ']
+
+
+control_code_width = {
+ '\x0F': '00000000',
+ '\x16': '00\'00"',
+ '\x17': '00\'00"',
+ '\x18': '00000',
+ '\x19': '100',
+ '\x1D': '00',
+ '\x1E': '00000',
+ '\x1F': '00\'00"',
+}
+
+
+# Tediously measured by filling a full line of a gossip stone's text box with one character until it is reasonably full
+# (with a right margin) and counting how many characters fit. OoT does not appear to use any kerning, but, if it does,
+# it will only make the characters more space-efficient, so this is an underestimate of the number of letters per line,
+# at worst. This ensures that we will never bleed text out of the text box while line wrapping.
+# Larger numbers in the denominator mean more of that character fits on a line; conversely, larger values in this table
+# mean the character is wider and can't fit as many on one line.
+character_table = {
+ '\x0F': 655200,
+ '\x16': 292215,
+ '\x17': 292215,
+ '\x18': 300300,
+ '\x19': 145860,
+ '\x1D': 85800,
+ '\x1E': 300300,
+ '\x1F': 265980,
+ 'a': 51480, # LINE_WIDTH / 35
+ 'b': 51480, # LINE_WIDTH / 35
+ 'c': 51480, # LINE_WIDTH / 35
+ 'd': 51480, # LINE_WIDTH / 35
+ 'e': 51480, # LINE_WIDTH / 35
+ 'f': 34650, # LINE_WIDTH / 52
+ 'g': 51480, # LINE_WIDTH / 35
+ 'h': 51480, # LINE_WIDTH / 35
+ 'i': 25740, # LINE_WIDTH / 70
+ 'j': 34650, # LINE_WIDTH / 52
+ 'k': 51480, # LINE_WIDTH / 35
+ 'l': 25740, # LINE_WIDTH / 70
+ 'm': 81900, # LINE_WIDTH / 22
+ 'n': 51480, # LINE_WIDTH / 35
+ 'o': 51480, # LINE_WIDTH / 35
+ 'p': 51480, # LINE_WIDTH / 35
+ 'q': 51480, # LINE_WIDTH / 35
+ 'r': 42900, # LINE_WIDTH / 42
+ 's': 51480, # LINE_WIDTH / 35
+ 't': 42900, # LINE_WIDTH / 42
+ 'u': 51480, # LINE_WIDTH / 35
+ 'v': 51480, # LINE_WIDTH / 35
+ 'w': 81900, # LINE_WIDTH / 22
+ 'x': 51480, # LINE_WIDTH / 35
+ 'y': 51480, # LINE_WIDTH / 35
+ 'z': 51480, # LINE_WIDTH / 35
+ 'A': 81900, # LINE_WIDTH / 22
+ 'B': 51480, # LINE_WIDTH / 35
+ 'C': 72072, # LINE_WIDTH / 25
+ 'D': 72072, # LINE_WIDTH / 25
+ 'E': 51480, # LINE_WIDTH / 35
+ 'F': 51480, # LINE_WIDTH / 35
+ 'G': 81900, # LINE_WIDTH / 22
+ 'H': 60060, # LINE_WIDTH / 30
+ 'I': 25740, # LINE_WIDTH / 70
+ 'J': 51480, # LINE_WIDTH / 35
+ 'K': 60060, # LINE_WIDTH / 30
+ 'L': 51480, # LINE_WIDTH / 35
+ 'M': 81900, # LINE_WIDTH / 22
+ 'N': 72072, # LINE_WIDTH / 25
+ 'O': 81900, # LINE_WIDTH / 22
+ 'P': 51480, # LINE_WIDTH / 35
+ 'Q': 81900, # LINE_WIDTH / 22
+ 'R': 60060, # LINE_WIDTH / 30
+ 'S': 60060, # LINE_WIDTH / 30
+ 'T': 51480, # LINE_WIDTH / 35
+ 'U': 60060, # LINE_WIDTH / 30
+ 'V': 72072, # LINE_WIDTH / 25
+ 'W': 100100, # LINE_WIDTH / 18
+ 'X': 72072, # LINE_WIDTH / 25
+ 'Y': 60060, # LINE_WIDTH / 30
+ 'Z': 60060, # LINE_WIDTH / 30
+ ' ': 51480, # LINE_WIDTH / 35
+ '1': 25740, # LINE_WIDTH / 70
+ '2': 51480, # LINE_WIDTH / 35
+ '3': 51480, # LINE_WIDTH / 35
+ '4': 60060, # LINE_WIDTH / 30
+ '5': 51480, # LINE_WIDTH / 35
+ '6': 51480, # LINE_WIDTH / 35
+ '7': 51480, # LINE_WIDTH / 35
+ '8': 51480, # LINE_WIDTH / 35
+ '9': 51480, # LINE_WIDTH / 35
+ '0': 60060, # LINE_WIDTH / 30
+ '!': 51480, # LINE_WIDTH / 35
+ '?': 72072, # LINE_WIDTH / 25
+ '\'': 17325, # LINE_WIDTH / 104
+ '"': 34650, # LINE_WIDTH / 52
+ '.': 25740, # LINE_WIDTH / 70
+ ',': 25740, # LINE_WIDTH / 70
+ '/': 51480, # LINE_WIDTH / 35
+ '-': 34650, # LINE_WIDTH / 52
+ '_': 51480, # LINE_WIDTH / 35
+ '(': 42900, # LINE_WIDTH / 42
+ ')': 42900, # LINE_WIDTH / 42
+ '$': 51480 # LINE_WIDTH / 35
+}
+
+# To run tests, enter the following into a python3 REPL:
+# >>> import Messages
+# >>> from TextBox import line_wrap_tests
+# >>> line_wrap_tests()
+def line_wrap_tests():
+ test_wrap_simple_line()
+ test_honor_forced_line_wraps()
+ test_honor_box_breaks()
+ test_honor_control_characters()
+ test_honor_player_name()
+ test_maintain_multiple_forced_breaks()
+ test_trim_whitespace()
+ test_support_long_words()
+
+
+def test_wrap_simple_line():
+ words = 'Hello World! Hello World! Hello World!'
+ expected = 'Hello World! Hello World! Hello\x01World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Wrap Simple Line" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Wrap Simple Line" test passed!')
+
+
+def test_honor_forced_line_wraps():
+ words = 'Hello World! Hello World!&Hello World! Hello World! Hello World!'
+ expected = 'Hello World! Hello World!\x01Hello World! Hello World! Hello\x01World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Honor Forced Line Wraps" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Honor Forced Line Wraps" test passed!')
+
+
+def test_honor_box_breaks():
+ words = 'Hello World! Hello World!^Hello World! Hello World! Hello World!'
+ expected = 'Hello World! Hello World!\x04Hello World! Hello World! Hello\x01World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Honor Box Breaks" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Honor Box Breaks" test passed!')
+
+
+def test_honor_control_characters():
+ words = 'Hello World! #Hello# World! Hello World!'
+ expected = 'Hello World! \x05\x00Hello\x05\x00 World! Hello\x01World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Honor Control Characters" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Honor Control Characters" test passed!')
+
+
+def test_honor_player_name():
+ words = 'Hello @! Hello World! Hello World!'
+ expected = 'Hello \x0F! Hello World!\x01Hello World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Honor Player Name" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Honor Player Name" test passed!')
+
+
+def test_maintain_multiple_forced_breaks():
+ words = 'Hello World!&&&Hello World!'
+ expected = 'Hello World!\x01\x01\x01Hello World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Maintain Multiple Forced Breaks" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Maintain Multiple Forced Breaks" test passed!')
+
+
+def test_trim_whitespace():
+ words = 'Hello World! & Hello World!'
+ expected = 'Hello World!\x01Hello World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Trim Whitespace" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Trim Whitespace" test passed!')
+
+
+def test_support_long_words():
+ words = 'Hello World! WWWWWWWWWWWWWWWWWWWW Hello World!'
+ expected = 'Hello World!\x01WWWWWWWWWWWWWWWWWWWW\x01Hello World!'
+ result = line_wrap(words)
+
+ if result != expected:
+ print('"Support Long Words" test failed: Got ' + result + ', wanted ' + expected)
+ else:
+ print('"Support Long Words" test passed!')
diff --git a/worlds/oot/Utils.py b/worlds/oot/Utils.py
new file mode 100644
index 00000000..405cdd4d
--- /dev/null
+++ b/worlds/oot/Utils.py
@@ -0,0 +1,97 @@
+import io, re, json
+import os, sys
+import subprocess
+import Utils
+from functools import lru_cache
+
+__version__ = Utils.__version__ + ' f.LUM'
+
+def data_path(*args):
+ return Utils.local_path('worlds', 'oot', 'data', *args)
+
+@lru_cache(maxsize=13) # Cache Overworld.json and the 12 dungeons
+def read_json(file_path):
+ json_string = ""
+ with io.open(file_path, 'r') as file:
+ for line in file.readlines():
+ json_string += line.split('#')[0].replace('\n', ' ')
+ json_string = re.sub(' +', ' ', json_string)
+ try:
+ return json.loads(json_string)
+ except json.JSONDecodeError as error:
+ raise Exception("JSON parse error around text:\n" + \
+ json_string[error.pos-35:error.pos+35] + "\n" + \
+ " ^^\n")
+
+def is_bundled():
+ return getattr(sys, 'frozen', False)
+
+# From the pyinstaller Wiki: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-subprocess
+# Create a set of arguments which make a ``subprocess.Popen`` (and
+# variants) call work with or without Pyinstaller, ``--noconsole`` or
+# not, on Windows and Linux. Typical use::
+# subprocess.call(['program_to_run', 'arg_1'], **subprocess_args())
+def subprocess_args(include_stdout=True):
+ # The following is true only on Windows.
+ if hasattr(subprocess, 'STARTUPINFO'):
+ # On Windows, subprocess calls will pop up a command window by default
+ # when run from Pyinstaller with the ``--noconsole`` option. Avoid this
+ # distraction.
+ si = subprocess.STARTUPINFO()
+ si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+ # Windows doesn't search the path by default. Pass it an environment so
+ # it will.
+ env = os.environ
+ else:
+ si = None
+ env = None
+
+ # ``subprocess.check_output`` doesn't allow specifying ``stdout``::
+ # So, add it only if it's needed.
+ if include_stdout:
+ ret = {'stdout': subprocess.PIPE}
+ else:
+ ret = {}
+
+ # On Windows, running this from the binary produced by Pyinstaller
+ # with the ``--noconsole`` option requires redirecting everything
+ # (stdin, stdout, stderr) to avoid an OSError exception
+ # "[Error 6] the handle is invalid."
+ ret.update({'stdin': subprocess.PIPE,
+ 'stderr': subprocess.PIPE,
+ 'startupinfo': si,
+ 'env': env })
+ return ret
+
+def get_version_bytes(a):
+ version_bytes = [0x00, 0x00, 0x00]
+ if not a:
+ return version_bytes;
+ sa = a.replace('v', '').replace(' ', '.').split('.')
+
+ for i in range(0,3):
+ try:
+ version_byte = int(sa[i])
+ except ValueError:
+ break
+ version_bytes[i] = version_byte
+
+ return version_bytes
+
+def compare_version(a, b):
+ if not a and not b:
+ return 0
+ elif a and not b:
+ return 1
+ elif not a and b:
+ return -1
+
+ sa = get_version_bytes(a)
+ sb = get_version_bytes(b)
+
+ for i in range(0,3):
+ if sa[i] > sb[i]:
+ return 1
+ if sa[i] < sb[i]:
+ return -1
+ return 0
diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py
new file mode 100644
index 00000000..00bd0215
--- /dev/null
+++ b/worlds/oot/__init__.py
@@ -0,0 +1,734 @@
+import logging
+import os
+import copy
+from collections import Counter
+
+logger = logging.getLogger("Ocarina of Time")
+
+from .Location import OOTLocation, LocationFactory, location_name_to_id
+from .Entrance import OOTEntrance
+from .EntranceShuffle import shuffle_random_entrances
+from .Items import OOTItem, item_table, oot_data_to_ap_id
+from .ItemPool import generate_itempool, get_junk_item, get_junk_pool
+from .Regions import OOTRegion, TimeOfDay
+from .Rules import set_rules, set_shop_rules, set_entrances_based_rules
+from .RuleParser import Rule_AST_Transformer
+from .Options import oot_options
+from .Utils import data_path, read_json
+from .LocationList import business_scrubs, set_drop_location_names
+from .DungeonList import dungeon_table, create_dungeons
+from .LogicTricks import normalized_name_tricks
+from .Rom import Rom
+from .Patches import patch_rom
+from .N64Patch import create_patch_file
+from .Cosmetics import patch_cosmetics
+from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
+from .HintList import getRequiredHints
+
+from Utils import get_options, output_path
+from BaseClasses import MultiWorld, CollectionState, RegionType
+from Options import Range, Toggle, OptionList
+from Fill import fill_restrictive, FillError
+from ..AutoWorld import World
+
+location_id_offset = 67000
+
+class OOTWorld(World):
+ game: str = "Ocarina of Time"
+ options: dict = oot_options
+ topology_present: bool = True
+ item_name_to_id = {item_name: oot_data_to_ap_id(data, False) for item_name, data in item_table.items() if data[2] is not None}
+ location_name_to_id = location_name_to_id
+ remote_items: bool = False
+
+ data_version = 1
+
+
+ def __new__(cls, world, player):
+ # Add necessary objects to CollectionState on initialization
+ orig_init = CollectionState.__init__
+ orig_copy = CollectionState.copy
+
+ def oot_init(self, parent: MultiWorld):
+ orig_init(self, parent)
+ self.child_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
+ self.adult_reachable_regions = {player: set() for player in range(1, parent.players + 1)}
+ self.child_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
+ self.adult_blocked_connections = {player: set() for player in range(1, parent.players + 1)}
+ self.age = {player: None for player in range(1, parent.players + 1)}
+
+ def oot_copy(self):
+ ret = orig_copy(self)
+ ret.child_reachable_regions = {player: copy.copy(self.child_reachable_regions[player]) for player in
+ range(1, self.world.players + 1)}
+ ret.adult_reachable_regions = {player: copy.copy(self.adult_reachable_regions[player]) for player in
+ range(1, self.world.players + 1)}
+ ret.child_blocked_connections = {player: copy.copy(self.child_blocked_connections[player]) for player in
+ range(1, self.world.players + 1)}
+ ret.adult_blocked_connections = {player: copy.copy(self.adult_blocked_connections[player]) for player in
+ range(1, self.world.players + 1)}
+ return ret
+
+ CollectionState.__init__ = oot_init
+ CollectionState.copy = oot_copy
+ # also need to add the names to the passed MultiWorld's CollectionState, since it was initialized before we could get to it
+ if world:
+ world.state.child_reachable_regions = {player: set() for player in range(1, world.players + 1)}
+ world.state.adult_reachable_regions = {player: set() for player in range(1, world.players + 1)}
+ world.state.child_blocked_connections = {player: set() for player in range(1, world.players + 1)}
+ world.state.adult_blocked_connections = {player: set() for player in range(1, world.players + 1)}
+ world.state.age = {player: None for player in range(1, world.players + 1)}
+
+ return super().__new__(cls)
+
+
+ def generate_early(self):
+ # Player name MUST be at most 16 bytes ascii-encoded, otherwise won't write to ROM correctly
+ if len(bytes(self.world.get_player_name(self.player), 'ascii')) > 16:
+ raise Exception(f"OoT: Player {self.player}'s name ({self.world.get_player_name(self.player)}) must be ASCII-compatible")
+
+ self.parser = Rule_AST_Transformer(self, self.player)
+
+ for (option_name, option) in oot_options.items():
+ result = getattr(self.world, option_name)[self.player]
+ if isinstance(result, Range):
+ option_value = int(result)
+ elif isinstance(result, Toggle):
+ option_value = bool(result)
+ elif isinstance(result, OptionList):
+ option_value = result.value
+ else:
+ option_value = result.current_key
+ setattr(self, option_name, option_value)
+
+ self.shop_prices = {}
+ self.regions = [] # internal cache of regions for this world, used later
+ self.remove_from_start_inventory = [] # some items will be precollected but not in the inventory
+ self.starting_items = Counter()
+ self.starting_songs = False # whether starting_items contains a song
+ self.file_hash = [self.world.random.randint(0, 31) for i in range(5)]
+
+ self.item_name_groups = {
+ "medallions": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion", "Spirit Medallion"},
+ "stones": {"Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
+ "rewards": {"Light Medallion", "Forest Medallion", "Fire Medallion", "Water Medallion", "Shadow Medallion", "Spirit Medallion", \
+ "Kokiri Emerald", "Goron Ruby", "Zora Sapphire"},
+ "bottles": {"Bottle", "Bottle with Milk", "Deliver Letter", "Sell Big Poe", "Bottle with Red Potion", "Bottle with Green Potion", \
+ "Bottle with Blue Potion", "Bottle with Fairy", "Bottle with Fish", "Bottle with Blue Fire", "Bottle with Bugs", "Bottle with Poe"}
+ }
+
+ # Incompatible option handling
+ # ER and glitched logic are not compatible; glitched takes priority
+ if self.logic_rules == 'glitched':
+ self.shuffle_interior_entrances = False
+ self.shuffle_grotto_entrances = False
+ self.shuffle_dungeon_entrances = False
+ self.shuffle_overworld_entrances = False
+ self.owl_drops = False
+ self.warp_songs = False
+ self.spawn_positions = False
+
+ # Closed forest and adult start are not compatible; closed forest takes priority
+ if self.open_forest == 'closed':
+ self.starting_age = 'child'
+
+ # Skip child zelda and shuffle egg are not compatible; skip-zelda takes priority
+ if self.skip_child_zelda:
+ self.shuffle_weird_egg = False
+
+ # Determine skipped trials in GT
+ # This needs to be done before the logic rules in GT are parsed
+ trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light']
+ chosen_trials = self.world.random.sample(trial_list, self.trials) # chooses a list of trials to NOT skip
+ self.skipped_trials = {trial: (trial not in chosen_trials) for trial in trial_list}
+
+ # Determine which dungeons are MQ
+ # Possible future plan: allow user to pick which dungeons are MQ
+ self.mq_dungeons = 0 # temporary disable for client-side issues
+ mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons)
+ self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table}
+
+ # Determine tricks in logic
+ for trick in self.logic_tricks:
+ normalized_name = trick.casefold()
+ if normalized_name in normalized_name_tricks:
+ setattr(self, normalized_name_tricks[normalized_name]['name'], True)
+ else:
+ raise Exception(f'Unknown OOT logic trick for player {self.player}: {trick}')
+
+ # Not implemented for now, but needed to placate the generator. Remove as they are implemented
+ self.mq_dungeons_random = False # this will be a deprecated option later
+ self.ocarina_songs = False # just need to pull in the OcarinaSongs module
+ self.big_poe_count = 1 # disabled due to client-side issues for now
+ self.correct_chest_sizes = False # will probably never be implemented since multiworld items are always major
+ # ER options
+ self.shuffle_interior_entrances = 'off'
+ self.shuffle_grotto_entrances = False
+ self.shuffle_dungeon_entrances = False
+ self.shuffle_overworld_entrances = False
+ self.owl_drops = False
+ self.warp_songs = False
+ self.spawn_positions = False
+
+ # Set internal names used by the OoT generator
+ self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] # only 'keysanity' and 'remove' implemented
+
+ # Hint stuff
+ self.misc_hints = True # this is just always on
+ self.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text
+ self.gossip_hints = {}
+ self.required_locations = []
+ self.empty_areas = {}
+ self.major_item_locations = []
+
+ # ER names
+ self.ensure_tod_access = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances or self.spawn_positions
+ self.entrance_shuffle = (self.shuffle_interior_entrances != 'off') or self.shuffle_grotto_entrances or self.shuffle_dungeon_entrances or \
+ self.shuffle_overworld_entrances or self.owl_drops or self.warp_songs or self.spawn_positions
+ self.disable_trade_revert = (self.shuffle_interior_entrances != 'off') or self.shuffle_overworld_entrances
+ self.shuffle_special_interior_entrances = self.shuffle_interior_entrances == 'all'
+
+ # fixing some options
+ self.starting_tod = self.starting_tod.replace('_', '-') # Fixes starting time spelling: "witching_hour" -> "witching-hour"
+ self.shopsanity = self.shopsanity.replace('_value', '') # can't set "random" manually
+ self.shuffle_scrubs = self.shuffle_scrubs.replace('_prices', '')
+
+
+ # Get hint distribution
+ self.hint_dist_user = read_json(data_path('Hints', f'{self.hint_dist}.json'))
+
+ self.added_hint_types = {}
+ self.item_added_hint_types = {}
+ self.hint_exclusions = set()
+ if self.skip_child_zelda:
+ self.hint_exclusions.add('Song from Impa')
+ self.hint_type_overrides = {}
+ self.item_hint_type_overrides = {}
+
+ # unused hint stuff
+ self.named_item_pool = {}
+ self.hint_text_overrides = {}
+
+ for dist in hint_dist_keys:
+ self.added_hint_types[dist] = []
+ for loc in self.hint_dist_user['add_locations']:
+ if 'types' in loc:
+ if dist in loc['types']:
+ self.added_hint_types[dist].append(loc['location'])
+ self.item_added_hint_types[dist] = []
+ for i in self.hint_dist_user['add_items']:
+ if dist in i['types']:
+ self.item_added_hint_types[dist].append(i['item'])
+ self.hint_type_overrides[dist] = []
+ for loc in self.hint_dist_user['remove_locations']:
+ if dist in loc['types']:
+ self.hint_type_overrides[dist].append(loc['location'])
+ self.item_hint_type_overrides[dist] = []
+ for i in self.hint_dist_user['remove_items']:
+ if dist in i['types']:
+ self.item_hint_type_overrides[dist].append(i['item'])
+
+ self.always_hints = [hint.name for hint in getRequiredHints(self)]
+
+
+
+ def load_regions_from_json(self, file_path):
+ region_json = read_json(file_path)
+
+ for region in region_json:
+ new_region = OOTRegion(region['region_name'], RegionType.Generic, None, self.player)
+ new_region.world = self.world
+ if 'scene' in region:
+ new_region.scene = region['scene']
+ if 'hint' in region:
+ new_region.hint_text = region['hint']
+ if 'dungeon' in region:
+ new_region.dungeon = region['dungeon']
+ if 'time_passes' in region:
+ new_region.time_passes = region['time_passes']
+ new_region.provides_time = TimeOfDay.ALL
+ if new_region.name == 'Ganons Castle Grounds':
+ new_region.provides_time = TimeOfDay.DAMPE
+ if 'locations' in region:
+ for location, rule in region['locations'].items():
+ new_location = LocationFactory(location, self.player)
+ if new_location.type in ['HintStone', 'Hint']:
+ continue
+ new_location.parent_region = new_region
+ new_location.rule_string = rule
+ if self.world.logic_rules != 'none':
+ self.parser.parse_spot_rule(new_location)
+ if new_location.never:
+ # We still need to fill the location even if ALR is off.
+ logger.debug('Unreachable location: %s', new_location.name)
+ new_location.player = self.player
+ new_region.locations.append(new_location)
+ if 'events' in region:
+ for event, rule in region['events'].items():
+ # Allow duplicate placement of events
+ lname = '%s from %s' % (event, new_region.name)
+ new_location = OOTLocation(self.player, lname, type='Event', parent=new_region)
+ new_location.rule_string = rule
+ if self.world.logic_rules != 'none':
+ self.parser.parse_spot_rule(new_location)
+ if new_location.never:
+ logger.debug('Dropping unreachable event: %s', new_location.name)
+ else:
+ new_location.player = self.player
+ new_region.locations.append(new_location)
+ self.make_event_item(event, new_location)
+ new_location.show_in_spoiler = False
+ if 'exits' in region:
+ for exit, rule in region['exits'].items():
+ new_exit = OOTEntrance(self.player, '%s => %s' % (new_region.name, exit), new_region)
+ new_exit.vanilla_connected_region = exit
+ new_exit.rule_string = rule
+ if self.world.logic_rules != 'none':
+ self.parser.parse_spot_rule(new_exit)
+ if new_exit.never:
+ logger.debug('Dropping unreachable exit: %s', new_exit.name)
+ else:
+ new_region.exits.append(new_exit)
+
+ self.world.regions.append(new_region)
+ self.regions.append(new_region)
+ self.world._recache()
+
+
+ def set_scrub_prices(self):
+ # Get Deku Scrub Locations
+ scrub_locations = [location for location in self.get_locations() if 'Deku Scrub' in location.name]
+ scrub_dictionary = {}
+ self.scrub_prices = {}
+ for location in scrub_locations:
+ if location.default not in scrub_dictionary:
+ scrub_dictionary[location.default] = []
+ scrub_dictionary[location.default].append(location)
+
+ # Loop through each type of scrub.
+ for (scrub_item, default_price, text_id, text_replacement) in business_scrubs:
+ price = default_price
+ if self.shuffle_scrubs == 'low':
+ price = 10
+ elif self.shuffle_scrubs == 'random':
+ # this is a random value between 0-99
+ # average value is ~33 rupees
+ price = int(self.world.random.betavariate(1, 2) * 99)
+
+ # Set price in the dictionary as well as the location.
+ self.scrub_prices[scrub_item] = price
+ if scrub_item in scrub_dictionary:
+ for location in scrub_dictionary[scrub_item]:
+ location.price = price
+ if location.item is not None:
+ location.item.price = price
+
+
+ def random_shop_prices(self):
+ shop_item_indexes = ['7', '5', '8', '6']
+ self.shop_prices = {}
+ for region in self.regions:
+ if self.shopsanity == 'random':
+ shop_item_count = self.world.random.randint(0,4)
+ else:
+ shop_item_count = int(self.shopsanity)
+
+ for location in region.locations:
+ if location.type == 'Shop':
+ if location.name[-1:] in shop_item_indexes[:shop_item_count]:
+ self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5
+
+
+ def fill_bosses(self, bossCount=9):
+ rewardlist = (
+ 'Kokiri Emerald',
+ 'Goron Ruby',
+ 'Zora Sapphire',
+ 'Forest Medallion',
+ 'Fire Medallion',
+ 'Water Medallion',
+ 'Spirit Medallion',
+ 'Shadow Medallion',
+ 'Light Medallion'
+ )
+ boss_location_names = (
+ 'Queen Gohma',
+ 'King Dodongo',
+ 'Barinade',
+ 'Phantom Ganon',
+ 'Volvagia',
+ 'Morpha',
+ 'Bongo Bongo',
+ 'Twinrova',
+ 'Links Pocket'
+ )
+ boss_rewards = [self.create_item(reward) for reward in rewardlist]
+ boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names]
+
+ placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
+ unplaced_prizes = [item for item in boss_rewards if item.name not in placed_prizes]
+ empty_boss_locations = [loc for loc in boss_locations if loc.item is None]
+ prizepool = list(unplaced_prizes)
+ prize_locs = list(empty_boss_locations)
+
+ while bossCount:
+ bossCount -= 1
+ self.world.random.shuffle(prizepool)
+ self.world.random.shuffle(prize_locs)
+ item = prizepool.pop()
+ loc = prize_locs.pop()
+ self.world.push_item(loc, item, collect=False)
+ loc.locked = True
+ loc.event = True
+
+
+ def create_item(self, name: str):
+ if name in item_table:
+ return OOTItem(name, self.player, item_table[name], False)
+ return OOTItem(name, self.player, ('Event', True, None, None), True)
+
+
+ def make_event_item(self, name, location, item=None):
+ if item is None:
+ item = self.create_item(name)
+ self.world.push_item(location, item, collect=False)
+ location.locked = True
+ location.event = True
+ if name not in item_table:
+ location.internal = True
+ return item
+
+
+ def create_regions(self): # create and link regions
+ if self.logic_rules == 'glitchless':
+ world_type = 'World'
+ else:
+ world_type = 'Glitched World'
+ overworld_data_path = data_path(world_type, 'Overworld.json')
+ menu = OOTRegion('Menu', None, None, self.player)
+ start = OOTEntrance(self.player, 'New Game', menu)
+ menu.exits.append(start)
+ self.world.regions.append(menu)
+ self.load_regions_from_json(overworld_data_path)
+ start.connect(self.world.get_region('Root', self.player))
+ create_dungeons(self)
+ self.parser.create_delayed_rules()
+
+ if self.shopsanity != 'off':
+ self.random_shop_prices()
+ self.set_scrub_prices()
+
+ # logger.info('Setting Entrances.')
+ # set_entrances(self)
+ # Enforce vanilla for now
+ for region in self.regions:
+ for exit in region.exits:
+ exit.connect(self.world.get_region(exit.vanilla_connected_region, self.player))
+ if self.entrance_shuffle:
+ shuffle_random_entrances(self)
+
+
+ def set_rules(self):
+ set_rules(self)
+
+
+ def generate_basic(self): # generate item pools, place fixed items
+ # Generate itempool
+ generate_itempool(self)
+ junk_pool = get_junk_pool(self)
+ # Determine starting items
+ for item in self.world.precollected_items:
+ if item.player != self.player:
+ continue
+ if item.name in self.remove_from_start_inventory:
+ self.remove_from_start_inventory.remove(item.name)
+ else:
+ self.starting_items[item.name] += 1
+ if item.type == 'Song':
+ self.starting_songs = True
+ # Call the junk fill and get a replacement
+ if item in self.itempool:
+ self.itempool.remove(item)
+ self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
+ if self.start_with_consumables:
+ self.starting_items['Deku Sticks'] = 30
+ self.starting_items['Deku Nuts'] = 40
+ if self.start_with_rupees:
+ self.starting_items['Rupees'] = 999
+
+ # Uniquely rename drop locations for each region and erase them from the spoiler
+ set_drop_location_names(self)
+
+ # Fill boss prizes
+ self.fill_bosses()
+
+ # relevant for both dungeon item fill and song fill
+ dungeon_song_locations = [
+ "Deku Tree Queen Gohma Heart",
+ "Dodongos Cavern King Dodongo Heart",
+ "Jabu Jabus Belly Barinade Heart",
+ "Forest Temple Phantom Ganon Heart",
+ "Fire Temple Volvagia Heart",
+ "Water Temple Morpha Heart",
+ "Shadow Temple Bongo Bongo Heart",
+ "Spirit Temple Twinrova Heart",
+ "Song from Impa",
+ "Sheik in Ice Cavern",
+ "Bottom of the Well Lens of Truth Chest", "Bottom of the Well MQ Lens of Truth Chest", # only one exists
+ "Gerudo Training Grounds Maze Path Final Chest", "Gerudo Training Grounds MQ Ice Arrows Chest", # only one exists
+ ]
+
+ # Place/set rules for dungeon items
+ itempools = {
+ 'dungeon': [],
+ 'overworld': [],
+ 'any_dungeon': [],
+ 'keysanity': [],
+ }
+ any_dungeon_locations = []
+ for dungeon in self.dungeons:
+ itempools['dungeon'] = []
+ # Put the dungeon items into their appropriate pools.
+ # Build in reverse order since we need to fill boss key first and pop() returns the last element
+ if self.shuffle_mapcompass in itempools:
+ itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items)
+ if self.shuffle_smallkeys in itempools:
+ itempools[self.shuffle_smallkeys].extend(dungeon.small_keys)
+ shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
+ if shufflebk in itempools:
+ itempools[shufflebk].extend(dungeon.boss_key)
+
+ # We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
+ dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
+ if loc.item is None and (self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
+ if itempools['dungeon']: # only do this if there's anything to shuffle
+ self.world.random.shuffle(dungeon_locations)
+ fill_restrictive(self.world, self.state_with_items(self.itempool), dungeon_locations, itempools['dungeon'], True, True)
+ any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
+
+ # Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
+ if self.shuffle_fortresskeys == 'any_dungeon':
+ fortresskeys = list(filter(lambda item: item.player == self.player and item.type == 'FortressSmallKey', self.itempool))
+ itempools['any_dungeon'].extend(fortresskeys)
+ for key in fortresskeys:
+ self.itempool.remove(key)
+ if itempools['any_dungeon']:
+ itempools['any_dungeon'].sort(key=lambda item: {'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'FortressSmallKey': 1}.get(item.type, 0))
+ self.world.random.shuffle(any_dungeon_locations)
+ fill_restrictive(self.world, self.state_with_items(self.itempool), any_dungeon_locations, itempools['any_dungeon'], True, True)
+
+ # If anything is overworld-only, enforce them as local and not in the remaining dungeon locations
+ if itempools['overworld'] or self.shuffle_fortresskeys == 'overworld':
+ from worlds.generic.Rules import forbid_items_for_player
+ fortresskeys = {'Small Key (Gerudo Fortress)'} if self.shuffle_fortresskeys == 'overworld' else set()
+ local_overworld_items = set(map(lambda item: item.name, itempools['overworld'])).union(fortresskeys)
+ for location in self.world.get_locations():
+ if location.player != self.player or location in any_dungeon_locations:
+ forbid_items_for_player(location, local_overworld_items, self.player)
+ self.itempool.extend(itempools['overworld'])
+
+ # Dump keysanity items into the itempool
+ self.itempool.extend(itempools['keysanity'])
+
+ # Now that keys are in the pool, we can forbid tunics from child-only shops
+ set_entrances_based_rules(self)
+
+ # Place songs
+ # 5 built-in retries because this section can fail sometimes
+ if self.shuffle_song_items != 'any':
+ tries = 5
+ if self.shuffle_song_items == 'song':
+ song_locations = list(filter(lambda location: location.type == 'Song',
+ self.world.get_unfilled_locations(player=self.player)))
+ elif self.shuffle_song_items == 'dungeon':
+ song_locations = list(filter(lambda location: location.name in dungeon_song_locations,
+ self.world.get_unfilled_locations(player=self.player)))
+ else:
+ raise Exception(f"Unknown song shuffle type: {self.shuffle_song_items}")
+
+ songs = list(filter(lambda item: item.player == self.player and item.type == 'Song', self.itempool))
+ for song in songs:
+ self.itempool.remove(song)
+ while tries:
+ try:
+ self.world.random.shuffle(songs) # shuffling songs makes it less likely to fail by placing ZL last
+ self.world.random.shuffle(song_locations)
+ fill_restrictive(self.world, self.state_with_items(self.itempool), song_locations[:], songs[:], True, True)
+ logger.debug(f"Successfully placed songs for player {self.player} after {6-tries} attempt(s)")
+ tries = 0
+ except FillError as e:
+ tries -= 1
+ if tries == 0:
+ raise e
+ logger.debug(f"Failed placing songs for player {self.player}. Retries left: {tries}")
+ # undo what was done
+ for song in songs:
+ song.location = None
+ song.world = None
+ for location in song_locations:
+ location.item = None
+ location.locked = False
+ location.event = False
+
+ # Place shop items
+ # fast fill will fail because there is some logic on the shop items. we'll gather them up and place the shop items
+ if self.shopsanity != 'off':
+ shop_items = list(filter(lambda item: item.player == self.player and item.type == 'Shop', self.itempool))
+ shop_locations = list(filter(lambda location: location.type == 'Shop' and location.name not in self.shop_prices,
+ self.world.get_unfilled_locations(player=self.player)))
+ shop_items.sort(key=lambda item: 1 if item.name in ["Buy Goron Tunic", "Buy Zora Tunic"] else 0)
+ self.world.random.shuffle(shop_locations)
+ for item in shop_items:
+ self.itempool.remove(item)
+ fill_restrictive(self.world, self.state_with_items(self.itempool), shop_locations, shop_items, True, True)
+ set_shop_rules(self)
+
+ # Locations which are not sendable must be converted to events
+ # This includes all locations for which show_in_spoiler is false, and shuffled shop items.
+ for loc in self.get_locations():
+ if loc.address is not None and (not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop')
+ or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
+ loc.address = None
+
+ # Gather items for ice trap appearances
+ self.fake_items = []
+ if self.ice_trap_appearance in ['major_only', 'anything']:
+ self.fake_items.extend([item for item in self.itempool if item.index and self.is_major_item(item)])
+ if self.ice_trap_appearance in ['junk_only', 'anything']:
+ self.fake_items.extend([item for item in self.itempool if item.index and not self.is_major_item(item) and item.name != 'Ice Trap'])
+
+ # Put all remaining items into the general itempool
+ self.world.itempool += self.itempool
+
+ # Kill unreachable events that can't be gotten even with all items
+ # Make sure to only kill actual internal events, not in-game "events"
+ all_state = self.state_with_items(self.itempool)
+ all_locations = [loc for loc in self.world.get_locations() if loc.player == self.player]
+ reachable = self.world.get_reachable_locations(all_state, self.player)
+ unreachable = [loc for loc in all_locations if loc.internal and loc.event and loc.locked and loc not in reachable]
+ for loc in unreachable:
+ loc.parent_region.locations.remove(loc)
+ # Exception: Sell Big Poe is an event which is only reachable if Bottle with Big Poe is in the item pool.
+ # We allow it to be removed only if Bottle with Big Poe is not in the itempool.
+ bigpoe = self.world.get_location('Sell Big Poe from Market Guard House', self.player)
+ if not all_state.has('Bottle with Big Poe', self.player) and bigpoe not in reachable:
+ bigpoe.parent_region.locations.remove(bigpoe)
+ self.world.clear_location_cache()
+
+ # If fast scarecrow then we need to kill the Pierre location as it will be unreachable
+ if self.free_scarecrow:
+ loc = self.world.get_location("Pierre", self.player)
+ loc.parent_region.locations.remove(loc)
+ # If open zora's domain then we need to kill Deliver Rutos Letter
+ if self.zora_fountain == 'open':
+ loc = self.world.get_location("Deliver Rutos Letter", self.player)
+ loc.parent_region.locations.remove(loc)
+
+ def pre_fill(self):
+ # If skip child zelda is active and Song from Impa is unfilled, put a local giveable item into it.
+ impa = self.world.get_location("Song from Impa", self.player)
+ if self.skip_child_zelda and impa.item is None:
+ from .SaveContext import SaveContext
+ item_to_place = self.world.random.choice([item for item in self.world.itempool
+ if item.player == self.player and item.name in SaveContext.giveable_items])
+ self.world.push_item(impa, item_to_place, False)
+ impa.locked = True
+ impa.event = True
+ self.world.itempool.remove(item_to_place)
+
+ # For now we will always output a patch file.
+ def generate_output(self, output_directory: str):
+ # Make ice traps appear as other random items
+ ice_traps = [loc.item for loc in self.get_locations() if loc.item.name == 'Ice Trap']
+ for trap in ice_traps:
+ trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name)
+
+ outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_player_name(self.player)}"
+ rom = Rom(file=get_options()['oot_options']['rom_file']) # a ROM must be provided, cannot produce patches without it
+ if self.hints != 'none':
+ buildWorldGossipHints(self)
+ patch_rom(self, rom)
+ patch_cosmetics(self, rom)
+ rom.update_header()
+ create_patch_file(rom, output_path(output_directory, outfile_name+'.apz5'))
+ rom.restore()
+
+
+ # Helper functions
+ def get_shuffled_entrances(self):
+ return []
+
+ # make this a generator later?
+ def get_locations(self):
+ return [loc for region in self.regions for loc in region.locations]
+
+ def get_location(self, location):
+ return self.world.get_location(location, self.player)
+
+ def get_region(self, region):
+ return self.world.get_region(region, self.player)
+
+ def state_with_items(self, items):
+ ret = CollectionState(self.world)
+ for item in items:
+ self.collect(ret, item)
+ ret.sweep_for_events()
+ return ret
+
+ def is_major_item(self, item: OOTItem):
+ if item.type == 'Token':
+ return self.bridge == 'tokens' or self.lacs_condition == 'tokens'
+
+ if item.type in ('Drop', 'Event', 'Shop', 'DungeonReward') or not item.advancement:
+ return False
+
+ if item.name.startswith('Bombchus') and not self.bombchus_in_logic:
+ return False
+
+ if item.type in ['Map', 'Compass']:
+ return False
+ if item.type == 'SmallKey' and self.shuffle_smallkeys in ['dungeon', 'vanilla']:
+ return False
+ if item.type == 'FortressSmallKey' and self.shuffle_fortresskeys == 'vanilla':
+ return False
+ if item.type == 'BossKey' and self.shuffle_bosskeys in ['dungeon', 'vanilla']:
+ return False
+ if item.type == 'GanonBossKey' and self.shuffle_ganon_bosskey in ['dungeon', 'vanilla']:
+ return False
+
+ return True
+
+
+ # Run this once for to gather up all required locations (for WOTH), barren regions (for foolish), and location of major items.
+ # required_locations and major_item_locations need to be ordered for deterministic hints.
+ def gather_hint_data(self):
+ if self.required_locations and self.empty_areas and self.major_item_locations:
+ return
+
+ items_by_region = {}
+ for r in self.regions:
+ items_by_region[r.hint_text] = {'dungeon': False, 'weight': 0, 'prog_items': 0}
+ for d in self.dungeons:
+ items_by_region[d.hint_text] = {'dungeon': True, 'weight': 0, 'prog_items': 0}
+ del(items_by_region["Link's Pocket"])
+ del(items_by_region[None])
+
+ for loc in self.get_locations():
+ if loc.item.code: # is a real item
+ hint_area = get_hint_area(loc)
+ items_by_region[hint_area]['weight'] += 1
+ if loc.item.advancement and (not loc.locked or loc.item.type == 'Song'):
+ # Non-locked progression. Increment counter
+ items_by_region[hint_area]['prog_items'] += 1
+ # Skip item at location and see if game is still beatable
+ state = CollectionState(self.world)
+ state.locations_checked.add(loc)
+ if not self.world.can_beat_game(state):
+ self.required_locations.append(loc)
+ self.empty_areas = {region: info for (region, info) in items_by_region.items() if not info['prog_items']}
+
+ for loc in self.world.get_filled_locations():
+ if (loc.item.player == self.player and self.is_major_item(loc.item)
+ or (loc.item.player == self.player and loc.item.name in self.item_added_hint_types['item'])
+ or (loc.name in self.added_hint_types['item'] and loc.player == self.player)):
+ self.major_item_locations.append(loc)
diff --git a/worlds/oot/crc.py b/worlds/oot/crc.py
new file mode 100644
index 00000000..cfaa1b95
--- /dev/null
+++ b/worlds/oot/crc.py
@@ -0,0 +1,36 @@
+import itertools
+from .ntype import BigStream, uint32
+
+def calculate_crc(self):
+
+ t1 = t2 = t3 = t4 = t5 = t6 = 0xDF26F436
+ u32 = 0xFFFFFFFF
+
+ m1 = self.read_bytes(0x1000, 0x100000)
+ words = map(uint32.value, zip(m1[0::4], m1[1::4], m1[2::4], m1[3::4]))
+
+ m2 = self.read_bytes(0x750, 0x100)
+ words2 = map(uint32.value, zip(m2[0::4], m2[1::4], m2[2::4], m2[3::4]))
+
+ for d, d2 in zip(words, itertools.cycle(words2)):
+ # keep t2 and t6 in u32 for comparisons; others can wait to be truncated
+ if ((t6 + d) & u32) < t6:
+ t4 += 1
+
+ t6 = (t6+d) & u32
+ t3 ^= d
+ shift = d & 0x1F
+ r = ((d << shift) | (d >> (32 - shift)))
+ t5 += r
+
+ if t2 > d:
+ t2 ^= r & u32
+ else:
+ t2 ^= t6 ^ d
+
+ t1 += d2 ^ d
+
+ crc0 = (t6 ^ t4 ^ t3) & u32
+ crc1 = (t5 ^ t2 ^ t1) & u32
+
+ return uint32.bytes(crc0) + uint32.bytes(crc1)
\ No newline at end of file
diff --git a/worlds/oot/data/.gitignore b/worlds/oot/data/.gitignore
new file mode 100644
index 00000000..28cde53d
--- /dev/null
+++ b/worlds/oot/data/.gitignore
@@ -0,0 +1,4 @@
+/Bingo
+/Music
+presets_default.json
+settings_mapping.json
\ No newline at end of file
diff --git a/worlds/oot/data/Compress/Compress b/worlds/oot/data/Compress/Compress
new file mode 100644
index 00000000..868d6df3
Binary files /dev/null and b/worlds/oot/data/Compress/Compress differ
diff --git a/worlds/oot/data/Compress/Compress.exe b/worlds/oot/data/Compress/Compress.exe
new file mode 100644
index 00000000..fbdac2c2
Binary files /dev/null and b/worlds/oot/data/Compress/Compress.exe differ
diff --git a/worlds/oot/data/Compress/Compress.out b/worlds/oot/data/Compress/Compress.out
new file mode 100644
index 00000000..e0e2f301
Binary files /dev/null and b/worlds/oot/data/Compress/Compress.out differ
diff --git a/worlds/oot/data/Compress/Compress32.exe b/worlds/oot/data/Compress/Compress32.exe
new file mode 100644
index 00000000..f8c25929
Binary files /dev/null and b/worlds/oot/data/Compress/Compress32.exe differ
diff --git a/worlds/oot/data/Compress/Compress_ARM64 b/worlds/oot/data/Compress/Compress_ARM64
new file mode 100644
index 00000000..5c0336f5
Binary files /dev/null and b/worlds/oot/data/Compress/Compress_ARM64 differ
diff --git a/worlds/oot/data/Compress/src/bSwap.h b/worlds/oot/data/Compress/src/bSwap.h
new file mode 100644
index 00000000..e41d6673
--- /dev/null
+++ b/worlds/oot/data/Compress/src/bSwap.h
@@ -0,0 +1,20 @@
+#ifndef BSWAP_H
+#define BSWAP_H
+
+#include
+
+uint32_t bSwap32(uint32_t a)
+{
+ return( (a & 0x000000FF) << 24 |
+ (a & 0x0000FF00) << 8 |
+ (a & 0x00FF0000) >> 8 |
+ (a & 0xFF000000) >> 24 );
+}
+
+uint16_t bSwap16(uint16_t a)
+{
+ return( (a & 0x00FF) << 8 |
+ (a & 0xFF00) >> 8 );
+}
+
+#endif
diff --git a/worlds/oot/data/Compress/src/compressor.c b/worlds/oot/data/Compress/src/compressor.c
new file mode 100644
index 00000000..ddd7c55e
--- /dev/null
+++ b/worlds/oot/data/Compress/src/compressor.c
@@ -0,0 +1,601 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include "bSwap.h"
+#include "yaz0.c"
+#include "crc.c"
+
+/* Needed to compile on Windows */
+#ifdef _WIN32
+#include
+#endif
+
+/* Different ROM sizes */
+#define UINTSIZE 0x1000000
+#define COMPSIZE 0x2000000
+
+/* Number of extra bytes to add to compression buffer */
+#define COMPBUFF 0x250
+
+//Structs {{{1
+/* DMA table entry */
+typedef struct
+{
+ uint32_t startV;
+ uint32_t endV;
+ uint32_t startP;
+ uint32_t endP;
+}
+table_t;
+
+/* Temporary storage for output data */
+typedef struct
+{
+ table_t table;
+ uint8_t* data;
+ uint8_t comp;
+ uint32_t size;
+}
+output_t;
+
+/* Archive struct */
+typedef struct
+{
+ uint32_t fileCount;
+ uint32_t* refSize;
+ uint32_t* srcSize;
+ uint8_t** ref;
+ uint8_t** src;
+}
+archive_t;
+/* 1}}} */
+
+/* Functions {{{1 */
+uint32_t findTable(uint8_t*);
+void getTableEnt(table_t*, uint32_t*, uint32_t);
+void* threadFunc(void*);
+void errorCheck(int, char**);
+void makeArchive();
+int32_t getNumCores();
+int32_t getNext();
+/* 1}}} */
+
+/* Globals {{{1 */
+char* inName;
+char* outName;
+uint8_t* inROM;
+uint8_t* outROM;
+uint8_t* refTab;
+pthread_mutex_t filelock;
+pthread_mutex_t countlock;
+int32_t numFiles, nextFile;
+int32_t arcCount, outSize;
+uint32_t* fileTab;
+archive_t* archive;
+output_t* out;
+/* 1}}} */
+
+/* int main(int, char**) {{{1 */
+int main(int argc, char** argv)
+{
+ FILE* file;
+ int32_t tabStart, tabSize, tabCount, junk;
+ volatile int32_t prev;
+ int32_t i, j, size, numCores, tempSize;
+ pthread_t* threads;
+ table_t tab;
+
+ errorCheck(argc, argv);
+ printf("Zelda64 Compressor, Version 2\n");
+ fflush(stdout);
+
+ /* Open input, read into inROM */
+ file = fopen(argv[1], "rb");
+ fseek(file, 0, SEEK_END);
+ tempSize = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ inROM = calloc(tempSize, sizeof(uint8_t));
+ junk = fread(inROM, tempSize, 1, file);
+ fclose(file);
+
+ /* Read archive if it exists*/
+ file = fopen("ARCHIVE.bin", "rb");
+ if(file != NULL)
+ {
+ /* Get number of files */
+ printf("Loading Archive.\n");
+ fflush(stdout);
+ archive = malloc(sizeof(archive_t));
+ junk = fread(&(archive->fileCount), sizeof(uint32_t), 1, file);
+
+ /* Allocate space for files and sizes */
+ archive->refSize = malloc(sizeof(uint32_t) * archive->fileCount);
+ archive->srcSize = malloc(sizeof(uint32_t) * archive->fileCount);
+ archive->ref = malloc(sizeof(uint8_t*) * archive->fileCount);
+ archive->src = malloc(sizeof(uint8_t*) * archive->fileCount);
+
+ /* Read in file size and then file data */
+ for(i = 0; i < archive->fileCount; i++)
+ {
+ /* Decompressed "Reference" file */
+ junk = fread(&tempSize, sizeof(uint32_t), 1, file);
+ archive->ref[i] = malloc(tempSize);
+ archive->refSize[i] = tempSize;
+ junk = fread(archive->ref[i], 1, tempSize, file);
+
+ /* Compressed "Source" file */
+ junk = fread(&tempSize, sizeof(uint32_t), 1, file);
+ archive->src[i] = malloc(tempSize);
+ archive->srcSize[i] = tempSize;
+ junk = fread(archive->src[i], 1, tempSize, file);
+ }
+ fclose(file);
+ }
+ else
+ {
+ printf("No archive found, this could take a while.\n");
+ fflush(stdout);
+ archive = NULL;
+ }
+
+ /* Find the file table and relevant info */
+ tabStart = findTable(inROM);
+ fileTab = (uint32_t*)(inROM + tabStart);
+ getTableEnt(&tab, fileTab, 2);
+ tabSize = tab.endV - tab.startV;
+ tabCount = tabSize / 16;
+
+ /* Allocate space for the exclusion list */
+ /* Default to 1 (compress), set exclusions to 0 */
+ file = fopen("dmaTable.dat", "r");
+ size = tabCount - 1;
+ refTab = malloc(sizeof(uint8_t) * size);
+ memset(refTab, 1, size);
+
+ /* The first 3 files are never compressed */
+ /* They should never be given to the compression function anyway though */
+ refTab[0] = refTab[1] = refTab[2] = 0;
+
+ /* Read in the rest of the exclusion list */
+ for(i = 0; fscanf(file, "%d", &j) == 1; i++)
+ {
+ /* Make sure the number is within the dmaTable */
+ if(j > size || j < -size)
+ {
+ fprintf(stderr, "Error: Entry %d in dmaTable.dat is out of bounds\n", i);
+ exit(1);
+ }
+
+ /* If j was negative, the file shouldn't exist */
+ /* Otherwise, set file to not compress */
+ if(j < 0)
+ refTab[(~j + 1)] = 2;
+ else
+ refTab[j] = 0;
+ }
+ fclose(file);
+
+ /* Initialise some stuff */
+ out = malloc(sizeof(output_t) * tabCount);
+ pthread_mutex_init(&filelock, NULL);
+ pthread_mutex_init(&countlock, NULL);
+ numFiles = tabCount;
+ outSize = COMPSIZE;
+ nextFile = 3;
+ arcCount = 0;
+
+ /* Get CPU core count */
+ numCores = getNumCores();
+ threads = malloc(sizeof(pthread_t) * numCores);
+ printf("Detected %d cores.\n", (numCores));
+ printf("Starting compression.\n");
+ fflush(stdout);
+
+ /* Create all the threads */
+ for(i = 0; i < numCores; i++)
+ pthread_create(&threads[i], NULL, threadFunc, NULL);
+
+ /* Wait for all of the threads to finish */
+ for(i = 0; i < numCores; i++)
+ pthread_join(threads[i], NULL);
+ printf("\n");
+
+ /* Get size of new ROM */
+ /* Start with size of first 3 files */
+ tempSize = tabStart + tabSize;
+ for(i = 3; i < tabCount; i++)
+ tempSize += out[i].size;
+
+ /* If ROM is too big, update size */
+ if(tempSize > outSize)
+ outSize = tempSize;
+
+ /* Setup for copying to outROM */
+ printf("Files compressed, writing new ROM.\n");
+ outROM = calloc(outSize, sizeof(uint8_t));
+ memcpy(outROM, inROM, tabStart + tabSize);
+ prev = tabStart + tabSize;
+ tabStart += 0x20;
+
+ /* Free some stuff */
+ pthread_mutex_destroy(&filelock);
+ pthread_mutex_destroy(&countlock);
+ if(archive != NULL)
+ {
+ free(archive->ref);
+ free(archive->src);
+ free(archive->refSize);
+ free(archive->srcSize);
+ free(archive);
+ }
+ free(threads);
+ free(refTab);
+
+ /* Write data to outROM */
+ for(i = 3; i < tabCount; i++)
+ {
+ tab = out[i].table;
+ size = out[i].size;
+ tabStart += 0x10;
+
+ /* Finish table and copy to outROM */
+ if(tab.startV != tab.endV)
+ {
+ /* Set up physical addresses */
+ tab.startP = prev;
+ if(out[i].comp == 1)
+ tab.endP = tab.startP + size;
+ else if(out[i].comp == 2)
+ tab.startP = tab.endP = 0xFFFFFFFF;
+
+ /* If the file existed, write it */
+ if(tab.startP != 0xFFFFFFFF)
+ memcpy(outROM + tab.startP, out[i].data, size);
+
+ /* Write the table entry */
+ tab.startV = bSwap32(tab.startV);
+ tab.endV = bSwap32(tab.endV);
+ tab.startP = bSwap32(tab.startP);
+ tab.endP = bSwap32(tab.endP);
+ memcpy(outROM + tabStart, &tab, sizeof(table_t));
+ }
+
+ prev += size;
+ if(out[i].data != NULL)
+ free(out[i].data);
+ }
+ free(out);
+
+ /* Fix the CRC before writing the ROM */
+ fix_crc(outROM);
+
+ /* Make and fill the output ROM */
+ file = fopen(outName, "wb");
+ fwrite(outROM, outSize, 1, file);
+ fclose(file);
+
+ /* Make the archive if needed */
+ if(archive == NULL)
+ {
+ printf("Creating archive.\n");
+ makeArchive();
+ }
+
+ /* Free up the last bit of memory */
+ if(argc != 3)
+ free(outName);
+ free(inROM);
+ free(outROM);
+
+ printf("Compression complete.\n");
+
+ return(0);
+}
+/* 1}}} */
+
+/* uint32_t findTAble(uint8_t*) {{{1 */
+uint32_t findTable(uint8_t* argROM)
+{
+ uint32_t i;
+ uint32_t* tempROM;
+
+ tempROM = (uint32_t*)argROM;
+
+ /* Start at the end of the makerom (0x10600000) */
+ /* Look for dma entry for the makeom */
+ /* Should work for all Zelda64 titles */
+ for(i = 1048; i+4 < UINTSIZE; i += 4)
+ {
+ if(tempROM[i] == 0x00000000)
+ if(tempROM[i+1] == 0x60100000)
+ return(i * 4);
+ }
+
+ fprintf(stderr, "Error: Couldn't find dma table in ROM!\n");
+ exit(1);
+}
+/* 1}}} */
+
+/* void getTableEnt(table_t*, uint32_t*, uint32_t) {{{1 */
+void getTableEnt(table_t* tab, uint32_t* files, uint32_t i)
+{
+ tab->startV = bSwap32(files[i*4]);
+ tab->endV = bSwap32(files[(i*4)+1]);
+ tab->startP = bSwap32(files[(i*4)+2]);
+ tab->endP = bSwap32(files[(i*4)+3]);
+}
+/* 1}}} */
+
+/* void* threadFunc(void*) {{{1 */
+void* threadFunc(void* null)
+{
+ uint8_t* src;
+ uint8_t* dst;
+ table_t t;
+ int32_t i, nextArchive, size, srcSize;
+
+ while((i = getNext()) != -1)
+ {
+ /* Setup the src */
+ getTableEnt(&(t), fileTab, i);
+ srcSize = t.endV - t.startV;
+ src = inROM + t.startV;
+
+ /* If refTab is 1, compress */
+ /* If refTab is 2, file shouldn't exist */
+ /* Otherwise, just copy src into out */
+ if(refTab[i] == 1)
+ {
+ pthread_mutex_lock(&countlock);
+ nextArchive = arcCount++;
+ pthread_mutex_unlock(&countlock);
+
+ /* If uncompressed is the same as archive, just copy/paste the compressed */
+ /* Otherwise, compress it manually */
+ if((archive != NULL) && (memcmp(src, archive->ref[nextArchive], archive->refSize[nextArchive]) == 0))
+ {
+ out[i].comp = 1;
+ size = archive->srcSize[nextArchive];
+ out[i].data = malloc(size);
+ memcpy(out[i].data, archive->src[nextArchive], size);
+ }
+ else
+ {
+ size = srcSize + COMPBUFF;
+ dst = calloc(size, sizeof(uint8_t));
+ yaz0_encode(src, srcSize, dst, &(size));
+ out[i].comp = 1;
+ out[i].data = malloc(size);
+ memcpy(out[i].data, dst, size);
+ free(dst);
+ }
+
+ if(archive != NULL)
+ {
+ free(archive->ref[nextArchive]);
+ free(archive->src[nextArchive]);
+ }
+ }
+ else if(refTab[i] == 2)
+ {
+ out[i].comp = 2;
+ size = 0;
+ out[i].data = NULL;
+ }
+ else
+ {
+ out[i].comp = 0;
+ size = srcSize;
+ out[i].data = malloc(size);
+ memcpy(out[i].data, src, size);
+ }
+
+ /* Set up the table entry and size */
+ out[i].table = t;
+ out[i].size = size;
+ }
+
+ return(NULL);
+}
+/* 1}}} */
+
+/* void makeArchive() {{{1 */
+void makeArchive()
+{
+ table_t tab;
+ uint32_t tabSize, tabCount, tabStart;
+ uint32_t fileSize, fileCount, i;
+ FILE* file;
+
+ /* Find DMAtable info */
+ tabStart = findTable(outROM);
+ fileTab = (uint32_t*)(outROM + tabStart);
+ getTableEnt(&tab, fileTab, 2);
+ tabSize = tab.endV - tab.startV;
+ tabCount = tabSize / 16;
+ fileCount = 0;
+
+ /* Find the number of compressed files in the ROM */
+ /* Ignore first 3 files, as they're never compressed */
+ for(i = 3; i < tabCount; i++)
+ {
+ getTableEnt(&tab, fileTab, i);
+
+ if(tab.endP != 0 && tab.endP != 0xFFFFFFFF)
+ fileCount++;
+ }
+
+ /* Open output file */
+ file = fopen("ARCHIVE.bin", "wb");
+ if(file == NULL)
+ {
+ perror("ARCHIVE.bin");
+ fprintf(stderr, "Error: Could not create archive\n");
+ return;
+ }
+
+ /* Write the archive data */
+ fwrite(&fileCount, sizeof(uint32_t), 1, file);
+
+ /* Write the fileSize and data for each ref & src */
+ for(i = 3; i < tabCount; i++)
+ {
+ getTableEnt(&tab, fileTab, i);
+
+ if(tab.endP != 0 && tab.endP != 0xFFFFFFFF)
+ {
+ /* Write the size and data for the decompressed portion */
+ fileSize = tab.endV - tab.startV;
+ fwrite(&fileSize, sizeof(uint32_t), 1, file);
+ fwrite(inROM + tab.startV, 1, fileSize, file);
+
+ /* Write the size and data for the compressed portion */
+ fileSize = tab.endP - tab.startP;
+ fwrite(&fileSize, sizeof(uint32_t), 1, file);
+ fwrite((outROM + tab.startP), 1, fileSize, file);
+ }
+ }
+
+ fclose(file);
+}
+/* 1}}} */
+
+/* int32_t getNumCores() {{{1 */
+int32_t getNumCores()
+{
+ /* Windows */
+ #ifdef _WIN32
+
+ SYSTEM_INFO info;
+ GetSystemInfo(&info);
+ return(info.dwNumberOfProcessors);
+
+ /* Mac */
+ #elif MACOS
+
+ int nm[2];
+ size_t len;
+ uint32_t count;
+
+ len = 4;
+ nm[0] = CTL_HW;
+ nm[1] = HW_AVAILCPU;
+ sysctl(nm, 2, &count, &len, NULL, 0);
+
+ if (count < 1)
+ {
+ nm[1] = HW_NCPU;
+ sysctl(nm, 2, &count, &len, NULL, 0);
+ if (count < 1)
+ count = 1;
+ }
+ return(count);
+
+ /* Linux */
+ #else
+
+ return(sysconf(_SC_NPROCESSORS_ONLN));
+
+ #endif
+}
+/* 1}}} */
+
+/* int32_t getNext() {{{1 */
+int32_t getNext()
+{
+ int32_t file, temp;
+
+ pthread_mutex_lock(&filelock);
+
+ file = nextFile++;
+
+ /* Progress tracker */
+ if (file < numFiles)
+ {
+ temp = numFiles - (file + 1);
+ printf("%d files remaining\n", temp);
+ fflush(stdout);
+ }
+ else
+ {
+ file = -1;
+ }
+
+ pthread_mutex_unlock(&filelock);
+
+ return(file);
+}
+/* 1}}} */
+
+/* void errorCheck(int, char**) {{{1 */
+void errorCheck(int argc, char** argv)
+{
+ int i, j;
+ FILE* file;
+
+ /* Check for arguments */
+ if(argc < 2)
+ {
+ fprintf(stderr, "Usage: %s [Input ROM]