Ocarina of Time (#64)

* first commit (not including OoT data files yet)

* added some basic options

* rule parser works now at least

* make sure to commit everything this time

* temporary change to BaseClasses for oot

* overworld location graph builds mostly correctly

* adding oot data files

* commenting out world options until later since they only existed to make the RuleParser work

* conversion functions between AP ids and OOT ids

* world graph outputs

* set scrub prices

* itempool generates, entrances connected, way too many options added

* fixed set_rules and set_shop_rules

* temp baseclasses changes

* Reaches the fill step now, old event-based system retained in case the new way breaks

* Song placements and misc fixes everywhere

* temporary changes to make oot work

* changed root exits for AP fill framework

* prevent infinite recursion due to OoT sharing usage of the address field

* age reachability works hopefully, songs are broken again

* working spoiler log generation on beatable-only

* Logic tricks implemented

* need this for logic tricks

* fixed map/compass being placed on Serenade location

* kill unreachable events before filling the world

* add a bunch of utility functions to prepare for rom patching

* move OptionList into generic options

* fixed some silly bugs with OptionList

* properly seed all random behavior (so far)

* ROM generation working

* fix hints trying to get alttp dungeon hint texts

* continue fixing hints

* add oot to network data package

* change item and location IDs to 66000 and 67000 range respectively

* push removed items to precollected items

* fixed various issues with cross-contamination with multiple world generation

* reenable glitched logic (hopefully)

* glitched world files age-check fix

* cleaned up some get_locations calls

* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work

* reenable MQ dungeons

* fix forest mq exception

* made targeting style an option for now, will be cosmetic later

* reminder to move targeting to cosmetics

* some oot option maintenance

* enabled starting time of day

* fixed issue breaking shop slots in multiworld generation

* added "off" option for text shuffle and hints

* shopsanity functionality restored

* change patch file extension

* remove unnecessary utility functions + imports

* update MIT license

* change option to "patch_uncompressed_rom" instead of "compress_rom"

* compliance with new AutoWorld systems

* Kill only internal events, remove non-internal big poe event in code

* re-add the big poe event and handle it correctly

* remove extra method in Range option

* fix typo

* Starting items, starting with consumables option

* do not remove nonexistent item

* move set_shop_rules to after shop items are placed

* some cleanup

* add retries for song placement

* flagged Skull Mask and Mask of Truth as advancement items

* update OoT to use LogicMixin

* Fixed trying to assign starting items from the wrong players

* fixed song retry step

* improved option handling, comments, and starting item replacements

* DefaultOnToggle writes Yes or No to spoiler

* enable compression of output if Compress executable is present

* clean up compression

* check whether (de)compressor exists before running the process

* allow specification of rom path in host.yaml

* check if decompressed file already exists before decompressing again

* fix triforce hunt generation

* rename all the oot state functions with prefix

* OoT: mark triforce pieces as completion goal for triforce hunt

* added overworld and any-dungeon shuffle for dungeon items

* Hide most unshuffled locations and events from the list of locations in spoiler

* build oot option ranges with a generic function instead of defining each separately

* move oot output-type control to host.yaml instead of individual yamls

* implement dungeon song shuffle

* minor improvements to overworld dungeon item shuffle

* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list

* always output patch file to folder, remove option to generate ROM in preparation for removal

* re-add the fix for infinite recursion due to not being light or dark world

* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently

* oot: remove item_names and location_names

* oot: minor fixes

* oot: comment out ROM patching

* oot: only add CollectionState objects on creation if actually needed

* main entrance shuffle method and entrances-based rules

* fix entrances based rules

* disable master quest and big poe count options for client compatibility

* use get_player_name instead of get_player_names

* fix OptionList

* fix oot options for new option system

* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES

* fill AP player name in oot rom with 0 instead of 0xDF

* encode player name with ASCII for fixed-width

* revert oot player name array to 8 bytes per name

* remove Pierre location if fast scarecrow is on

* check player name length

* "free_scarecrow" not "fast_scarecrow"

* OoT locations now properly store the AP ID instead of the oot internal ID

* oot __version__ updates in lockstep with AP version

* pull in unmodified oot cosmetic files

* also grab JSONDump since it's needed apparently

* gather extra needed methods, modify imports

* delete cosmetics log, replace all instances of SettingsList with OOTWorld

* cosmetic options working, except for sound effects (due to ear-safe issues)

* SFX, Music, and Fanfare randomization reenabled

* move OoT data files into the worlds folder

* move Compress and Decompress into oot data folder

* Replace get_all_state with custom method to avoid the cache

* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues

* set data_version to 0

* make Kokiri Sword shuffle off by default

* reenable "Random Choice" for various cosmetic options

* kill Ruto's Letter turnin if open fountain
also fix for shopsanity

* place Buy Goron/Zora Tunic first in shop shuffle

* make ice traps appear as other items instead of breaking generation

* managed to break ice traps on non-major-only

* only handle ice traps if they are on

* fix shopsanity for non-oot games, and write player name instead of player number

* light arrows hint uses player name instead of player number

* Reenable "skip child zelda" option

* fix entrances_based_rules

* fix ganondorf hint if starting with light arrows

* fix dungeonitem shuffle and shopsanity interaction

* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group

* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any

* keep bosses and bombchu bowling chus out of data package

* revert workaround for infinite recursion and fix it properly

* fix shared shop id caches during patching process

* fix shop text box overflows, as much as possible

* add default oot host.yaml option

* add .apz5, .n64, .z64 to gitignore

* Properly document and name all (functioning) OOT options

* clean up some imports

* remove unnecessary files from oot's data

* fix typo in gitignore

* readd the Compress and Decompress utilities, since they are needed for generation

* cleanup of imports and some minor optimizations

* increase shop offset for item IDs to 0xCB

* remove shop item AP ids entirely

* prevent triforce pieces for other players from being received by yourself

* add "excluded" property to Location

* Hint system adapted and reenabled; hints still unseeded

* make hints deterministic with lists instead of sets

* do not allow hints to point to Light Arrows on non-vanilla bridge

* foreign locations hint as their full name in OoT rather than their region

* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated

* consolidate versioning in Utils

* ice traps appear as major items rather than any progression item

* set prescription and claim check as defaults for adult trade item settings

* add oot options to playerSettings

* allow case-insensitive logic tricks in yaml

* fix oot shopsanity option formatting

* Write OoT override info even if local item, enabling local checks to show up immediately in the client

* implement CollectionState.can_live_dmg for oot glitched logic

* filter item names for invalid characters when patching shops

* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world

* set hidden-spoiler items and locations with Shop items to events

* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start

* Fix oot Glitched and No Logic generation

* fix indenting

* Greatly reduce displayed cosmetic options

* Change oot data version to 1

* add apz5 distribution to webhost

* print player name if an ALttP dungeon contains a good item for OoT world

* delete unneeded commented code

* remove OcarinaSongs import to satisfy lint
This commit is contained in:
espeon65536 2021-09-02 08:35:05 -04:00 committed by GitHub
parent 74c30ce09a
commit 51c38fc628
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 80588 additions and 7 deletions

3
.gitignore vendored
View File

@ -5,9 +5,12 @@
*.bmbp
*.apbp
*.apmc
*.apz5
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.wixobj
*.lck
*.db3

View File

@ -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)

View File

@ -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)))

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -16,6 +16,9 @@
{% elif patch.game == "Factorio" %}
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% elif patch.game == "Ocarina of Time" %}
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% else %}
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>

View File

@ -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"):

View File

@ -85,4 +85,7 @@ factorio_options:
executable: "factorio\\bin\\x64\\factorio"
minecraft_options:
forge_directory: "Minecraft Forge server"
max_heap_size: "2G"
max_heap_size: "2G"
oot_options:
# File name of the OoT v1.0 ROM
rom_file: "The Legend of Zelda - Ocarina of Time.z64"

View File

@ -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:

View File

@ -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):

403
worlds/oot/Colors.py Normal file
View File

@ -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])

814
worlds/oot/Cosmetics.py Normal file
View File

@ -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.")

56
worlds/oot/Dungeon.py Normal file
View File

@ -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

129
worlds/oot/DungeonList.py Normal file
View File

@ -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))

19
worlds/oot/Entrance.py Normal file
View File

@ -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

View File

@ -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

1292
worlds/oot/HintList.py Normal file

File diff suppressed because it is too large Load Diff

1070
worlds/oot/Hints.py Normal file

File diff suppressed because it is too large Load Diff

112
worlds/oot/IconManip.py Normal file
View File

@ -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))

1410
worlds/oot/ItemPool.py Normal file

File diff suppressed because it is too large Load Diff

414
worlds/oot/Items.py Normal file
View File

@ -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,
}),
}

122
worlds/oot/JSONDump.py Normal file
View File

@ -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)

26
worlds/oot/LICENSE Normal file
View File

@ -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.

53
worlds/oot/Location.py Normal file
View File

@ -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

932
worlds/oot/LocationList.py Normal file
View File

@ -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

1359
worlds/oot/LogicTricks.py Normal file

File diff suppressed because it is too large Load Diff

732
worlds/oot/MQ.py Normal file
View File

@ -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)

995
worlds/oot/Messages.py Normal file
View File

@ -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 _: '<pad>' ),
0x01: ('line-break', 0, lambda _: '\n' ),
0x02: ('end', 0, lambda _: '' ),
0x04: ('box-break', 0, lambda _: '\n\n' ),
0x05: ('color', 1, lambda d: '<color ' + "{:02x}".format(d) + '>' ),
0x06: ('gap', 1, lambda d: '<' + str(d) + 'px gap>' ),
0x07: ('goto', 2, lambda d: '<goto ' + "{:04x}".format(d) + '>' ),
0x08: ('instant', 0, lambda _: '<allow instant text>' ),
0x09: ('un-instant', 0, lambda _: '<disallow instant text>' ),
0x0A: ('keep-open', 0, lambda _: '<keep open>' ),
0x0B: ('event', 0, lambda _: '<event>' ),
0x0C: ('box-break-delay', 1, lambda d: '\n▼<wait ' + str(d) + ' frames>\n' ),
0x0E: ('fade-out', 1, lambda d: '<fade after ' + str(d) + ' frames?>' ),
0x0F: ('name', 0, lambda _: '<name>' ),
0x10: ('ocarina', 0, lambda _: '<ocarina>' ),
0x12: ('sound', 2, lambda d: '<play SFX ' + "{:04x}".format(d) + '>' ),
0x13: ('icon', 1, lambda d: '<icon ' + "{:02x}".format(d) + '>' ),
0x14: ('speed', 1, lambda d: '<delay each character by ' + str(d) + ' frames>' ),
0x15: ('background', 3, lambda d: '<set background to ' + "{:06x}".format(d) + '>' ),
0x16: ('marathon', 0, lambda _: '<marathon time>' ),
0x17: ('race', 0, lambda _: '<race time>' ),
0x18: ('points', 0, lambda _: '<points>' ),
0x19: ('skulltula', 0, lambda _: '<skulltula count>' ),
0x1A: ('unskippable', 0, lambda _: '<text is unskippable>' ),
0x1B: ('two-choice', 0, lambda _: '<start two choice>' ),
0x1C: ('three-choice', 0, lambda _: '<start three choice>' ),
0x1D: ('fish', 0, lambda _: '<fish weight>' ),
0x1E: ('high-score', 1, lambda d: '<high-score ' + "{:02x}".format(d) + '>' ),
0x1F: ('time', 0, lambda _: '<current time>' ),
}
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

484
worlds/oot/Music.py Normal file
View File

@ -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)

271
worlds/oot/N64Patch.py Normal file
View File

@ -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

782
worlds/oot/Options.py Normal file
View File

@ -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,
}

2166
worlds/oot/Patches.py Normal file

File diff suppressed because it is too large Load Diff

61
worlds/oot/Regions.py Normal file
View File

@ -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]

321
worlds/oot/Rom.py Normal file
View File

@ -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))

507
worlds/oot/RuleParser.py Normal file
View File

@ -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)), '<string>', '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), '<string>', '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), '<string>', '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))),
'<string>', '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

203
worlds/oot/Rules.py Normal file
View File

@ -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)

1000
worlds/oot/SaveContext.py Normal file

File diff suppressed because it is too large Load Diff

212
worlds/oot/Sounds.py Normal file
View File

@ -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

369
worlds/oot/TextBox.py Normal file
View File

@ -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!')

97
worlds/oot/Utils.py Normal file
View File

@ -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

734
worlds/oot/__init__.py Normal file
View File

@ -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)

36
worlds/oot/crc.py Normal file
View File

@ -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)

4
worlds/oot/data/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/Bingo
/Music
presets_default.json
settings_mapping.json

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,20 @@
#ifndef BSWAP_H
#define BSWAP_H
#include <stdint.h>
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

View File

@ -0,0 +1,601 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include "bSwap.h"
#include "yaz0.c"
#include "crc.c"
/* Needed to compile on Windows */
#ifdef _WIN32
#include <Windows.h>
#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] <Output ROM>\n", argv[0]);
exit(1);
}
/* Check that input ROM exists & has permissions */
inName = argv[1];
file = fopen(inName, "rb");
if(file == NULL)
{
perror(inName);
exit(1);
}
/* Check that dmaTable.dat exists & has permissions */
file = fopen("dmaTable.dat", "r");
if(file == NULL)
{
perror("dmaTable.dat");
fprintf(stderr, "Please make a dmaTable.dat file first\n");
exit(1);
}
/* Check that output ROM is writeable */
/* Create output filename if needed */
if(argc < 3)
{
i = strlen(inName) + 6;
outName = malloc(i);
strcpy(outName, inName);
for(; i >= 0; i--)
{
if(outName[i] == '.')
{
outName[i] = '\0';
break;
}
}
strcat(outName, "-comp.z64");
file = fopen(outName, "wb");
if(file == NULL)
{
perror(outName);
free(outName);
exit(1);
}
fclose(file);
}
else
{
outName = argv[2];
file = fopen(outName, "wb");
if(file == NULL)
{
perror(outName);
exit(1);
}
fclose(file);
}
}
/* 1}}} */

View File

@ -0,0 +1,175 @@
/* snesrc - SNES Recompiler
*
* Copyright notice for this file:
* Copyright (C) 2005 Parasyte
*
* Based on uCON64's N64 checksum algorithm by Andreas Sterbenz
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <stdio.h>
#include <stdlib.h>
#define ROL(i, b) (((i) << (b)) | ((i) >> (32 - (b))))
#define BYTES2LONG(b) ( (b)[0] << 24 | \
(b)[1] << 16 | \
(b)[2] << 8 | \
(b)[3] )
#define N64_HEADER_SIZE 0x40
#define N64_BC_SIZE (0x1000 - N64_HEADER_SIZE)
#define N64_CRC1 0x10
#define N64_CRC2 0x14
#define CHECKSUM_START 0x00001000
#define CHECKSUM_LENGTH 0x00100000
#define CHECKSUM_CIC6102 0xF8CA4DDC
#define CHECKSUM_CIC6103 0xA3886759
#define CHECKSUM_CIC6105 0xDF26F436
#define CHECKSUM_CIC6106 0x1FEA617A
unsigned int crc_table[256];
void gen_table()
{
uint32_t crc, poly;
int32_t i, j;
poly = 0xEDB88320;
for (i = 0; i < 256; i++) {
crc = i;
for (j = 8; j > 0; j--) {
if (crc & 1) crc = (crc >> 1) ^ poly;
else crc >>= 1;
}
crc_table[i] = crc;
}
}
unsigned int crc32(unsigned char *data, int len)
{
uint32_t crc, i;
crc = ~0;
for (i = 0; i < len; i++)
crc = (crc >> 8) ^ crc_table[(crc ^ data[i]) & 0xFF];
return ~crc;
}
int N64GetCIC(unsigned char *data)
{
switch (crc32(&data[N64_HEADER_SIZE], N64_BC_SIZE)) {
case 0x6170A4A1: return 6101;
case 0x90BB6CB5: return 6102;
case 0x0B050EE0: return 6103;
case 0x98BC2C86: return 6105;
case 0xACC8580A: return 6106;
}
return 0;
}
int N64CalcCRC(unsigned int *crc, unsigned char *data)
{
int32_t bootcode, i;
uint32_t seed, r, d;
uint32_t t1, t2, t3;
uint32_t t4, t5, t6;
switch ((bootcode = N64GetCIC(data))) {
case 6101:
case 6102:
seed = CHECKSUM_CIC6102;
break;
case 6103:
seed = CHECKSUM_CIC6103;
break;
case 6105:
seed = CHECKSUM_CIC6105;
break;
case 6106:
seed = CHECKSUM_CIC6106;
break;
default:
return 0;
}
t1 = t2 = t3 = t4 = t5 = t6 = seed;
i = CHECKSUM_START;
while (i < (CHECKSUM_START + CHECKSUM_LENGTH)) {
d = BYTES2LONG(&data[i]);
if ((t6 + d) < t6) t4++;
t6 += d;
t3 ^= d;
r = ROL(d, (d & 0x1F));
t5 += r;
if (t2 > d) t2 ^= r;
else t2 ^= t6 ^ d;
if (bootcode == 6105) t1 += BYTES2LONG(&data[N64_HEADER_SIZE + 0x0710 + (i & 0xFF)]) ^ d;
else t1 += t5 ^ d;
i += 4;
}
if (bootcode == 6103) {
crc[0] = (t6 ^ t4) + t3;
crc[1] = (t5 ^ t2) + t1;
}
else if (bootcode == 6106) {
crc[0] = (t6 * t4) + t3;
crc[1] = (t5 * t2) + t1;
}
else {
crc[0] = t6 ^ t4 ^ t3;
crc[1] = t5 ^ t2 ^ t1;
}
return 1;
}
void fix_crc (uint8_t* buffer)
{
uint8_t CRC1[4];
uint8_t CRC2[4];
uint32_t crc[2];
uint32_t i;
gen_table();
/* If the CRC calc was successful, do stuff */
if(N64CalcCRC(crc, buffer))
{
for(i = 0; i < 4; i++)
{
CRC1[i] = (crc[0] >> (24-8*i))&0xFF;
CRC2[i] = (crc[1] >> (24-8*i))&0xFF;
}
/* If the CRC1 changed, update it */
if(crc[0] != BYTES2LONG(&buffer[N64_CRC1]))
memcpy(buffer + N64_CRC1, CRC1, 4);
/* If the CRC2 changed, update it */
if (crc[1] != BYTES2LONG(&buffer[N64_CRC2]))
memcpy(buffer + N64_CRC2, CRC2, 4);
}
}

View File

@ -0,0 +1,186 @@
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
uint32_t RabinKarp(uint8_t*, int, int, uint32_t*);
uint32_t findBest(uint8_t*, int, int, uint32_t*, uint32_t*, uint32_t*, uint8_t*);
int yaz0_internal(uint8_t*, int, uint8_t*);
void yaz0_encode(uint8_t*, int, uint8_t*, int*);
uint32_t RabinKarp(uint8_t* src, int srcSize, int srcPos, uint32_t* matchPos)
{
int startPos, smp, i;
uint32_t hash, curHash, curSize;
uint32_t bestSize, bestPos;
smp = srcSize - srcPos;
startPos = srcPos - 0x1000;
bestPos = bestSize = 0;
/* If available size is too small, return */
if(smp < 3)
return(0);
/* If available size is too big, reduce it */
if(smp > 0x111)
smp = 0x111;
/* If start position is negative, make it 0 */
if(startPos < 0)
startPos = 0;
/* Generate "hash" by converting to an int */
hash = bSwap32(*(int*)(src + srcPos));
hash = hash >> 8;
curHash = bSwap32(*(int*)(src + startPos));
curHash = curHash >> 8;
/* Search through data */
for(i = startPos; i < srcPos; i++)
{
/* If 3 bytes match, check for more */
if(curHash == hash)
{
for(curSize = 3; curSize < smp; curSize++)
if(src[i + curSize] != src[srcPos + curSize])
break;
/* Uodate best if needed */
if(curSize > bestSize)
{
bestSize = curSize;
bestPos = i;
if(bestSize == 0x111)
break;
}
}
/* Scoot over 1 byte */
curHash = (curHash << 8 | src[i + 3]) & 0x00FFFFFF;
}
/* Set match position, return the size of the match */
*matchPos = bestPos;
return(bestSize);
}
uint32_t findBest(uint8_t* src, int srcSize, int srcPos, uint32_t* matchPos, uint32_t* pMatch, uint32_t* pSize, uint8_t* pFlag)
{
int rv;
/* Check to see if this location was found by a look-ahead */
if(*pFlag == 1)
{
*pFlag = 0;
return(*pSize);
}
/* Find best match */
*pFlag = 0;
rv = RabinKarp(src, srcSize, srcPos, matchPos);
/* Look-ahead */
if(rv >= 3)
{
/* Find best match if current one were to be a 1 byte copy */
*pSize = RabinKarp(src, srcSize, srcPos+1, pMatch);
if(*pSize >= rv+2)
{
rv = *pFlag = 1;
*matchPos = *pMatch;
}
}
return(rv);
}
int yaz0_internal(uint8_t* src, int srcSize, uint8_t* dst)
{
int dstPos, srcPos, codeBytePos;
uint32_t numBytes, matchPos, dist, pMatch, pSize;
uint8_t codeByte, bitmask, pFlag;
srcPos = codeBytePos = 0;
dstPos = codeBytePos + 1;
bitmask = 0x80;
codeByte = pFlag = 0;
/* Go through all of src */
while(srcPos < srcSize)
{
/* Try to find matching bytes for compressing */
numBytes = findBest(src, srcSize, srcPos, &matchPos, &pMatch, &pSize, &pFlag);
/* Single byte copy */
if(numBytes < 3)
{
dst[dstPos++] = src[srcPos++];
codeByte |= bitmask;
}
/* Three byte encoding */
else if (numBytes > 0x11)
{
dist = srcPos - matchPos - 1;
/* Copy over 0R RR */
dst[dstPos++] = dist >> 8;
dst[dstPos++] = dist & 0xFF;
/* Reduce N if needed, copy over NN */
if(numBytes > 0x111)
numBytes = 0x111;
dst[dstPos++] = (numBytes - 0x12) & 0xFF;
srcPos += numBytes;
}
/* Two byte encoding */
else
{
dist = srcPos - matchPos - 1;
/* Copy over NR RR */
dst[dstPos++] = ((numBytes - 2) << 4) | (dist >> 8);
dst[dstPos++] = dist & 0xFF;
srcPos += numBytes;
}
/* Move bitmask to next byte */
bitmask = bitmask >> 1;
/* If all 8 bytes were used, write and move to the next one */
if(bitmask == 0)
{
dst[codeBytePos] = codeByte;
codeBytePos = dstPos;
if(srcPos < srcSize)
dstPos++;
codeByte = 0;
bitmask = 0x80;
}
}
/* Copy over last byte if it hasn't already */
if(bitmask != 0)
dst[codeBytePos] = codeByte;
/* Return size of dst */
return(dstPos);
}
void yaz0_encode(uint8_t* src, int srcSize, uint8_t* dst, int* dstSize)
{
int temp;
/* Write Yaz0 header */
temp = bSwap32(srcSize);
memcpy(dst, "Yaz0", 4);
memcpy(dst + 4, &temp, 4);
/* Encode, adjust dstSize */
temp = yaz0_internal(src, srcSize, dst + 16);
*dstSize = (temp + 31) & -16;
return;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,30 @@
#ifndef BSWAP_H
#define BSWAP_H
#include <stdint.h>
#if BYTE_ORDER == LITTLE_ENDIAN
#define bSwap32(x) _bSwap32(x)
#define bSwap16(x) _bSwap16(x)
#elif BYTE_ORDER == BIG_ENDIAN
#define bSwap32(x) (x)
#define bSwap16(x) (x)
#endif
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

View File

@ -0,0 +1,175 @@
/* snesrc - SNES Recompiler
*
* Copyright notice for this file:
* Copyright (C) 2005 Parasyte
*
* Based on uCON64's N64 checksum algorithm by Andreas Sterbenz
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include <stdio.h>
#include <stdlib.h>
#define ROL(i, b) (((i) << (b)) | ((i) >> (32 - (b))))
#define BYTES2LONG(b) ( (b)[0] << 24 | \
(b)[1] << 16 | \
(b)[2] << 8 | \
(b)[3] )
#define N64_HEADER_SIZE 0x40
#define N64_BC_SIZE (0x1000 - N64_HEADER_SIZE)
#define N64_CRC1 0x10
#define N64_CRC2 0x14
#define CHECKSUM_START 0x00001000
#define CHECKSUM_LENGTH 0x00100000
#define CHECKSUM_CIC6102 0xF8CA4DDC
#define CHECKSUM_CIC6103 0xA3886759
#define CHECKSUM_CIC6105 0xDF26F436
#define CHECKSUM_CIC6106 0x1FEA617A
unsigned int crc_table[256];
void gen_table()
{
uint32_t crc, poly;
int32_t i, j;
poly = 0xEDB88320;
for (i = 0; i < 256; i++) {
crc = i;
for (j = 8; j > 0; j--) {
if (crc & 1) crc = (crc >> 1) ^ poly;
else crc >>= 1;
}
crc_table[i] = crc;
}
}
unsigned int crc32(unsigned char *data, int len)
{
uint32_t crc, i;
crc = ~0;
for (i = 0; i < len; i++)
crc = (crc >> 8) ^ crc_table[(crc ^ data[i]) & 0xFF];
return ~crc;
}
int N64GetCIC(unsigned char *data)
{
switch (crc32(&data[N64_HEADER_SIZE], N64_BC_SIZE)) {
case 0x6170A4A1: return 6101;
case 0x90BB6CB5: return 6102;
case 0x0B050EE0: return 6103;
case 0x98BC2C86: return 6105;
case 0xACC8580A: return 6106;
}
return 0;
}
int N64CalcCRC(unsigned int *crc, unsigned char *data)
{
int32_t bootcode, i;
uint32_t seed, r, d;
uint32_t t1, t2, t3;
uint32_t t4, t5, t6;
switch ((bootcode = N64GetCIC(data))) {
case 6101:
case 6102:
seed = CHECKSUM_CIC6102;
break;
case 6103:
seed = CHECKSUM_CIC6103;
break;
case 6105:
seed = CHECKSUM_CIC6105;
break;
case 6106:
seed = CHECKSUM_CIC6106;
break;
default:
return 0;
}
t1 = t2 = t3 = t4 = t5 = t6 = seed;
i = CHECKSUM_START;
while (i < (CHECKSUM_START + CHECKSUM_LENGTH)) {
d = BYTES2LONG(&data[i]);
if ((t6 + d) < t6) t4++;
t6 += d;
t3 ^= d;
r = ROL(d, (d & 0x1F));
t5 += r;
if (t2 > d) t2 ^= r;
else t2 ^= t6 ^ d;
if (bootcode == 6105) t1 += BYTES2LONG(&data[N64_HEADER_SIZE + 0x0710 + (i & 0xFF)]) ^ d;
else t1 += t5 ^ d;
i += 4;
}
if (bootcode == 6103) {
crc[0] = (t6 ^ t4) + t3;
crc[1] = (t5 ^ t2) + t1;
}
else if (bootcode == 6106) {
crc[0] = (t6 * t4) + t3;
crc[1] = (t5 * t2) + t1;
}
else {
crc[0] = t6 ^ t4 ^ t3;
crc[1] = t5 ^ t2 ^ t1;
}
return 1;
}
void fix_crc (uint8_t* buffer)
{
uint8_t CRC1[4];
uint8_t CRC2[4];
uint32_t crc[2];
uint32_t i;
gen_table();
/* If the CRC calc was successful, do stuff */
if(N64CalcCRC(crc, buffer))
{
for(i = 0; i < 4; i++)
{
CRC1[i] = (crc[0] >> (24-8*i))&0xFF;
CRC2[i] = (crc[1] >> (24-8*i))&0xFF;
}
/* If the CRC1 changed, update it */
if(crc[0] != BYTES2LONG(&buffer[N64_CRC1]))
memcpy(buffer + N64_CRC1, CRC1, 4);
/* If the CRC2 changed, update it */
if (crc[1] != BYTES2LONG(&buffer[N64_CRC2]))
memcpy(buffer + N64_CRC2, CRC2, 4);
}
}

View File

@ -0,0 +1,259 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include "crc.c"
#include "bSwap.h"
#define UINTSIZE 0x01000000
#define COMPSIZE 0x02000000
#define DCMPSIZE 0x04000000
/* Structs */
typedef struct
{
uint32_t startV; /* Start Virtual Address */
uint32_t endV; /* End Virtual Address */
uint32_t startP; /* Start Physical Address */
uint32_t endP; /* End Phycical Address */
}
table_t;
/* Functions */
void decompress(uint8_t*, uint8_t*, int32_t);
table_t getTabEnt(uint32_t);
void setTabEnt(uint32_t, table_t);
void loadROM(char*);
int32_t findTable();
/* Globals */
uint8_t* inROM;
uint8_t* outROM;
uint32_t* inTable;
uint32_t* outTable;
int main(int argc, char** argv)
{
FILE* outFile;
int32_t tabStart, tabSize, tabCount;
int32_t size, i;
table_t tab, tempTab;
char* name;
/* Check arguments */
if(argc < 2 || argc > 3)
{
fprintf(stderr, "Usage: %s [Input ROM] <Output ROM>\n", argv[0]);
exit(1);
}
/* If no output file was specified, make one */
/* Add "-decomp.z64" to the end of the input file */
if(argc != 3)
{
size = strlen(argv[1]);
name = malloc(size + 7);
strcpy(name, argv[1]);
for(i = size; i >= 0; i--)
{
if(name[i] == '.')
{
name[i] = '\0';
break;
}
}
strcat(name, "-decomp.z64");
}
else
name = argv[2];
inROM = malloc(DCMPSIZE);
outROM = malloc(DCMPSIZE);
/* Load the ROM into inROM and outROM */
loadROM(argv[1]);
/* Find table offsets */
tabStart = findTable();
inTable = (uint32_t*)(inROM + tabStart);
outTable = (uint32_t*)(outROM + tabStart);
tab = getTabEnt(2);
tabSize = tab.endV - tab.startV;
tabCount = tabSize / 16;
/* Set everything past the table in outROM to 0 */
memset((uint8_t*)(outROM) + tab.endV, 0, DCMPSIZE - tab.endV);
for(i = 3; i < tabCount; i++)
{
tempTab = getTabEnt(i);
size = tempTab.endV - tempTab.startV;
/* dmaTable will have 0xFFFFFFFF if file doesn't exist */
if(tempTab.startP >= DCMPSIZE || tempTab.endP == 0xFFFFFFFF)
continue;
/* Copy if not compressed, decompress otherwise */
if(tempTab.endP == 0x00000000)
memcpy((void*)outROM + tempTab.startV, (void*)inROM + tempTab.startP, size);
else
decompress((void*)inROM + tempTab.startP, (void*)outROM + tempTab.startV, size);
/* Clean up outROM's table */
tempTab.startP = tempTab.startV;
tempTab.endP = 0x00000000;
setTabEnt(i, tempTab);
}
/* Fix the CRC */
fix_crc(outROM);
/* Write the new ROM */
outFile = fopen(name, "wb");
fwrite(outROM, sizeof(uint32_t), UINTSIZE, outFile);
fclose(outFile);
free(outROM);
free(inROM);
if(argc != 3)
free(name);
return(0);
}
int32_t findTable()
{
int32_t i, temp;
uint32_t* tempROM;
tempROM = (uint32_t*)inROM;
/* 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 table\n");
exit(1);
}
void loadROM(char* name)
{
uint32_t size, i;
uint16_t* tempROM;
FILE* romFile;
/* Open file, make sure it exists */
romFile = fopen(name, "rb");
if(romFile == NULL)
{
perror(name);
exit(1);
}
/* Find size of file */
fseek(romFile, 0, SEEK_END);
size = ftell(romFile);
fseek(romFile, 0, SEEK_SET);
/* If it's not the right size, exit */
if(size != COMPSIZE)
{
fprintf(stderr, "Error, %s is not the correct size", name);
exit(1);
}
/* Read to inROM, close romFile, and copy to outROM */
fread(inROM, sizeof(char), size, romFile);
tempROM = (uint16_t*)inROM;
fclose(romFile);
/* bSwap16 if needed */
if (inROM[0] == 0x37)
for (i = 0; i < UINTSIZE; i++)
tempROM[i] = bSwap16(tempROM[i]);
memcpy(outROM, inROM, size);
}
table_t getTabEnt(uint32_t i)
{
table_t tab;
/* First 32 bytes are VROM start address, next 32 are VROM end address */
/* Next 32 bytes are Physical start address, last 32 are Physical end address */
tab.startV = bSwap32(inTable[i*4] );
tab.endV = bSwap32(inTable[(i*4)+1]);
tab.startP = bSwap32(inTable[(i*4)+2]);
tab.endP = bSwap32(inTable[(i*4)+3]);
return(tab);
}
void setTabEnt(uint32_t i, table_t tab)
{
/* First 32 bytes are VROM start address, next 32 are VROM end address */
/* Next 32 bytes are Physical start address, last 32 are Physical end address */
outTable[i*4] = bSwap32(tab.startV);
outTable[(i*4)+1] = bSwap32(tab.endV);
outTable[(i*4)+2] = bSwap32(tab.startP);
outTable[(i*4)+3] = bSwap32(tab.endP);
}
void decompress(uint8_t* source, uint8_t* decomp, int32_t decompSize)
{
uint32_t srcPlace = 0, dstPlace = 0;
uint32_t i, dist, copyPlace, numBytes;
uint8_t codeByte, byte1, byte2;
uint8_t bitCount = 0;
source += 0x10;
while(dstPlace < decompSize)
{
/* If there are no more bits to test, get a new byte */
if(!bitCount)
{
codeByte = source[srcPlace++];
bitCount = 8;
}
/* If bit 7 is a 1, just copy 1 byte from source to destination */
/* Else do some decoding */
if(codeByte & 0x80)
{
decomp[dstPlace++] = source[srcPlace++];
}
else
{
/* Get 2 bytes from source */
byte1 = source[srcPlace++];
byte2 = source[srcPlace++];
/* Calculate distance to move in destination */
/* And the number of bytes to copy */
dist = ((byte1 & 0xF) << 8) | byte2;
copyPlace = dstPlace - (dist + 1);
numBytes = byte1 >> 4;
/* Do more calculations on the number of bytes to copy */
if(!numBytes)
numBytes = source[srcPlace++] + 0x12;
else
numBytes += 2;
/* Copy data from a previous point in destination */
/* to current point in destination */
for(i = 0; i < numBytes; i++)
decomp[dstPlace++] = decomp[copyPlace++];
}
/* Set up for the next read cycle */
codeByte = codeByte << 1;
bitCount--;
}
}

View File

@ -0,0 +1,29 @@
[
{
"region_name": "Bottom of the Well",
"dungeon": "Bottom of the Well",
"locations": {
"Bottom of the Well MQ Compass Chest": "
Kokiri_Sword or (Sticks and logic_child_deadhand)",
"Bottom of the Well MQ Map Chest": "
can_play(Zeldas_Lullaby) or has_explosives",
"Bottom of the Well MQ Lens of Truth Chest": "
has_explosives and
(Small_Key_Bottom_of_the_Well, 2)",
"Bottom of the Well MQ Dead Hand Freestanding Key": "
has_explosives or (logic_botw_mq_dead_hand_key and Boomerang)",
"Bottom of the Well MQ East Inner Room Freestanding Key": "
can_play(Zeldas_Lullaby) or has_explosives",
"Bottom of the Well MQ GS Basement": "can_child_attack",
"Bottom of the Well MQ GS West Inner Room": "
can_child_attack and
(can_play(Zeldas_Lullaby) or has_explosives)",
"Bottom of the Well MQ GS Coffin Room": "
can_child_attack and
(Small_Key_Bottom_of_the_Well, 2)"
},
"exits": {
"Kakariko Village": "True"
}
}
]

View File

@ -0,0 +1,50 @@
[
{
"region_name": "Bottom of the Well",
"dungeon": "Bottom of the Well",
"exits": {
"Bottom of the Well Main Area" : "is_child and (can_child_attack or Nuts)"
}
},
{
"region_name": "Bottom of the Well Main Area",
"dungeon": "Bottom of the Well",
"locations": {
"Bottom of the Well Front Left Fake Wall Chest": "True",
"Bottom of the Well Front Center Bombable Chest": "has_explosives",
"Bottom of the Well Right Bottom Fake Wall Chest": "True",
"Bottom of the Well Compass Chest": "True",
"Bottom of the Well Center Skulltula Chest": "True",
"Bottom of the Well Back Left Bombable Chest": "has_explosives",
"Bottom of the Well Freestanding Key": "Sticks or can_use(Dins_Fire) or Boomerang",
"Bottom of the Well Lens of Truth Chest": "
(can_play(Zeldas_Lullaby) or (Small_Key_Bottom_of_the_Well,3)) and
(Kokiri_Sword or Sticks)",
"Bottom of the Well Invisible Chest": "(can_play(Zeldas_Lullaby) or (Small_Key_Bottom_of_the_Well,3))",
"Bottom of the Well Underwater Front Chest": "can_play(Zeldas_Lullaby)",
"Bottom of the Well Underwater Left Chest": "can_play(Zeldas_Lullaby)",
"Bottom of the Well Map Chest": "
has_explosives or
(((Small_Key_Bottom_of_the_Well, 3) or
can_use(Dins_Fire) or Sticks) and
Progressive_Strength_Upgrade)",
"Bottom of the Well Fire Keese Chest": "
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well Like Like Chest": "
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well GS West Inner Room": "
(Boomerang or can_isg) and
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well GS East Inner Room": "
(Boomerang or can_isg) and
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well GS Like Like Cage": "
(Boomerang or can_isg) and
(Small_Key_Bottom_of_the_Well, 3)",
"Stick Pot": "True"
},
"exits": {
"Bottom of the Well" : "True"
}
}
]

View File

@ -0,0 +1,49 @@
[
{
"region_name": "Deku Tree Lobby",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Map Chest": "True",
"Deku Tree MQ Slingshot Chest": "can_child_attack",
"Deku Tree MQ Slingshot Room Back Chest": "Sticks or can_use(Dins_Fire)",
"Deku Tree MQ Basement Chest": "Sticks or can_use(Dins_Fire)",
"Deku Tree MQ GS Lobby": "can_child_attack"
},
"exits": {
"KF Outside Deku Tree": "True",
"Deku Tree Compass Room": "Slingshot and (Sticks or can_use(Dins_Fire))",
"Deku Tree Boss Room": "Slingshot and (Sticks or can_use(Dins_Fire))"
}
},
{
"region_name": "Deku Tree Compass Room",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Compass Chest": "True",
"Deku Tree MQ GS Compass Room": "
Boomerang and ((Bombs and can_play(Song_of_Time)) or has_bombchus)"
},
"exits": {
"Deku Tree Lobby": "True"
}
},
{
"region_name": "Deku Tree Boss Room",
"dungeon": "Deku Tree",
"events": {
"Deku Tree Clear": "Buy_Deku_Shield and (Kokiri_Sword or Sticks)"
},
"locations": {
"Deku Tree MQ Before Spinning Log Chest": "True",
"Deku Tree MQ After Spinning Log Chest": "can_play(Song_of_Time)",
"Deku Tree MQ GS Basement Graves Room": "Boomerang and can_play(Song_of_Time)",
"Deku Tree MQ GS Basement Back Room": "Boomerang",
"Deku Tree MQ Deku Scrub": "True",
"Deku Tree Queen Gohma Heart": "Buy_Deku_Shield and (Kokiri_Sword or Sticks)",
"Queen Gohma": "Buy_Deku_Shield and (Kokiri_Sword or Sticks)"
},
"exits": {
"Deku Tree Lobby": "True"
}
}
]

View File

@ -0,0 +1,57 @@
[
{
"region_name": "Deku Tree Lobby",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree Map Chest": "True",
"Deku Tree Compass Chest": "True",
"Deku Tree Compass Room Side Chest": "True",
"Deku Tree Basement Chest": "is_adult or can_child_attack or Nuts",
"Deku Tree GS Compass Room": "is_adult or can_child_attack",
"Deku Tree GS Basement Vines": "is_adult or can_child_attack",
"Deku Tree GS Basement Gate": "is_adult or can_child_attack",
"Deku Tree GS Basement Back Room": "
(here(has_fire_source_with_torch or can_use(Bow)) and
here(can_use(Slingshot) or can_use(Bow)) and
here(can_blast_or_smash) and
here(can_use(Hookshot) or can_use(Boomerang) or can_hover)) or
(is_child and (has_explosives or Blue_Fire) and
(can_use(Boomerang) or can_hover) and has_fire_source_with_torch) or
(is_adult and has_explosives and Progressive_Hookshot and Hover_Boots and can_live_dmg(0.5) and
(Bow or has_fire_source))"
},
"exits": {
"Deku Tree Slingshot Room": "here(has_shield)",
"Deku Tree Boss Room": "here(has_fire_source_with_torch()) or can_shield
or is_adult"
}
},
{
"region_name": "Deku Tree Slingshot Room",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree Slingshot Chest": "True",
"Deku Tree Slingshot Room Side Chest": "True"
},
"exits": {
"Deku Tree Lobby": "True"
}
},
{
"region_name": "Deku Tree Boss Room",
"dungeon": "Deku Tree",
"events": {
"Deku Tree Clear": "(Nuts or can_use(Slingshot) or has_bombchus or can_use(Hookshot) or can_use(Bow) or can_use(Boomerang)) and
((here(has_shield or can_use(Megaton_Hammer)) and (is_adult or Kokiri_Sword or Sticks)) or is_adult)"
},
"locations": {
"Deku Tree Queen Gohma Heart": "(Nuts or can_use(Slingshot) or has_bombchus or can_use(Hookshot) or can_use(Bow) or can_use(Boomerang)) and
((here(has_shield or can_use(Megaton_Hammer)) and (is_adult or Kokiri_Sword or Sticks)) or is_adult)",
"Queen Gohma": "(Nuts or can_use(Slingshot) or has_bombchus or can_use(Hookshot) or can_use(Bow) or can_use(Boomerang)) and
((here(has_shield or can_use(Megaton_Hammer)) and (is_adult or Kokiri_Sword or Sticks)) or is_adult)"
},
"exits": {
"Deku Tree Lobby": "True"
}
}
]

View File

@ -0,0 +1,65 @@
[
{
"region_name": "Dodongos Cavern Beginning",
"dungeon": "Dodongos Cavern",
"exits": {
"Dodongos Cavern Entryway": "True",
"Dodongos Cavern Lobby": "can_blast_or_smash or Progressive_Strength_Upgrade"
}
},
{
"region_name": "Dodongos Cavern Lobby",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern MQ Map Chest": "True",
"Dodongos Cavern MQ Compass Chest": "is_adult or can_child_attack or Nuts",
"Dodongos Cavern MQ Larvae Room Chest": "can_use(Sticks) or has_fire_source",
"Dodongos Cavern MQ Torch Puzzle Room Chest": "
can_blast_or_smash or can_use(Sticks) or can_use(Dins_Fire) or
(is_adult and (logic_dc_jump or Hover_Boots or Progressive_Hookshot))",
"Dodongos Cavern MQ Bomb Bag Chest": "
is_adult or
(Slingshot and
(has_explosives or
((Sticks or can_use(Dins_Fire)) and can_take_damage)))",
"Dodongos Cavern MQ GS Song of Time Block Room": "
can_play(Song_of_Time) and (can_child_attack or is_adult)",
"Dodongos Cavern MQ GS Larvae Room": "can_use(Sticks) or has_fire_source",
"Dodongos Cavern MQ GS Lizalfos Room": "can_blast_or_smash",
"Dodongos Cavern MQ GS Scrub Room": "
(can_use(Boomerang) and (Slingshot or (can_become_adult and has_explosives)) and
(has_explosives or (Progressive_Strength_Upgrade and
(Megaton_Hammer or
((Sticks or can_use(Dins_Fire) or
(can_become_adult and (logic_dc_jump or Hover_Boots))) and
can_take_damage))))) or
(can_use(Hookshot) and (has_explosives or Progressive_Strength_Upgrade or
Bow or can_use(Dins_Fire)))",
"Dodongos Cavern MQ Deku Scrub Lobby Rear": "can_stun_deku",
"Dodongos Cavern MQ Deku Scrub Lobby Front": "can_stun_deku",
"Dodongos Cavern MQ Deku Scrub Staircase": "can_stun_deku",
"Dodongos Cavern MQ Deku Scrub Side Room Near Lower Lizalfos": "
is_adult or has_explosives or
((Sticks or can_use(Dins_Fire)) and can_take_damage)",
"Dodongos Cavern Gossip Stone": "True"
},
"exits": {
"Dodongos Cavern Boss Area": "has_explosives"
}
},
{
"region_name": "Dodongos Cavern Boss Area",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern MQ Under Grave Chest": "True",
"Dodongos Cavern Boss Room Chest": "True",
"Dodongos Cavern King Dodongo Heart": "
(Bombs or Progressive_Strength_Upgrade) and
(is_adult or Sticks or Kokiri_Sword)",
"King Dodongo": "
(Bombs or Progressive_Strength_Upgrade) and
(is_adult or Sticks or Kokiri_Sword)",
"Dodongos Cavern MQ GS Back Area": "True"
}
}
]

View File

@ -0,0 +1,91 @@
[
{
"region_name": "Dodongos Cavern Beginning",
"dungeon": "Dodongos Cavern",
"exits": {
"Dodongos Cavern Lobby": "
here(can_blast_or_smash or Blue_Fire) or Progressive_Strength_Upgrade"
}
},
{
"region_name": "Dodongos Cavern Lobby",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern Map Chest": "True",
"Dodongos Cavern Compass Chest": "
is_adult or Sticks or
(can_use(Dins_Fire) and (Slingshot or has_explosives or Kokiri_Sword))
or can_shield",
"Dodongos Cavern GS Side Room Near Lower Lizalfos": "
has_explosives or is_adult or can_child_attack or Blue_Fire",
"Dodongos Cavern GS Scarecrow": "True",
"Dodongos Cavern Deku Scrub Side Room Near Dodongos": "
is_adult or Slingshot or Sticks or
has_explosives or Kokiri_Sword or can_shield",
"Dodongos Cavern Deku Scrub Lobby": "True",
"Dodongos Cavern Gossip Stone": "True"
},
"exits": {
"Dodongos Cavern Beginning": "True",
"Dodongos Cavern Climb": "
here(
(is_adult or can_shield or
( (Sticks or can_use(Dins_Fire)) and
(Slingshot or Sticks or has_explosives or Kokiri_Sword)
)
)
) and
(has_explosives or Progressive_Strength_Upgrade or
can_use(Dins_Fire) or can_use(Bow) )"
}
},
{
"region_name": "Dodongos Cavern Climb",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern Bomb Flower Platform Chest": "True",
"Dodongos Cavern GS Vines Above Stairs": "True",
"Dodongos Cavern Deku Scrub Near Bomb Bag Right": "can_blast_or_smash or (Blue_Fire and can_stun_deku)",
"Dodongos Cavern Deku Scrub Near Bomb Bag Left": "can_blast_or_smash or (Blue_Fire and can_stun_deku)"
},
"exits": {
"Dodongos Cavern Lobby": "True",
"Dodongos Cavern Far Bridge": "True"
}
},
{
"region_name": "Dodongos Cavern Far Bridge",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern Bomb Bag Chest": "True",
"Dodongos Cavern End of Bridge Chest": "can_blast_or_smash or Blue_Fire",
"Dodongos Cavern GS Alcove Above Stairs": "can_use(Hookshot) or can_use(Boomerang)
or (can_live_dmg(0.5) and can_use(Hover_Boots)) or can_hover"
},
"exits": {
"Dodongos Cavern Boss Area": "has_explosives",
"Dodongos Cavern Lobby": "True"
}
},
{
"region_name": "Dodongos Cavern Boss Area",
"dungeon": "Dodongos Cavern",
"locations": {
"Dodongos Cavern Boss Room Chest": "True",
"Dodongos Cavern King Dodongo Heart": "
(Bombs or Progressive_Strength_Upgrade or
(has_bombchus and ((is_adult and can_shield) or (is_child and can_isg)))
) and
(is_adult or Sticks or Kokiri_Sword)",
"King Dodongo": "
(Bombs or Progressive_Strength_Upgrade or
(has_bombchus and ((is_adult and can_shield) or (is_child and can_isg)))
) and
(is_adult or Sticks or Kokiri_Sword)",
"Dodongos Cavern GS Back Room": "True"
},
"exits": {
"Dodongos Cavern Lobby": "True"
}
}
]

View File

@ -0,0 +1,95 @@
[
{
"region_name": "Fire Temple Lower",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Map Room Side Chest": "True",
"Fire Temple MQ Near Boss Chest": "
((Hover_Boots or (logic_fire_mq_near_boss and Bow)) and has_fire_source) or
(Progressive_Hookshot and (can_use(Fire_Arrows) or
(can_use(Dins_Fire) and
((damage_multiplier != 'ohko' and damage_multiplier != 'quadruple') or can_use(Goron_Tunic) or
Bow or (Progressive_Hookshot, 2)))))"
},
"exits": {
"DMC Central": "True",
"Fire Boss Room": "
has_fire_source and can_use(Goron_Tunic) and Megaton_Hammer and Boss_Key_Fire_Temple and
(logic_fire_boss_door_jump or Hover_Boots or at('Fire Temple Upper', True))",
"Fire Lower Locked Door": "
(Small_Key_Fire_Temple, 5) and
(has_explosives or Megaton_Hammer or Progressive_Hookshot)",
"Fire Big Lava Room": "can_use(Megaton_Hammer)"
}
},
{
"region_name": "Fire Lower Locked Door",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Megaton Hammer Chest": "True",
"Fire Temple MQ Map Chest": "Megaton_Hammer"
}
},
{
"region_name": "Fire Big Lava Room",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Boss Key Chest": "
has_fire_source and (Bow or logic_fire_mq_bk_chest) and
Progressive_Hookshot",
"Fire Temple MQ Big Lava Room Blocked Door Chest": "
has_fire_source and Progressive_Hookshot and has_explosives",
"Fire Temple MQ GS Big Lava Room Open Door": "True"
},
"exits": {
"Fire Lower Maze": "
can_use(Goron_Tunic) and (Small_Key_Fire_Temple, 2) and has_fire_source"
}
},
{
"region_name": "Fire Lower Maze",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Lizalfos Maze Lower Chest": "True",
"Fire Temple MQ Lizalfos Maze Side Room Chest": "
has_explosives and (logic_fire_mq_maze_side_room or can_use(Hookshot))"
},
"exits": {
"Fire Upper Maze": "
(has_explosives or logic_rusted_switches) and can_use(Hookshot)"
}
},
{
"region_name": "Fire Upper Maze",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Lizalfos Maze Upper Chest": "True",
"Fire Temple MQ Compass Chest": "has_explosives",
"Fire Temple MQ GS Skull On Fire": "
can_play(Song_of_Time) or can_use(Longshot)"
},
"exits": {
"Fire Temple Upper": "(Small_Key_Fire_Temple, 3) and Bow"
}
},
{
"region_name": "Fire Temple Upper",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple MQ Freestanding Key": "True",
"Fire Temple MQ Chest On Fire": "(Small_Key_Fire_Temple, 4)",
"Fire Temple MQ GS Fire Wall Maze Side Room": "
can_play(Song_of_Time) or Hover_Boots",
"Fire Temple MQ GS Fire Wall Maze Center": "has_explosives",
"Fire Temple MQ GS Above Fire Wall Maze": "(Small_Key_Fire_Temple, 5)"
}
},
{
"region_name": "Fire Boss Room",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Volvagia Heart": "True",
"Volvagia": "True"
}
}
]

View File

@ -0,0 +1,96 @@
[
{
"region_name": "Fire Temple Lower",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Near Boss Chest" : "True",
"Fire Temple Flare Dancer Chest": "
((Small_Key_Fire_Temple, 8) or not keysanity) and (can_use(Megaton_Hammer) or can_use(Hookshot) or has_explosives)",
"Fire Temple Boss Key Chest": "(
((Small_Key_Fire_Temple, 8) or not keysanity) and can_use(Megaton_Hammer)) or (can_mega and can_use(Hookshot))",
"Fire Temple Volvagia Heart": "
(can_use(Goron_Tunic) or (Fairy and has_explosives)) and can_use(Megaton_Hammer) and
(Boss_Key_Fire_Temple or at('Fire Temple Flame Maze', True))",
"Volvagia": "
(can_use(Goron_Tunic) or (Fairy and has_explosives)) and can_use(Megaton_Hammer) and
(Boss_Key_Fire_Temple or at('Fire Temple Flame Maze', True))",
"Fire Temple GS Boss Key Loop": "
((Small_Key_Fire_Temple, 8) or not keysanity)"
},
"exits": {
"Fire Temple Big Lava Room":"(Small_Key_Fire_Temple, 2)"
}
},
{
"region_name": "Fire Temple Big Lava Room",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Big Lava Room Lower Open Door Chest": "True",
"Fire Temple Big Lava Room Blocked Door Chest": "has_explosives",
"Fire Temple GS Song of Time Room": "is_adult"
},
"exits": {
"Fire Temple Lower": "True",
"Fire Temple Middle": "
(can_use(Goron_Tunic) or Fairy) and (Small_Key_Fire_Temple, 4) and
(has_explosives or can_use(Bow) or can_use(Hookshot))"
}
},
{
"region_name": "Fire Temple Middle",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Boulder Maze Lower Chest": "True",
"Fire Temple Boulder Maze Upper Chest": "(Small_Key_Fire_Temple, 6)
or (has_explosives and can_live_dmg(0.5) and (Small_Key_Fire_Temple, 4))",
"Fire Temple Boulder Maze Side Room Chest": "True",
"Fire Temple Boulder Maze Shortcut Chest": "((Small_Key_Fire_Temple, 6) and has_explosives) or
(((has_explosives and can_live_dmg(0.5)) or can_weirdshot) and (Small_Key_Fire_Temple, 4))",
"Fire Temple Scarecrow Chest": "(
(Small_Key_Fire_Temple, 6) or (has_explosives and can_live_dmg(0.5) and (Small_Key_Fire_Temple, 4)) )
and (can_use(Scarecrow) or can_hover)",
"Fire Temple Map Chest": "
(Small_Key_Fire_Temple, 6) or ((Small_Key_Fire_Temple, 5) and can_use(Bow)) or
(has_explosives and can_live_dmg(0.5) and (Small_Key_Fire_Temple, 4))",
"Fire Temple GS Boulder Maze": "(Small_Key_Fire_Temple, 4) and has_explosives",
"Fire Temple GS Scarecrow Climb": "(
(Small_Key_Fire_Temple, 6) or (has_explosives and can_live_dmg(0.5) and (Small_Key_Fire_Temple, 4)) )
and (can_use(Scarecrow) or can_hover)",
"Fire Temple GS Scarecrow Top": "(
(Small_Key_Fire_Temple, 6) or (has_explosives and can_live_dmg(0.5) and (Small_Key_Fire_Temple, 4)) )
and (can_use(Scarecrow) or can_hover)"
},
"exits": {
"Fire Temple Flame Maze": "(Small_Key_Fire_Temple, 7)"
}
},
{
"region_name": "Fire Temple Flame Maze",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Compass Chest": "(Small_Key_Fire_Temple, 7)"
},
"exits": {
"Fire Temple Upper": "(Small_Key_Fire_Temple, 7)"
}
},
{
"region_name": "Fire Temple Upper",
"dungeon": "Fire Temple",
"locations": {
"Fire Temple Highest Goron Chest": "(
can_use(Megaton_Hammer) or (has_explosives and can_live_dmg(1.0))
) or
(
(can_mega and can_use(Hookshot)) or
(can_play(Song_of_Time) and
(can_use(Megaton_Hammer) or can_use(Hover_Boots) or (has_explosives and can_live_dmg(0.5)) )
)
)
",
"Fire Temple Megaton Hammer Chest": "has_explosives or
can_use(Megaton_Hammer)"
}
}
]

View File

@ -0,0 +1,146 @@
[
{
"region_name": "Forest Temple Lobby",
"dungeon": "Forest Temple",
"locations": {
#If entrances are shuffled, Adult might lose access to the Kokiri forest
#Babas for nuts. As this is sphere 0 for both ages, this case is covered
#by putting the babas here.
"Forest Temple Deku Baba Nuts": "True",
"Forest Temple Deku Baba Sticks": "True",
"Forest Temple MQ First Room Chest": "True",
"Forest Temple MQ GS First Hallway": "True"
},
"exits": {
"Sacred Forest Meadow": "True",
"Forest Temple Central Area": "(Small_Key_Forest_Temple, 1)"
}
},
{
"region_name": "Forest Temple Central Area",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Wolfos Chest": "can_play(Song_of_Time)",
"Forest Temple MQ GS Block Push Room": "True"
},
"exits": {
"Forest Temple NW Outdoors": "can_use(Bow)",
"Forest Temple NE Outdoors": "can_use(Bow)",
"Forest Temple After Block Puzzle": "
(has_bombchus and logic_forest_mq_block_puzzle) or Progressive_Strength_Upgrade",
"Forest Temple Outdoor Ledge": "
(has_bombchus and logic_forest_mq_block_puzzle) or
Progressive_Strength_Upgrade or can_use(Hover_Boots)",
"Forest Temple Boss Region": "
Forest_Temple_Jo_and_Beth and Forest_Temple_Amy_and_Meg"
}
},
{
"region_name": "Forest Temple After Block Puzzle",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Boss Key Chest": "(Small_Key_Forest_Temple, 3)"
},
"exits": {
"Forest Temple Bow Region": "(Small_Key_Forest_Temple, 4)"
}
},
{
"region_name": "Forest Temple Outdoor Ledge",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Redead Chest": "True"
},
"exits": {
"Forest Temple NW Outdoors": "True"
}
},
{
"region_name": "Forest Temple NW Outdoors",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ GS Level Island Courtyard": "True"
},
"exits": {
"Forest Temple NE Outdoors": "
can_use(Iron_Boots) or can_use(Longshot) or
(Progressive_Scale, 2) or (logic_forest_well_swim and can_use(Hookshot))",
"Forest Temple Outdoors Top Ledges": "can_use(Fire_Arrows)"
}
},
{
"region_name": "Forest Temple NE Outdoors",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Well Chest": "can_use(Bow)",
"Forest Temple MQ GS Raised Island Courtyard": "True",
"Forest Temple MQ GS Well": "can_use(Iron_Boots) or can_use(Bow)"
},
"exits": {
"Forest Temple Outdoors Top Ledges": "
can_use(Longshot) or can_use(Hover_Boots) or can_play(Song_of_Time) or
(logic_forest_vines and can_use(Hookshot))",
"Forest Temple NE Outdoors Ledge": "can_use(Longshot)"
}
},
{
"region_name": "Forest Temple Outdoors Top Ledges",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Raised Island Courtyard Upper Chest": "True"
},
"exits": {
"Forest Temple NE Outdoors": "True"
}
},
{
"region_name": "Forest Temple NE Outdoors Ledge",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Raised Island Courtyard Lower Chest": "True"
},
"exits": {
"Forest Temple NE Outdoors": "True",
"Forest Temple Falling Room": "can_play(Song_of_Time)"
}
},
{
"region_name": "Forest Temple Bow Region",
"dungeon": "Forest Temple",
"events": {
"Forest Temple Jo and Beth": "can_use(Bow)"
},
"locations": {
"Forest Temple MQ Bow Chest": "True",
"Forest Temple MQ Map Chest": "can_use(Bow)",
"Forest Temple MQ Compass Chest": "can_use(Bow)"
},
"exits": {
"Forest Temple Falling Room": "
(Small_Key_Forest_Temple, 5) and
(can_use(Bow) or can_use(Dins_Fire))" # Only 5 keys because the door you could waste your key on is the door you're trying to use keys to get to.
}
},
{
"region_name": "Forest Temple Falling Room",
"dungeon": "Forest Temple",
"events": {
"Forest Temple Amy and Meg": "can_use(Bow) and (Small_Key_Forest_Temple, 6)"
},
"locations": {
"Forest Temple MQ Falling Ceiling Room Chest": "True"
},
"exits": {
"Forest Temple NE Outdoors Ledge": "True"
}
},
{
"region_name": "Forest Temple Boss Region",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple MQ Basement Chest": "True",
"Forest Temple Phantom Ganon Heart": "Boss_Key_Forest_Temple",
"Phantom Ganon": "Boss_Key_Forest_Temple"
}
}
]

View File

@ -0,0 +1,163 @@
[
{
"region_name": "Forest Temple Lobby",
"dungeon": "Forest Temple",
"locations": {
#If entrances are shuffled, Adult might lose access to the Kokiri forest
#Babas for nuts. As this is sphere 0 for both ages, this case is covered
#by putting the babas here.
"Deku Baba Nuts": "is_adult",
"Deku Baba Sticks": "is_adult",
"Forest Temple First Room Chest": "True",
"Forest Temple First Stalfos Chest": "can_jumpslash",
"Forest Temple GS First Room": "can_use(Dins_Fire) or can_use_projectile or (can_jumpslash and can_live_dmg(0.5))",
"Forest Temple GS Lobby": "can_use(Hookshot) or can_use(Boomerang) or can_hover"
},
"exits": {
"Forest Temple NW Outdoors": "True",
"Forest Temple NE Outdoors": "can_use(Bow) or can_use(Slingshot)",
"Forest Temple Block Push Room": "(Small_Key_Forest_Temple, 1)",
"Forest Temple Basement": "(Forest_Temple_Jo_and_Beth and Forest_Temple_Amy_and_Meg) or (can_use(Hover_Boots) and can_mega)",
"Forest Temple Falling Room": "can_hover or (can_use(Hover_Boots) and Bombs and can_live_dmg(0.5))",
"Forest Temple Boss Room": "is_adult"
}
},
{
"region_name": "Forest Temple NW Outdoors",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple GS Level Island Courtyard": "
can_use(Longshot) or
at('Forest Temple Outside Upper Ledge', can_use(Hookshot) or can_hover)"
},
"exits": {
"Forest Temple Outdoors High Balconies": "
is_adult or
(has_explosives or
((can_use(Boomerang) or Nuts or Buy_Deku_Shield) and
(Sticks or Kokiri_Sword or can_use(Slingshot))))",
"Forest Temple Outside Upper Ledge": "can_hover or (can_use(Hover_Boots) and has_explosives and can_live_dmg(0.5))",
"Forest Temple Boss Room": "is_child and can_live_dmg(0.5)"
}
},
{
"region_name": "Forest Temple NE Outdoors",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Raised Island Courtyard Chest": "
can_use(Hookshot) or
at('Forest Temple Falling Room', True) or can_hover",
"Forest Temple GS Raised Island Courtyard": "
can_use(Hookshot) or can_hover or
at('Forest Temple Falling Room', can_use(Bow) or can_use(Dins_Fire) or has_explosives or can_use(Boomerang))"
},
"exits": {
"Forest Temple Outdoors High Balconies": "can_use(Hookshot)",
#Longshot can grab some very high up vines to drain the well.
"Forest Temple NW Outdoors": "can_use(Iron_Boots) or (Progressive_Scale, 2)",
"Forest Temple Lobby": "True",
"Forest Temple Falling Room": "can_hover or
at('Forest Temple Outdoors High Balconies',
can_use(Hover_Boots) and can_use(Scarecrow) and can_live_dmg(0.5))"
}
},
{
"region_name": "Forest Temple Outdoors High Balconies",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Well Chest": "True",
"Forest Temple Map Chest": "True"
},
"exits": {
"Forest Temple NW Outdoors": "True",
"Forest Temple NE Outdoors": "True"
}
},
{
"region_name": "Forest Temple Falling Room",
"dungeon": "Forest Temple",
"events": {
"Forest Temple Amy and Meg": "can_use(Bow)"
},
"locations": {
"Forest Temple Falling Ceiling Room Chest": "True"
},
"exits": {
"Forest Temple NE Outdoors": "True"
}
},
{
"region_name": "Forest Temple Block Push Room",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Eye Switch Chest": "
Progressive_Strength_Upgrade and (can_use(Bow) or can_use(Slingshot))"
},
"exits": {
"Forest Temple Outside Upper Ledge": "can_jumpslash",
"Forest Temple Bow Region": "
(Progressive_Strength_Upgrade or (can_mega and (can_hover or Hover_Boots)))
and (Small_Key_Forest_Temple, 3) and is_adult",
"Forest Temple Straightened Hall": "
(Progressive_Strength_Upgrade or (is_adult and can_mega and (can_hover or Hover_Boots)))
and (Small_Key_Forest_Temple, 2) and (can_use(Bow) or (can_use(Slingshot) and can_hover))"
}
},
{
"region_name": "Forest Temple Straightened Hall",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Boss Key Chest": "True"
},
"exits": {
"Forest Temple Outside Upper Ledge": "True"
}
},
{
"region_name": "Forest Temple Outside Upper Ledge",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Floormaster Chest": "is_adult or can_child_damage"
},
"exits": {
"Forest Temple NW Outdoors": "True"
}
},
{
"region_name": "Forest Temple Bow Region",
"dungeon": "Forest Temple",
"events": {
"Forest Temple Jo and Beth": "can_use(Bow)"
},
"locations": {
"Forest Temple Bow Chest": "True",
"Forest Temple Red Poe Chest": "can_use(Bow)",
"Forest Temple Blue Poe Chest": "can_use(Bow)"
},
"exits": {
"Forest Temple Falling Room": "
(Small_Key_Forest_Temple, 5) and (Bow or can_use(Dins_Fire))"
}
},
{
"region_name": "Forest Temple Basement",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Basement Chest": "True",
"Forest Temple GS Basement": "can_use(Hookshot) or can_use(Boomerang) or can_hover"
},
"exits":{
"Forest Temple Boss Room": "Boss_Key_Forest_Temple"
}
},
{
"region_name": "Forest Temple Boss Room",
"dungeon": "Forest Temple",
"locations": {
"Forest Temple Phantom Ganon Heart": "(can_use(Hookshot) or can_use(Bow)) or
(can_use(Slingshot) and (Kokiri_Sword or Sticks))",
"Phantom Ganon": "(can_use(Hookshot) or can_use(Bow)) or
(can_use(Slingshot) and (Kokiri_Sword or Sticks))"
}
}
]

View File

@ -0,0 +1,123 @@
[
{
"region_name": "Ganons Castle Lobby",
"exits": {
"Ganons Castle Grounds": "True",
"Ganons Castle Forest Trial": "True",
"Ganons Castle Fire Trial": "True",
"Ganons Castle Water Trial": "True",
"Ganons Castle Shadow Trial": "True",
"Ganons Castle Spirit Trial": "True",
"Ganons Castle Light Trial": "can_use(Golden_Gauntlets)",
"Ganons Castle Tower": "
(skipped_trials[Forest] or 'Forest Trial Clear') and
(skipped_trials[Fire] or 'Fire Trial Clear') and
(skipped_trials[Water] or 'Water Trial Clear') and
(skipped_trials[Shadow] or 'Shadow Trial Clear') and
(skipped_trials[Spirit] or 'Spirit Trial Clear') and
(skipped_trials[Light] or 'Light Trial Clear')",
"Ganons Castle Deku Scrubs": "logic_lens_castle_mq or can_use(Lens_of_Truth)"
}
},
{
"region_name": "Ganons Castle Deku Scrubs",
"locations": {
"Ganons Castle MQ Deku Scrub Center-Left": "True",
"Ganons Castle MQ Deku Scrub Center": "True",
"Ganons Castle MQ Deku Scrub Center-Right": "True",
"Ganons Castle MQ Deku Scrub Left": "True",
"Ganons Castle MQ Deku Scrub Right": "True"
}
},
{
"region_name": "Ganons Castle Forest Trial",
"dungeon": "Ganons Castle",
"events": {
"Forest Trial Clear": "can_use(Light_Arrows) and can_play(Song_of_Time)"
},
"locations": {
"Ganons Castle MQ Forest Trial Eye Switch Chest": "Bow",
"Ganons Castle MQ Forest Trial Frozen Eye Switch Chest": "has_fire_source",
"Ganons Castle MQ Forest Trial Freestanding Key": "Progressive_Hookshot"
}
},
{
"region_name": "Ganons Castle Fire Trial",
"dungeon": "Ganons Castle",
"events": {
"Fire Trial Clear": "
can_use(Goron_Tunic) and can_use(Golden_Gauntlets) and
can_use(Light_Arrows) and (can_use(Longshot) or Hover_Boots)"
}
},
{
"region_name": "Ganons Castle Water Trial",
"dungeon": "Ganons Castle",
"events": {
"Water Trial Clear": "
Blue_Fire and can_use(Light_Arrows) and
(Small_Key_Ganons_Castle, 3)"
},
"locations": {
"Ganons Castle MQ Water Trial Chest": "Blue_Fire",
"Blue Fire": "has_bottle"
}
},
{
"region_name": "Ganons Castle Shadow Trial",
"dungeon": "Ganons Castle",
"events": {
"Shadow Trial Clear": "
can_use(Light_Arrows) and (logic_lens_castle_mq or can_use(Lens_of_Truth)) and
(Hover_Boots or (Progressive_Hookshot and has_fire_source))"
},
"locations": {
"Ganons Castle MQ Shadow Trial Bomb Flower Chest": "
(Bow and (Progressive_Hookshot or Hover_Boots)) or
(Hover_Boots and (logic_lens_castle_mq or can_use(Lens_of_Truth)) and
(has_explosives or Progressive_Strength_Upgrade or can_use(Dins_Fire)))",
"Ganons Castle MQ Shadow Trial Eye Switch Chest": "
Bow and (logic_lens_castle_mq or can_use(Lens_of_Truth)) and
(Hover_Boots or (Progressive_Hookshot and has_fire_source))"
}
},
{
"region_name": "Ganons Castle Spirit Trial",
"dungeon": "Ganons Castle",
"events": {
"Spirit Trial Clear": "
can_use(Light_Arrows) and Megaton_Hammer and
has_bombchus and Fire_Arrows and Mirror_Shield"
},
"locations": {
"Ganons Castle MQ Spirit Trial First Chest": "(Bow or logic_rusted_switches) and Megaton_Hammer",
"Ganons Castle MQ Spirit Trial Invisible Chest": "
(Bow or logic_rusted_switches) and Megaton_Hammer and
has_bombchus and (logic_lens_castle_mq or can_use(Lens_of_Truth))",
"Ganons Castle MQ Spirit Trial Sun Front Left Chest": "
Megaton_Hammer and has_bombchus and
can_use(Fire_Arrows) and Mirror_Shield",
"Ganons Castle MQ Spirit Trial Sun Back Left Chest": "
Megaton_Hammer and has_bombchus and
can_use(Fire_Arrows) and Mirror_Shield",
"Ganons Castle MQ Spirit Trial Golden Gauntlets Chest": "
Megaton_Hammer and has_bombchus and
can_use(Fire_Arrows) and Mirror_Shield",
"Ganons Castle MQ Spirit Trial Sun Back Right Chest": "
Megaton_Hammer and has_bombchus and
can_use(Fire_Arrows) and Mirror_Shield"
}
},
{
"region_name": "Ganons Castle Light Trial",
"dungeon": "Ganons Castle",
"events": {
"Light Trial Clear": "
can_use(Light_Arrows) and Progressive_Hookshot and
(Small_Key_Ganons_Castle, 3)"
},
"locations": {
"Ganons Castle MQ Light Trial Lullaby Chest": "can_play(Zeldas_Lullaby)"
}
}
]

View File

@ -0,0 +1,115 @@
[
{
"region_name": "Ganons Castle Lobby",
"dungeon": "Ganons Castle",
"exits": {
"Ganons Castle Grounds": "True",
"Ganons Castle Forest Trial": "True",
"Ganons Castle Fire Trial": "True",
"Ganons Castle Water Trial": "True",
"Ganons Castle Shadow Trial": "True",
"Ganons Castle Spirit Trial": "True",
"Ganons Castle Light Trial": "can_use(Golden_Gauntlets) or (Bombs and can_shield) or (can_mega and can_use(Hover_Boots))",
"Ganons Castle Tower": "True",
"Ganons Castle Deku Scrubs": "True"
}
},
{
"region_name": "Ganons Castle Deku Scrubs",
"dungeon": "Ganons Castle",
"locations": {
"Ganons Castle Deku Scrub Center-Left": "True",
"Ganons Castle Deku Scrub Center-Right": "True",
"Ganons Castle Deku Scrub Right": "True",
"Ganons Castle Deku Scrub Left": "True"
}
},
{
"region_name": "Ganons Castle Forest Trial",
"dungeon": "Ganons Castle",
"events": {
"Forest Trial Clear": "can_use(Light_Arrows) and (Fire_Arrows or Dins_Fire)"
},
"locations": {
"Ganons Castle Forest Trial Chest": "True"
}
},
{
"region_name": "Ganons Castle Fire Trial",
"dungeon": "Ganons Castle",
"events": {
"Fire Trial Clear": "
(can_use(Goron_Tunic) or Fairy) and (can_use(Golden_Gauntlets) or can_use(Hover_Boots)) and
can_use(Light_Arrows) and (can_use(Longshot) or (can_mega and can_use(Hover_Boots)))"
}
},
{
"region_name": "Ganons Castle Water Trial",
"dungeon": "Ganons Castle",
"events": {
"Water Trial Clear": "(Blue_Fire or can_use(Hookshot)) and Megaton_Hammer and can_use(Light_Arrows)"
},
"locations": {
"Ganons Castle Water Trial Left Chest": "True",
"Ganons Castle Water Trial Right Chest": "True",
"Blue Fire": "has_bottle"
}
},
{
"region_name": "Ganons Castle Shadow Trial",
"dungeon": "Ganons Castle",
"events": {
"Shadow Trial Clear": "
can_use(Light_Arrows) and Megaton_Hammer and (
(has_bombchus and can_isg) or
(has_explosives and Hover_Boots and can_shield) or
( (can_use(Longshot) and (Hover_Boots or can_use(Dins_Fire)))
or can_use(Fire_Arrows) ))"
},
"locations": {
"Ganons Castle Shadow Trial Front Chest": "
can_use(Fire_Arrows) or can_use(Hookshot) or
Hover_Boots or can_play(Song_of_Time) or can_mega",
"Ganons Castle Shadow Trial Golden Gauntlets Chest": "(has_bombchus and can_isg) or
(has_explosives and Hover_Boots and can_shield) or
( (can_use(Longshot) and (Hover_Boots or can_use(Dins_Fire)))
or can_use(Fire_Arrows) )"
}
},
{
"region_name": "Ganons Castle Spirit Trial",
"dungeon": "Ganons Castle",
"events": {
# includes below 2 reqs here
"Spirit Trial Clear": "
can_use(Light_Arrows) and Mirror_Shield and
(has_bombchus or (can_shield and (can_use(Longshot) or Bow)))"
},
"locations": {
"Ganons Castle Spirit Trial Crystal Switch Chest": "True",
# include req for silver rupees here
"Ganons Castle Spirit Trial Invisible Chest": "
has_bombchus or (can_shield and (can_use(Longshot) or Bow))"
}
},
{
"region_name": "Ganons Castle Light Trial",
"dungeon": "Ganons Castle",
"events": {
"Light Trial Clear": "
can_use(Light_Arrows) and (Progressive_Hookshot or (has_explosives and can_shield)) and
(Small_Key_Ganons_Castle, 2)"
},
"locations": {
"Ganons Castle Light Trial First Left Chest": "True",
"Ganons Castle Light Trial Second Left Chest": "True",
"Ganons Castle Light Trial Third Left Chest": "True",
"Ganons Castle Light Trial First Right Chest": "True",
"Ganons Castle Light Trial Second Right Chest": "True",
"Ganons Castle Light Trial Third Right Chest": "True",
"Ganons Castle Light Trial Invisible Enemies Chest": "True",
"Ganons Castle Light Trial Lullaby Chest": "
can_play(Zeldas_Lullaby) and (Small_Key_Ganons_Castle, 1)"
}
}
]

View File

@ -0,0 +1,79 @@
[
{
"region_name": "Gerudo Training Grounds Lobby",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ Lobby Left Chest": "True",
"Gerudo Training Grounds MQ Lobby Right Chest": "True",
"Gerudo Training Grounds MQ Hidden Ceiling Chest": "logic_lens_gtg_mq or can_use(Lens_of_Truth)",
"Gerudo Training Grounds MQ Maze Path First Chest": "True",
"Gerudo Training Grounds MQ Maze Path Second Chest": "True",
"Gerudo Training Grounds MQ Maze Path Third Chest": "(Small_Key_Gerudo_Training_Grounds, 1)"
},
"exits": {
"Gerudo Fortress": "True",
"Gerudo Training Grounds Left Side": "has_fire_source",
"Gerudo Training Grounds Right Side": "Bow"
}
},
{
"region_name": "Gerudo Training Grounds Right Side",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ Dinolfos Chest": "True",
"Gerudo Training Grounds MQ Underwater Silver Rupee Chest": "
(Hover_Boots or at('Gerudo Training Grounds Central Maze Right', can_use(Longshot) or Bow)) and
has_fire_source and Iron_Boots and (logic_fewer_tunic_requirements or can_use(Zora_Tunic)) and
can_take_damage",
"Wall Fairy": "has_bottle and can_use(Bow)" #in the Dinalfos room shoot the Gerudo symbol above the door to the lava room.
}
},
{
"region_name": "Gerudo Training Grounds Left Side",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ First Iron Knuckle Chest": "True"
},
"exits": {
"Gerudo Training Grounds Stalfos Room": "can_use(Longshot) or (logic_gtg_mq_with_hookshot and can_use(Hookshot))"
}
},
{
"region_name": "Gerudo Training Grounds Stalfos Room",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ Before Heavy Block Chest": "True",
"Gerudo Training Grounds MQ Heavy Block Chest": "can_use(Silver_Gauntlets)",
"Blue Fire": "has_bottle"
},
"exits": {
"Gerudo Training Grounds Back Areas": "can_play(Song_of_Time) and (logic_lens_gtg_mq or can_use(Lens_of_Truth)) and Blue_Fire"
}
},
{
"region_name": "Gerudo Training Grounds Back Areas",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ Eye Statue Chest": "Bow",
"Gerudo Training Grounds MQ Second Iron Knuckle Chest": "True",
"Gerudo Training Grounds MQ Flame Circle Chest": "True"
},
"exits": {
"Gerudo Training Grounds Central Maze Right": "Megaton_Hammer",
"Gerudo Training Grounds Right Side": "can_use(Longshot)"
}
},
{
"region_name": "Gerudo Training Grounds Central Maze Right",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds MQ Maze Right Central Chest": "True",
"Gerudo Training Grounds MQ Maze Right Side Chest": "True",
"Gerudo Training Grounds MQ Ice Arrows Chest": "
(Small_Key_Gerudo_Training_Grounds, 3)"
},
"exits": {
"Gerudo Training Grounds Right Side": "True"
}
}
]

View File

@ -0,0 +1,112 @@
[
{
"region_name": "Gerudo Training Grounds Lobby",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Lobby Left Chest": "can_use(Bow) or can_use(Slingshot)",
"Gerudo Training Grounds Lobby Right Chest": "can_use(Bow) or can_use(Slingshot)",
"Gerudo Training Grounds Stalfos Chest": "can_jumpslash",
"Gerudo Training Grounds Beamos Chest": "has_explosives and can_jumpslash",
"Wall Fairy": "has_bottle and can_use(Bow)" #in the Beamos room shoot the Gerudo symbol above the door to the lava room.
},
"exits": {
"Gerudo Training Grounds Heavy Block Room": "True",
"Gerudo Training Grounds Lava Room": "
here(has_explosives and can_jumpslash)",
"Gerudo Training Grounds Central Maze": "True"
}
},
{
"region_name": "Gerudo Training Grounds Central Maze",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Hidden Ceiling Chest": "(Small_Key_Gerudo_Training_Grounds, 3)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives and (can_live_dmg(0.5) or can_use(Nayrus_Love)))",
"Gerudo Training Grounds Maze Path First Chest": "(Small_Key_Gerudo_Training_Grounds, 4)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives and (can_live_dmg(1.0) or can_use(Nayrus_Love)))",
"Gerudo Training Grounds Maze Path Second Chest": "(Small_Key_Gerudo_Training_Grounds, 6)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives and (can_live_dmg(1.0) or can_use(Nayrus_Love)))",
"Gerudo Training Grounds Maze Path Third Chest": "(Small_Key_Gerudo_Training_Grounds, 7)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives and (can_live_dmg(0.5) or can_use(Nayrus_Love)))",
"Gerudo Training Grounds Maze Path Final Chest": "(Small_Key_Gerudo_Training_Grounds, 9)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives and (can_live_dmg(0.5) or can_use(Nayrus_Love)))"
},
"exits": {
"Gerudo Training Grounds Central Maze Right": "(Small_Key_Gerudo_Training_Grounds, 9)
or (can_use(Hookshot) and can_mega) or (is_child and has_explosives)"
}
},
{
"region_name": "Gerudo Training Grounds Central Maze Right",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Maze Right Central Chest": "True",
"Gerudo Training Grounds Maze Right Side Chest": "True",
"Gerudo Training Grounds Freestanding Key": "True"
},
"exits": {
"Gerudo Training Grounds Lava Room": "True"
}
},
{
"region_name": "Gerudo Training Grounds Lava Room",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Underwater Silver Rupee Chest": "
can_use(Hookshot) and (can_play(Song_of_Time) or can_mega) and Iron_Boots"
},
"exits": {
"Gerudo Training Grounds Central Maze Right": "can_play(Song_of_Time) or is_child
or (can_use(Hookshot) and can_use(Hover_Boots) and can_shield and Bombs)",
"Gerudo Training Grounds Hammer Room": "can_use(Hookshot)"
}
},
{
"region_name": "Gerudo Training Grounds Hammer Room",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Hammer Room Clear Chest": "True",
"Gerudo Training Grounds Hammer Room Switch Chest": "can_use(Megaton_Hammer) or
can_live_dmg(0.5) or can_use(Nayrus_Love)"
},
"exits": {
"Gerudo Training Grounds Eye Statue Lower": "can_use(Bow)",
"Gerudo Training Grounds Lava Room": "True"
}
},
{
"region_name": "Gerudo Training Grounds Eye Statue Lower",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Eye Statue Chest": "can_use(Bow)"
},
"exits": {
"Gerudo Training Grounds Hammer Room": "True"
}
},
{
"region_name": "Gerudo Training Grounds Eye Statue Upper",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Near Scarecrow Chest": "can_use(Bow)"
},
"exits": {
"Gerudo Training Grounds Eye Statue Lower": "True"
}
},
{
"region_name": "Gerudo Training Grounds Heavy Block Room",
"dungeon": "Gerudo Training Grounds",
"locations": {
"Gerudo Training Grounds Before Heavy Block Chest": "True",
"Gerudo Training Grounds Heavy Block First Chest": "can_use(Silver_Gauntlets)",
"Gerudo Training Grounds Heavy Block Second Chest": "can_use(Silver_Gauntlets)",
"Gerudo Training Grounds Heavy Block Third Chest": "can_use(Silver_Gauntlets)",
"Gerudo Training Grounds Heavy Block Fourth Chest": "can_use(Silver_Gauntlets)"
},
"exits": {
"Gerudo Training Grounds Eye Statue Upper": "can_use(Hookshot) or can_hover or
(is_adult and (Hover_Boots or (can_shield and Bombs)))"
}
}
]

View File

@ -0,0 +1,35 @@
[
{
"region_name": "Ice Cavern Beginning",
"dungeon": "Ice Cavern",
"exits": {
"Zoras Fountain": "True",
"Ice Cavern": "True"
}
},
{
"region_name": "Ice Cavern",
"dungeon": "Ice Cavern",
"locations": {
"Blue Fire": "is_adult and has_bottle"
},
"exits": {
"Ice Cavern Interior": "Blue_Fire"
}
},
{
"region_name": "Ice Cavern Interior",
"dungeon": "Ice Cavern",
"locations": {
"Ice Cavern MQ Map Chest": "True",
"Ice Cavern MQ Compass Chest": "True",
"Ice Cavern MQ Iron Boots Chest": "True",
"Ice Cavern MQ Freestanding PoH": "has_explosives",
"Sheik in Ice Cavern": "True",
"Ice Cavern MQ GS Red Ice": "can_play(Song_of_Time)",
"Ice Cavern MQ GS Ice Block": "True",
"Ice Cavern MQ GS Scarecrow": "
can_use(Scarecrow) or (Hover_Boots and can_use(Longshot))"
}
}
]

View File

@ -0,0 +1,31 @@
[
{ ##Child must be able to hover to get in here so can_hover is always true as child
"region_name": "Ice Cavern Beginning",
"dungeon": "Ice Cavern",
"exits": {
"Ice Cavern": "is_adult or Sticks or can_use(Dins_Fire)"
}
},
{
"region_name": "Ice Cavern",
"dungeon": "Ice Cavern",
"locations": {
# because child can always hover here, access to this chest is ageless and True
"Ice Cavern Map Chest": "Blue_Fire or (has_explosives and (can_live_dmg(0.5) or can_use(Nayrus_Love)))",
"Ice Cavern Compass Chest": "(is_adult or (is_child and (can_live_dmg(0.5) or can_use(Nayrus_Love)) )) and
(Blue_Fire or (has_explosives and (can_live_dmg(0.5) or can_use(Nayrus_Love)) ))",
"Ice Cavern Freestanding PoH": "(is_adult or (is_child and (can_live_dmg(0.5) or can_use(Nayrus_Love)))) and
(Blue_Fire or (is_adult and Bombs and can_shield and (can_live_dmg(0.5) or can_use(Nayrus_Love)))
or can_use(Boomerang))",
"Ice Cavern Iron Boots Chest": "(can_jumpslash or can_use(Slingshot) or can_use(Dins_Fire)) and (Blue_Fire or (can_use(Hover_Boots) and can_shield) or can_mega)",
"Sheik in Ice Cavern": "(can_jumpslash or can_use(Slingshot) or can_use(Dins_Fire)) and (Blue_Fire or (can_use(Hover_Boots) and can_shield) or can_mega)",
"Ice Cavern GS Spinning Scythe Room": "can_use(Hookshot) or can_use(Boomerang) or
(can_use(Hover_Boots) and can_mega)",
"Ice Cavern GS Heart Piece Room": "(is_adult or (is_child and (can_live_dmg(0.5) or can_use(Nayrus_Love)) )) and
(can_use(Boomerang) or can_use(Hookshot))",
"Ice Cavern GS Push Block Room": "(Blue_Fire or (can_use(Hover_Boots) and can_shield) or (is_adult and can_mega) or (is_child and can_live_dmg(0.5) and has_explosives))
and (can_use(Hookshot) or can_use(Boomerang) or can_hover)",
"Blue Fire": "has_bottle"
}
}
]

View File

@ -0,0 +1,61 @@
[
{
"region_name": "Jabu Jabus Belly Beginning",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly MQ Map Chest": "has_explosives",
"Jabu Jabus Belly MQ First Room Side Chest": "Slingshot"
},
"exits": {
"Zoras Fountain": "True",
"Jabu Jabus Belly Main": "Slingshot"
}
},
{
"region_name": "Jabu Jabus Belly Main",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly MQ Second Room Lower Chest": "True",
"Jabu Jabus Belly MQ Compass Chest": "True",
"Jabu Jabus Belly MQ Basement Near Vines Chest": "True",
"Jabu Jabus Belly MQ Basement Near Switches Chest": "True",
"Jabu Jabus Belly MQ Boomerang Room Small Chest": "True",
"Jabu Jabus Belly MQ Boomerang Chest": "True",
"Jabu Jabus Belly MQ GS Boomerang Chest Room": "
can_play(Song_of_Time) or (logic_jabu_mq_sot_gs and Boomerang)"
},
"exits": {
"Jabu Jabus Belly Beginning": "True",
"Jabu Jabus Belly Depths": "has_explosives and Boomerang"
}
},
{
"region_name": "Jabu Jabus Belly Depths",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly MQ Falling Like Like Room Chest": "True",
"Jabu Jabus Belly MQ GS Tailpasaran Room": "Sticks or can_use(Dins_Fire)",
"Jabu Jabus Belly MQ GS Invisible Enemies Room": "logic_lens_jabu_mq or can_use(Lens_of_Truth)"
},
"exits": {
"Jabu Jabus Belly Main": "True",
"Jabu Jabus Belly Boss Area": "
Sticks or (Kokiri_Sword and can_use(Dins_Fire))"
}
},
{
"region_name": "Jabu Jabus Belly Boss Area",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly MQ Cow" : "can_play(Eponas_Song)",
"Jabu Jabus Belly MQ Second Room Upper Chest": "True",
"Jabu Jabus Belly MQ Near Boss Chest": "True",
"Jabu Jabus Belly Barinade Heart": "True",
"Barinade": "True",
"Jabu Jabus Belly MQ GS Near Boss": "True"
},
"exits": {
"Jabu Jabus Belly Main": "True"
}
}
]

View File

@ -0,0 +1,49 @@
[
{
"region_name": "Jabu Jabus Belly Beginning",
"dungeon": "Jabu Jabus Belly",
"exits": {
"Jabu Jabus Belly Main": "can_use_projectile or can_isg"
}
},
{
"region_name": "Jabu Jabus Belly Main",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly Boomerang Chest": "True",
"Jabu Jabus Belly GS Water Switch Room": "True",
"Jabu Jabus Belly GS Lobby Basement Lower": "can_use(Boomerang) or can_use(Hookshot) or can_hover",
"Jabu Jabus Belly GS Lobby Basement Upper": "can_use(Boomerang) or can_use(Hookshot) or can_hover",
"Jabu Jabus Belly Deku Scrub": "True"
},
"exits": {
"Jabu Jabus Belly Beginning": "True",
"Jabu Jabus Belly Depths": "True"
}
},
{
"region_name": "Jabu Jabus Belly Depths",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly Map Chest": "can_use(Boomerang)",
"Jabu Jabus Belly Compass Chest": "can_use(Boomerang) or Fairy
or (is_adult and (can_live_dmg(0.5) or Hover_Boots) and has_explosives)"
},
"exits": {
"Jabu Jabus Belly Main": "True",
"Jabu Jabus Belly Boss Area": "can_use(Boomerang) or can_use(Hover_Boots) or can_mega"
}
},
{
"region_name": "Jabu Jabus Belly Boss Area",
"dungeon": "Jabu Jabus Belly",
"locations": {
"Jabu Jabus Belly Barinade Heart": "can_use(Boomerang) and (Sticks or Kokiri_Sword)",
"Barinade": "can_use(Boomerang) and (Sticks or Kokiri_Sword)",
"Jabu Jabus Belly GS Near Boss": "True"
},
"exits": {
"Jabu Jabus Belly Main": "True"
}
}
]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
[
{
"region_name": "Shadow Temple Entryway",
"dungeon": "Shadow Temple",
"exits": {
"Shadow Temple Beginning": "
(logic_lens_shadow_mq or can_use(Lens_of_Truth)) and
(can_use(Hover_Boots) or can_use(Hookshot))"
}
},
{
"region_name": "Shadow Temple Beginning",
"dungeon": "Shadow Temple",
"exits": {
"Shadow Temple Entryway": "True",
"Shadow Temple First Beamos": "can_use(Fire_Arrows) or Hover_Boots",
"Shadow Temple Dead Hand Area": "has_explosives and (Small_Key_Shadow_Temple, 6)"
}
},
{
"region_name": "Shadow Temple Dead Hand Area",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple MQ Compass Chest": "True",
"Shadow Temple MQ Hover Boots Chest": "can_play(Song_of_Time) and Bow"
}
},
{
"region_name": "Shadow Temple First Beamos",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple MQ Map Chest": "True",
"Shadow Temple MQ Early Gibdos Chest": "True",
"Shadow Temple MQ Near Ship Invisible Chest": "True"
},
"exits": {
"Shadow Temple Huge Pit": "has_explosives and (Small_Key_Shadow_Temple, 2)"
}
},
{
"region_name": "Shadow Temple Huge Pit",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple MQ Invisible Blades Visible Chest": "can_play(Song_of_Time)",
"Shadow Temple MQ Invisible Blades Invisible Chest": "can_play(Song_of_Time)",
"Shadow Temple MQ Beamos Silver Rupees Chest": "can_use(Longshot)",
"Shadow Temple MQ Falling Spikes Lower Chest": "True",
"Shadow Temple MQ Falling Spikes Upper Chest": "Progressive_Strength_Upgrade",
"Shadow Temple MQ Falling Spikes Switch Chest": "Progressive_Strength_Upgrade",
"Shadow Temple MQ Invisible Spikes Chest": "Hover_Boots and (Small_Key_Shadow_Temple, 3)",
"Shadow Temple MQ Stalfos Room Chest": "
Hover_Boots and (Small_Key_Shadow_Temple, 3) and Progressive_Hookshot",
"Shadow Temple MQ GS Falling Spikes Room": "Progressive_Hookshot"
},
"exits": {
"Shadow Temple Wind Tunnel": "
Hover_Boots and Progressive_Hookshot and (Small_Key_Shadow_Temple, 4)"
}
},
{
"region_name": "Shadow Temple Wind Tunnel",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple MQ Wind Hint Chest": "True",
"Shadow Temple MQ After Wind Enemy Chest": "True",
"Shadow Temple MQ After Wind Hidden Chest": "True",
"Shadow Temple MQ GS Wind Hint Room": "True",
"Shadow Temple MQ GS After Wind": "True"
},
"exits": {
"Shadow Temple Beyond Boat": "
can_play(Zeldas_Lullaby) and (Small_Key_Shadow_Temple, 5)"
}
},
{
"region_name": "Shadow Temple Beyond Boat",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Bongo Bongo Heart": "Bow and Boss_Key_Shadow_Temple",
"Bongo Bongo": "Bow and Boss_Key_Shadow_Temple",
"Shadow Temple MQ GS After Ship": "True",
"Shadow Temple MQ GS Near Boss": "Bow"
},
"exits": {
"Shadow Temple Invisible Maze": "
Bow and can_play(Song_of_Time) and can_use(Longshot)"
}
},
{
"region_name": "Shadow Temple Invisible Maze",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple MQ Spike Walls Left Chest": "(Small_Key_Shadow_Temple, 6)",
"Shadow Temple MQ Boss Key Chest": "(Small_Key_Shadow_Temple, 6)",
"Shadow Temple MQ Bomb Flower Chest": "True",
"Shadow Temple MQ Freestanding Key": "True"
}
}
]

View File

@ -0,0 +1,108 @@
[
{
"region_name": "Shadow Temple Entryway",
"dungeon": "Shadow Temple",
"exits": {
"Shadow Temple Beginning": "can_use(Hover_Boots) or can_use(Hookshot) or can_mega"
}
},
{
"region_name": "Shadow Temple Beginning",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Map Chest": "can_jumpslash or (can_use(Dins_Fire) and has_projectile(child))",
"Shadow Temple Hover Boots Chest": "can_jumpslash"
},
"exits": {
"Shadow Temple Entryway": "True",
"Shadow Temple First Beamos": "can_use(Hover_Boots) or can_mega",
"Shadow Boss": "can_hover and has_explosives and can_use(Hover_Boots) and
can_live_dmg(2.0)"
}
},
{
"region_name": "Shadow Temple First Beamos",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Compass Chest": "can_jumpslash or can_use(Dins_Fire)",
"Shadow Temple Early Silver Rupee Chest": "is_adult or (is_child and can_hover)"
},
"exits": {
"Shadow Temple Huge Pit": "has_explosives and (Small_Key_Shadow_Temple, 5)",
"Shadow Temple Boat": "can_jumpslash"
}
},
{
"region_name": "Shadow Temple Huge Pit",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Invisible Blades Visible Chest": "can_jumpslash or Slingshot",
"Shadow Temple Invisible Blades Invisible Chest": "can_jumpslash or Slingshot",
"Shadow Temple Falling Spikes Lower Chest": "True",
"Shadow Temple Falling Spikes Upper Chest": "is_adult or can_hover",
"Shadow Temple Falling Spikes Switch Chest": "is_adult or can_hover",
"Shadow Temple Invisible Spikes Chest": "(Small_Key_Shadow_Temple, 5) and (can_jumpslash or can_use(Dins_Fire))",
"Shadow Temple Freestanding Key": "
(Small_Key_Shadow_Temple, 5) and (can_use(Hookshot) or can_hover)
and (Progressive_Strength_Upgrade or has_explosives)",
"Shadow Temple GS Like Like Room": "is_adult or can_use(Boomerang) or can_hover",
"Shadow Temple GS Falling Spikes Room": "can_use(Hookshot) or (is_adult and can_mega) or (is_child and can_hover)",
"Shadow Temple GS Single Giant Pot": "(Small_Key_Shadow_Temple, 5) and (can_use(Hookshot) or can_hover)"
},
"exits": {
"Shadow Temple Wind Tunnel": "(can_use(Hookshot) or can_hover) and (Small_Key_Shadow_Temple, 5)"
}
},
{
"region_name": "Shadow Temple Wind Tunnel",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Wind Hint Chest": "can_jumpslash or can_use(Dins_Fire)",
"Shadow Temple After Wind Enemy Chest": "can_jumpslash or can_use(Dins_Fire)",
"Shadow Temple After Wind Hidden Chest": "has_explosives"
},
"exits": {
"Shadow Temple Boat": "(Small_Key_Shadow_Temple, 5)",
"Shadow Temple Huge Pit": "can_hover"
}
},
{
"region_name": "Shadow Temple Boat",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple GS Near Ship": "can_use(Longshot) or (can_use(Hover_Boots) and can_mega)
or (is_child and can_hover)"
},
"exits": {
"Shadow Temple Wind Tunnel": "(Small_Key_Shadow_Temple,5)",
"Shadow Temple Beyond Boat": "can_play(Zeldas_Lullaby) and
(is_adult or (is_child and can_hover))"
}
},
{
"region_name": "Shadow Temple Beyond Boat",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Spike Walls Left Chest": "can_use(Dins_Fire) or can_live_dmg(0.5) or Fairy or can_use(Nayrus_Love) or
(can_use(Hookshot) and can_mega)",
"Shadow Temple Boss Key Chest": "can_use(Dins_Fire) or can_live_dmg(0.5) or Fairy or can_use(Nayrus_Love) or
(can_use(Hookshot) and can_mega)",
"Shadow Temple Invisible Floormaster Chest": "True",
"Shadow Temple GS Triple Giant Pot": "True"
},
"exits": {
"Shadow Boss": "(has_bombchus or can_use(Distant_Scarecrow) or Bow or
(can_mega and can_use(Hover_Boots)) or can_hover) and
(Boss_Key_Shadow_Temple or (has_explosives and is_adult)) and
(can_mega or can_use(Hover_Boots)) and (Small_Key_Shadow_Temple, 5)"
}
},
{
"region_name": "Shadow Boss",
"dungeon": "Shadow Temple",
"locations": {
"Shadow Temple Bongo Bongo Heart": "True",
"Bongo Bongo": "True"
}
}
]

View File

@ -0,0 +1,134 @@
[
{
"region_name": "Spirit Temple Lobby",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Entrance Front Left Chest": "True",
"Spirit Temple MQ Entrance Back Left Chest": "
here(can_blast_or_smash) and
(can_use(Slingshot) or can_use(Bow))",
"Spirit Temple MQ Entrance Back Right Chest": "
has_bombchus or can_use(Bow) or can_use(Hookshot) or
can_use(Slingshot) or can_use(Boomerang)"
},
"exits": {
"Desert Colossus": "True",
"Child Spirit Temple": "is_child",
"Adult Spirit Temple": "
has_bombchus and can_use(Longshot) and can_use(Silver_Gauntlets)"
}
},
{
"region_name": "Child Spirit Temple",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Map Room Enemy Chest": "
(Sticks or Kokiri_Sword) and
has_bombchus and Slingshot and can_use(Dins_Fire)",
"Spirit Temple MQ Map Chest": "
Sticks or Kokiri_Sword or has_explosives",
"Spirit Temple MQ Silver Block Hallway Chest": "
has_bombchus and (Small_Key_Spirit_Temple, 7) and Slingshot and
(can_use(Dins_Fire) or
here(is_adult and can_use(Longshot) and can_use(Silver_Gauntlets) and
(can_use(Fire_Arrows) or
(logic_spirit_mq_frozen_eye and Bow and can_play(Song_of_Time)))))"
},
"exits": {
"Spirit Temple Shared": "has_bombchus and (Small_Key_Spirit_Temple, 2)"
}
},
{
"region_name": "Adult Spirit Temple",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Child Hammer Switch Chest": "
(Small_Key_Spirit_Temple, 7) and Megaton_Hammer and can_play(Requiem_of_Spirit)",
"Spirit Temple MQ Child Climb South Chest": "(Small_Key_Spirit_Temple, 7)",
"Spirit Temple MQ Statue Room Lullaby Chest": "can_play(Zeldas_Lullaby)",
"Spirit Temple MQ Statue Room Invisible Chest": "logic_lens_spirit_mq or can_use(Lens_of_Truth)",
"Spirit Temple MQ Beamos Room Chest": "(Small_Key_Spirit_Temple, 5)",
"Spirit Temple MQ Chest Switch Chest": "
(Small_Key_Spirit_Temple, 5) and can_play(Song_of_Time)",
"Spirit Temple MQ Boss Key Chest": "
(Small_Key_Spirit_Temple, 5) and can_play(Song_of_Time) and Mirror_Shield",
"Spirit Temple MQ GS Nine Thrones Room West": "(Small_Key_Spirit_Temple, 7)",
"Spirit Temple MQ GS Nine Thrones Room North": "(Small_Key_Spirit_Temple, 7)"
},
"exits": {
"Lower Adult Spirit Temple": "
(can_use(Fire_Arrows) or
(logic_spirit_mq_lower_adult and can_use(Dins_Fire) and Bow)) and Mirror_Shield",
"Spirit Temple Shared": "True",
"Spirit Temple Boss Area": "
(Small_Key_Spirit_Temple, 6) and can_play(Zeldas_Lullaby) and Megaton_Hammer",
"Mirror Shield Hand": "
(Small_Key_Spirit_Temple, 5) and can_play(Song_of_Time) and (logic_lens_spirit_mq or can_use(Lens_of_Truth))"
}
},
{
"region_name": "Spirit Temple Shared",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Child Climb North Chest": "(Small_Key_Spirit_Temple, 6)",
"Spirit Temple MQ Compass Chest": "
((Small_Key_Spirit_Temple, 7) and can_use(Slingshot)) or
(can_use(Longshot) and can_use(Silver_Gauntlets) and Bow) or
(Slingshot and Bow)",
"Spirit Temple MQ Sun Block Room Chest": "
can_play(Song_of_Time) or
(can_use(Longshot) and can_use(Silver_Gauntlets))",
"Spirit Temple MQ GS Sun Block Room": "
(logic_spirit_mq_sun_block_gs and can_play(Song_of_Time) and Boomerang) or
(can_use(Longshot) and can_use(Silver_Gauntlets))"
},
"exits": {
"Silver Gauntlets Hand": "
((Small_Key_Spirit_Temple, 7) and
(can_play(Song_of_Time) or
(can_use(Longshot) and can_use(Silver_Gauntlets)))) or
((Small_Key_Spirit_Temple, 4) and can_play(Song_of_Time) and
(logic_lens_spirit_mq or can_use(Lens_of_Truth)))"
}
},
{
"region_name": "Lower Adult Spirit Temple",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Leever Room Chest": "True",
"Spirit Temple MQ Symphony Room Chest": "
(Small_Key_Spirit_Temple, 7) and Megaton_Hammer and Ocarina and
Song_of_Time and Eponas_Song and Suns_Song and
Song_of_Storms and Zeldas_Lullaby",
"Spirit Temple MQ Entrance Front Right Chest": "Megaton_Hammer",
"Spirit Temple MQ GS Leever Room": "True",
"Spirit Temple MQ GS Symphony Room": "
(Small_Key_Spirit_Temple, 7) and Megaton_Hammer and Ocarina and
Song_of_Time and Eponas_Song and Suns_Song and
Song_of_Storms and Zeldas_Lullaby"
}
},
{
"region_name": "Spirit Temple Boss Area",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple MQ Mirror Puzzle Invisible Chest": "logic_lens_spirit_mq or can_use(Lens_of_Truth)",
"Spirit Temple Twinrova Heart": "Mirror_Shield and Boss_Key_Spirit_Temple",
"Twinrova": "Mirror_Shield and Boss_Key_Spirit_Temple"
}
},
{
"region_name": "Mirror Shield Hand",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Mirror Shield Chest": "True"
}
},
{
"region_name": "Silver Gauntlets Hand",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Silver Gauntlets Chest": "True"
}
}
]

View File

@ -0,0 +1,171 @@
[
{
"region_name": "Spirit Temple Lobby",
"dungeon": "Spirit Temple",
"exits": {
"Desert Colossus": "True",
"Child Spirit Temple": "is_child or (is_adult and (can_mega or Hover_Boots))",
"Early Adult Spirit Temple": "can_use(Silver_Gauntlets) or can_use(Hover_Boots) or (is_adult and can_shield)"
}
},
{
"region_name": "Child Spirit Temple",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Child Bridge Chest": "is_adult or
(
(can_use(Boomerang) or Slingshot or has_bombchus or can_mega) and
(Sticks or has_explosives or
( (Nuts or can_use(Boomerang)) and
(can_use(Kokiri_Sword) or Slingshot) ) ))",
"Spirit Temple Child Early Torches Chest": "(is_adult and has_fire_source) or
(has_fire_source_with_torch and (here(is_adult) or
(
(can_use(Boomerang) or Slingshot or has_bombchus or can_mega) and
(Sticks or has_explosives or
( (Nuts or can_use(Boomerang)) and
(can_use(Kokiri_Sword) or Slingshot) ) ))))",
"Spirit Temple GS Metal Fence": "is_adult or
(
(can_use(Boomerang) or Slingshot or has_bombchus or can_mega) and
(Sticks or has_explosives or
( (Nuts or can_use(Boomerang)) and
(can_use(Kokiri_Sword) or Slingshot) ) ))",
"Nut Crate": "True"
},
"exits": {
"Child Spirit Temple Climb": "(Small_Key_Spirit_Temple, 2) and
(is_child or ((can_mega and can_use(Longshot)) or can_use(Hover_Boots))
)"
}
},
{
"region_name": "Child Spirit Temple Climb",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Child Climb North Chest": "at('Spirit Temple Central Chamber', True)
or is_child or has_projectile(adult)",
"Spirit Temple Child Climb East Chest": "at('Spirit Temple Central Chamber', True)
or is_child or has_projectile(adult)",
"Spirit Temple GS Sun on Floor Room": "can_use(Boomerang) or can_use(Hookshot) or
(can_child_damage and (can_live_dmg(0.5) or Fairy or can_use(Nayrus_Love))) or
(is_adult and (can_live_dmg(0.5) or Fairy or can_use(Nayrus_Love)))"
},
"exits": {
"Spirit Temple Central Chamber": "has_explosives"
}
},
{
"region_name": "Early Adult Spirit Temple",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Compass Chest": "can_play(Zeldas_Lullaby) and
(can_use(Hookshot) or can_hover) and has_projectile(either)",
"Spirit Temple Early Adult Right Chest": "has_projectile(either)",
"Spirit Temple First Mirror Left Chest": "(Small_Key_Spirit_Temple, 2)",
"Spirit Temple First Mirror Right Chest": "(Small_Key_Spirit_Temple, 2)",
"Spirit Temple GS Boulder Room": "has_projectile(either) and
(can_play(Song_of_Time) or can_use(Hover_Boots))"
},
"exits": {
"Spirit Temple Central Chamber": "(Small_Key_Spirit_Temple, 2)"
}
},
{
"region_name": "Spirit Temple Central Chamber",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Map Chest": "can_use(Bow) or has_fire_source_with_torch",
"Spirit Temple Sun Block Room Chest": "has_fire_source_with_torch or can_use(Bow)",
"Spirit Temple Statue Room Hand Chest": "can_play(Zeldas_Lullaby)
and can_jumpslash",
"Spirit Temple Statue Room Northeast Chest": "can_play(Zeldas_Lullaby) and can_jumpslash and
(can_use(Hookshot) or can_use(Hover_Boots) or can_mega)",
"Spirit Temple GS Hall After Sun Block Room": "can_use(Hookshot) or can_use(Boomerang) or can_hover",
"Spirit Temple GS Lobby": "can_use(Hookshot) or can_use(Boomerang) or can_hover
or can_use(Hover_Boots)"
},
"exits": {
"Silver Gauntlets Hand": "True",
# access via Early Adult Spirit Temple requires 2 keys (+ jumpslash + explosives)
# access to Early Adult Spirit Temple guaranteed via can_jumpslash from here
"Spirit Temple Beyond Central Locked Door": "can_jumpslash and (
(Small_Key_Spirit_Temple, 2) or
can_hover or
can_use(Hookshot)) and has_explosives",
"Child Spirit Temple Climb": "True",
"Spirit Temple Boss": "can_use(Hookshot) and can_live_dmg(0.5) and Mirror_Shield and has_explosives",
"Early Adult Spirit Temple": "can_jumpslash or can_hover or can_use(Hookshot)"
}
},
{
"region_name": "Mirror Shield Hand",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Mirror Shield Chest": "True"
},
"exits": {
"Desert Colossus": "True",
"Silver Gauntlets Hand": "
can_hover or can_use(Hookshot) or (can_use(Hover_Boots) and can_mega)",
"Spirit Temple Beyond Central Locked Door": "True"
}
},
{
"region_name": "Silver Gauntlets Hand",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Silver Gauntlets Chest": "True"
},
"exits": {
"Desert Colossus": "True",
"Mirror Shield Hand": "can_hover or (can_use(Hover_Boots) and can_mega)",
"Spirit Temple Central Chamber": "(Small_Key_Spirit_Temple, 2)"
}
},
{
"region_name": "Spirit Temple Outdoor Hands",
"dungeon": "Spirit Temple",
"exits": {
"Silver Gauntlets Hand": "True",
"Mirror Shield Hand": "True"
}
},
{
"region_name": "Spirit Temple Beyond Central Locked Door",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Near Four Armos Chest": "can_use(Mirror_Shield)",
"Spirit Temple Hallway Right Invisible Chest": "True",
"Spirit Temple Hallway Left Invisible Chest": "True"
},
"exits": {
"Spirit Temple Beyond Final Locked Door": "(Small_Key_Spirit_Temple,5) and
(can_use(Hookshot) or has_explosives)",
"Mirror Shield Hand": "True",
"Spirit Temple Central Chamber": "has_explosives"
}
},
{
"region_name": "Spirit Temple Beyond Final Locked Door",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Boss Key Chest": "
can_play(Zeldas_Lullaby) and (can_live_dmg(1.0) or (Bow and
Progressive_Hookshot))",
"Spirit Temple Topmost Chest": "can_use(Mirror_Shield)"
},
"exits": {
"Spirit Temple Boss": "can_use(Mirror_Shield)",
"Spirit Temple Central Chamber": "can_use(Mirror_Shield) or can_use(Hookshot)"
}
},
{
"region_name": "Spirit Temple Boss",
"dungeon": "Spirit Temple",
"locations": {
"Spirit Temple Twinrova Heart": "True",
"Twinrova": "True"
}
}
]

View File

@ -0,0 +1,58 @@
[
{
"region_name": "Water Temple Lobby",
"dungeon": "Water Temple",
"events": {
"Water Temple Clear": "Boss_Key_Water_Temple and can_use(Longshot)"
},
"locations": {
"Water Temple MQ Map Chest": "has_fire_source and Iron_Boots",
"Water Temple MQ Central Pillar Chest": "
can_use(Zora_Tunic) and Iron_Boots and
(can_use(Fire_Arrows) or (can_use(Dins_Fire) and can_play(Song_of_Time)))",
# If the player leaves the dungeon without collecting the item at Morpha Heart,
# they won't be able to come back without Iron Boots.
# If it is the Iron Boots or an item required to get them, it means a soft-lock.
"Water Temple Morpha Heart": "
Boss_Key_Water_Temple and can_use(Longshot) and Iron_Boots",
"Morpha": "Boss_Key_Water_Temple and can_use(Longshot)"
},
"exits": {
"Lake Hylia": "True",
"Water Temple Lowered Water Levels": "can_play(Zeldas_Lullaby) and Iron_Boots",
"Water Temple Dark Link Region": "
Small_Key_Water_Temple and can_use(Longshot) and Iron_Boots"
}
},
{
"region_name": "Water Temple Lowered Water Levels",
"dungeon": "Water Temple",
"locations": {
"Water Temple MQ Compass Chest": "Bow or can_use(Dins_Fire)",
"Water Temple MQ Longshot Chest": "True",
"Water Temple MQ GS Lizalfos Hallway": "can_use(Dins_Fire)",
"Water Temple MQ GS Before Upper Water Switch": "can_use(Longshot)"
}
},
{
"region_name": "Water Temple Dark Link Region",
"dungeon": "Water Temple",
"locations": {
"Water Temple MQ Boss Key Chest": "can_use(Dins_Fire)",
"Water Temple MQ GS River": "True"
},
"exits": {
"Water Temple Basement Gated Areas": "
can_use(Dins_Fire) and (Hover_Boots or Ocarina)"
}
},
{
"region_name": "Water Temple Basement Gated Areas",
"dungeon": "Water Temple",
"locations": {
"Water Temple MQ Freestanding Key": "True",
"Water Temple MQ GS Triple Wall Torch": "can_use(Fire_Arrows)",
"Water Temple MQ GS Freestanding Key Area": "(Small_Key_Water_Temple, 2)"
}
}
]

View File

@ -0,0 +1,174 @@
[
{
"region_name": "Water Temple Lobby",
"dungeon": "Water Temple",
"locations": {},
"exits": {
"High Alcove": "is_adult or can_hover",
"Boss Area": "can_use(Longshot) or can_hover or (can_use(Hover_Boots) and (can_mega or Megaton_Hammer))",
"Dark Link Area": "(at('High Alcove', can_play(Zeldas_Lullaby)) or
(can_use(Hover_Boots) and (can_mega or Megaton_Hammer)))
and (Small_Key_Water_Temple, 4)",
"Under Entrance Block": "can_use(Hookshot) and Iron_Boots",
"Central Pillar from Lobby": "can_use(Hookshot) and Iron_Boots and
(Small_Key_Water_Temple, 4)",
"Compass Room": "can_use(Iron_Boots) and can_use(Hookshot)",
"Ruto Column": "can_use(Iron_Boots) or can_use(Longshot) or can_jumpslash"
}
},
{
"region_name": "High Alcove",
"dungeon": "Water Temple",
"locations": {},
"exits": {
"Compass Room": "is_adult",
"Ruto Column": "is_adult",
"Under Entrance Block": "can_use(Iron_Boots) and can_use(Hookshot)",
"Caged Skulltula": "is_adult",
"Dragon Head Area": "is_adult",
"Boss Key Area": "is_adult and
(Small_Key_Water_Temple, 4)
and (can_use(Longshot) or can_hover or Hover_Boots)",
"Boss Area": "can_play(Zeldas_Lullaby) and can_use(Longshot)",
"Water Temple Lobby": "can_play(Zeldas_Lullaby)"
}
},
{
"region_name": "Caged Skulltula",
"dungeon": "Water Temple",
"locations": {
"Water Temple GS Behind Gate": "(can_use(Hover_Boots) or can_hover or can_use(Hookshot)) and
(can_jumpslash or has_explosives)"
}
},
{
"region_name": "Compass Room",
"dungeon": "Water Temple",
"locations": {
"Water Temple Compass Chest": "True"
}
},
{
"region_name": "Under Entrance Block",
"dungeon": "Water Temple",
"locations": {
"Water Temple Central Bow Target Chest": "True"
}
},
{
"region_name": "Ruto Column",
"dungeon": "Water Temple",
"locations": {
"Water Temple Map Chest": "is_adult or can_child_damage",
"Water Temple Cracked Wall Chest": "(can_use(Hookshot) and Iron_Boots) or
(can_play(Zeldas_Lullaby) and (can_use(Hookshot) or has_explosives)) ",
"Water Temple Torches Chest": "(here(is_child and can_use(Sticks)) or has_fire_source or can_use(Bow))
and can_play(Zeldas_Lullaby)"
},
"exits": {
"Central Pillar": "can_play(Zeldas_Lullaby) and
((Small_Key_Water_Temple, 5)
or here(is_child and can_use(Sticks)) or has_fire_source or can_use(Bow))",
"Boss Key Area": "(Small_Key_Water_Temple, 4) and
(can_use(Longshot) or can_hover or can_use(Hover_Boots)) and can_play(Zeldas_Lullaby)",
"Dragon Head Area": "Progressive_Strength_Upgrade and (is_adult or can_child_attack) and can_play(Zeldas_Lullaby)",
"Caged Skulltula": "has_explosives and can_play(Zeldas_Lullaby)",
"Compass Room": "can_play(Zeldas_Lullaby) and can_use(Hookshot)",
"Under Entrance Block": "can_play(Zeldas_Lullaby) and
( (can_use(Hookshot) and can_mega) or (can_use(Bow) and (Hover_Boots or can_use(Longshot))
and (Progressive_Strength_Upgrade or (can_use(Hookshot) and can_mega))))",
"High Alcove": "(can_use(Hover_Boots) or can_use(Hookshot) or (is_adult and can_mega) or can_hover)
and can_play(Zeldas_Lullaby)"
}
},
{
"region_name": "Central Pillar from Lobby",
"dungeon": "Water Temple",
"exits": {
"Central Pillar": "True"
}
},
{
"region_name": "Central Pillar",
"dungeon": "Water Temple",
"locations": {
"Water Temple Central Pillar Chest": "can_use(Iron_Boots) and can_use(Hookshot) and
(at('Central Pillar from Lobby', True) or can_play(Zeldas_Lullaby))",
"Water Temple GS Central Pillar": "at('Central Pillar from Lobby', True) or
can_use(Longshot) or
at('High Alcove',
can_use(Farores_Wind) and can_play(Zeldas_Lullaby)
and (can_use(Hookshot) or can_use(Boomerang)))"
},
"exits": {
"High Alcove": "(can_use(Hover_Boots) or can_use(Hookshot) or (is_adult and can_mega) or can_hover
or has_projectile(either))
and can_play(Zeldas_Lullaby)",
"Compass Room": "can_play(Zeldas_Lullaby) and can_use(Hookshot)"
}
},
{
"region_name": "Boss Key Area",
"dungeon": "Water Temple",
"locations": {
"Water Temple Boss Key Chest": "(Small_Key_Water_Temple, 5) and
(is_adult or can_hover)",
"Water Temple GS Near Boss Key Chest": "(is_adult or can_hover) and
(can_use(Hookshot) or can_use(Boomerang) or can_mega)"
}
},
{
"region_name": "Dark Link Area",
"dungeon": "Water Temple",
"locations": {
"Water Temple Longshot Chest": "can_use(Hookshot)",
"Water Temple GS Falling Platform Room": "can_use(Hookshot)"
},
"exits": {
"River": "can_play(Song_of_Time) or (can_use(Hookshot) and (Hover_Boots or (Bombs and can_live_dmg(0.5))))"
}
},
{
"region_name": "River",
"dungeon": "Water Temple",
"locations": {
"Water Temple GS River": "can_use(Longshot) or (Iron_Boots and can_use(Hookshot))"
},
"exits": {
"River Chest": "can_use(Bow) or (can_use(Longshot) and has_bottle and Iron_Boots)"
}
},
{
"region_name": "River Chest",
"dungeon": "Water Temple",
"locations": {
"Water Temple River Chest": "True"
},
"exits": {
"Dragon Head Area": "True",
"River": "(can_use(Longshot) or can_use(Bow)) and can_mega"
}
},
{
"region_name": "Dragon Head Area",
"dungeon": "Water Temple",
"locations": {
"Water Temple Dragon Chest": "at('River Chest', is_adult) or has_bombchus or (Iron_Boots and can_use(Hookshot))"
},
"exits": {
"River Chest": "can_hover"
}
},
{
"region_name": "Boss Area",
"dungeon": "Water Temple",
"events": {
"Water Temple Clear": "can_jumpslash and (can_hover or Boss_Key_Water_Temple)"
},
"locations": {
"Morpha": "can_jumpslash and (can_hover or Boss_Key_Water_Temple)",
"Water Temple Morpha Heart": "can_jumpslash and (can_hover or Boss_Key_Water_Temple)"
}
}
]

View File

@ -0,0 +1,28 @@
{
"name": "balanced",
"gui_name": "Balanced",
"description": "Recommended hint spread.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 1},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 1},
"woth": {"order": 3, "weight": 3.5, "fixed": 0, "copies": 1},
"barren": {"order": 4, "weight": 2.0, "fixed": 0, "copies": 1},
"entrance": {"order": 5, "weight": 3.0, "fixed": 0, "copies": 1},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 0, "copies": 1},
"random": {"order": 7, "weight": 6.0, "fixed": 0, "copies": 1},
"item": {"order": 8, "weight": 5.0, "fixed": 0, "copies": 1},
"song": {"order": 9, "weight": 1.0, "fixed": 0, "copies": 1},
"overworld": {"order": 10, "weight": 2.0, "fixed": 0, "copies": 1},
"dungeon": {"order": 11, "weight": 1.5, "fixed": 0, "copies": 1},
"junk": {"order": 12, "weight": 3.0, "fixed": 0, "copies": 1},
"named-item": {"order": 13, "weight": 0.0, "fixed": 0, "copies": 1}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "bingo",
"gui_name": "Bingo",
"description": "Hints locations for items required for most bingo goals. Can be made board-specific by entering a Bingosync URL.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 0},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 0.0, "fixed": 0, "copies": 0},
"barren": {"order": 4, "weight": 0.0, "fixed": 0, "copies": 0},
"entrance": {"order": 5, "weight": 0.0, "fixed": 0, "copies": 2},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 0, "copies": 1},
"random": {"order": 7, "weight": 0.0, "fixed": 0, "copies": 2},
"item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 2},
"song": {"order": 9, "weight": 0.0, "fixed": 0, "copies": 2},
"overworld": {"order": 10, "weight": 0.0, "fixed": 0, "copies": 2},
"dungeon": {"order": 11, "weight": 0.0, "fixed": 0, "copies": 2},
"junk": {"order": 12, "weight": 1.0, "fixed": 0, "copies": 1},
"named-item": {"order": 13, "weight": 0.0, "fixed": 0, "copies": 2}
}
}

View File

@ -0,0 +1,51 @@
{
"name": "ddr",
"gui_name": "DDR",
"description": "DDR weekly race hints. Duplicates of each hint, 2 WotH, 3 Barren, 3 Named-Item, remainder of hints are Sometimes. Prevents some items from being hinted in WotH or Sometimes.",
"add_locations": [
{ "location": "Deku Theater Skull Mask", "types": ["always"]}
],
"remove_locations": [],
"add_items": [
{"item":"Hover Boots", "types":["named-item"]},
{"item":"Progressive Hookshot", "types":["named-item"]},
{"item":"Dins Fire", "types":["named-item"]},
{"item":"Bomb Bag", "types":["named-item"]},
{"item":"Boomerang", "types":["named-item"]},
{"item":"Bow", "types":["named-item"]},
{"item":"Megaton Hammer", "types":["named-item"]},
{"item":"Iron Boots", "types":["named-item"]},
{"item":"Magic Meter", "types":["named-item"]},
{"item":"Mirror Shield", "types":["named-item"]},
{"item":"Fire Arrows", "types":["named-item"]},
{"item":"Progressive Strength Upgrade", "types":["named-item"]}
],
"remove_items": [
{ "item": "Zeldas Lullaby", "types": ["woth", "sometimes"] },
{ "item": "Rutos Letter", "types": ["woth", "sometimes"] },
{ "item": "Goron Tunic", "types": ["woth", "sometimes"] },
{ "item": "Zora Tunic", "types": ["woth", "sometimes"] },
{ "item": "Bow", "types": ["barren"]},
{ "item": "Bomb Bag", "types": ["barren"]},
{ "item": "Magic Meter", "types": ["barren"]}
],
"dungeons_woth_limit": 1,
"dungeons_barren_limit": 1,
"named_items_required": false,
"vague_named_items": true,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 1},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 0.0, "fixed": 2, "copies": 2},
"barren": {"order": 4, "weight": 0.0, "fixed": 3, "copies": 2},
"entrance": {"order": 5, "weight": 0.0, "fixed": 0, "copies": 2},
"sometimes": {"order": 13,"weight": 0.0, "fixed": 99, "copies": 2},
"random": {"order": 7, "weight": 0.0, "fixed": 0, "copies": 2},
"item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 2},
"song": {"order": 9, "weight": 0.0, "fixed": 0, "copies": 2},
"overworld": {"order": 10, "weight": 0.0, "fixed": 0, "copies": 2},
"dungeon": {"order": 11, "weight": 0.0, "fixed": 0, "copies": 2},
"junk": {"order": 12, "weight": 0.0, "fixed": 0, "copies": 1},
"named-item": {"order": 6, "weight": 0.0, "fixed": 3, "copies": 2}
}
}

View File

@ -0,0 +1,31 @@
{
"name": "league",
"gui_name": "League",
"description": "Hint Distro for the OoTR League",
"add_locations": [
{ "location": "Deku Theater Skull Mask", "types": ["always"] }
],
"remove_locations": [],
"add_items": [],
"remove_items": [{"types": ["woth"], "item": "Zeldas Lullaby"}],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 2},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 0.0, "fixed": 5, "copies": 2},
"barren": {"order": 4, "weight": 0.0, "fixed": 3, "copies": 2},
"entrance": {"order": 5, "weight": 0.0, "fixed": 4, "copies": 2},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 100, "copies": 2},
"random": {"order": 7, "weight": 9.0, "fixed": 0, "copies": 2},
"item": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"song": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"overworld": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"dungeon": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"junk": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"named-item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 2}
},
"groups": [],
"disabled": []
}

View File

@ -0,0 +1,143 @@
{
"name": "mw2",
"gui_name": "MW Season 2",
"description": "Hints used for the multi-world tournament season 2.",
"add_locations": [
{
"location": "Song from Ocarina of Time",
"types": [
"always"
]
},
{
"location": "Deku Theater Skull Mask",
"types": [
"always"
]
},
{
"location": "DMC Deku Scrub",
"types": [
"always"
]
}
],
"remove_locations": [
{
"location": "Haunted Wasteland",
"types": [
"barren"
]
},
{
"location": "Temple of Time",
"types": [
"barren"
]
},
{
"location": "Hyrule Castle",
"types": [
"barren"
]
},
{
"location": "outside Ganon's Castle",
"types": [
"barren"
]
}
],
"add_items": [],
"remove_items": [
{
"item": "Zeldas Lullaby",
"types": [
"woth"
]
}
],
"dungeons_woth_limit": 3,
"dungeons_barren_limit": 1,
"named_items_required": true,
"distribution": {
"trial": {
"order": 1,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"always": {
"order": 2,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"woth": {
"order": 3,
"weight": 0.0,
"fixed": 7,
"copies": 2
},
"barren": {
"order": 4,
"weight": 0.0,
"fixed": 3,
"copies": 2
},
"entrance": {
"order": 5,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"sometimes": {
"order": 6,
"weight": 0.0,
"fixed": 100,
"copies": 2
},
"random": {
"order": 7,
"weight": 9.0,
"fixed": 0,
"copies": 2
},
"item": {
"order": 0,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"song": {
"order": 0,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"overworld": {
"order": 0,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"dungeon": {
"order": 0,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"junk": {
"order": 0,
"weight": 0.0,
"fixed": 0,
"copies": 2
},
"named-item": {
"order": 8,
"weight": 0.0,
"fixed": 0,
"copies": 2
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "scrubs",
"gui_name": "Scrubs",
"description": "Tournament hints used for Scrubs Races. Duplicates of each hint, Skull Mask is an always hint, 5 WotH, 3 Foolish, 8 sometimes. Can also be used to simulate S3 Tournament hints.",
"add_locations": [
{ "location": "Deku Theater Skull Mask", "types": ["always"] },
{ "location": "Deku Theater Mask of Truth", "types": ["always"] }
],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 2},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 0.0, "fixed": 5, "copies": 2},
"barren": {"order": 4, "weight": 0.0, "fixed": 3, "copies": 2},
"entrance": {"order": 5, "weight": 0.0, "fixed": 4, "copies": 2},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 100, "copies": 2},
"random": {"order": 7, "weight": 9.0, "fixed": 0, "copies": 2},
"item": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"song": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"overworld": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"dungeon": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"junk": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 2},
"named-item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 2}
},
"groups": [],
"disabled": []
}

View File

@ -0,0 +1,28 @@
{
"name": "strong",
"gui_name": "Strong",
"description": "More useful hints.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.00, "fixed": 0, "copies": 1},
"always": {"order": 2, "weight": 0.00, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 3.00, "fixed": 0, "copies": 2},
"barren": {"order": 4, "weight": 3.00, "fixed": 0, "copies": 1},
"entrance": {"order": 5, "weight": 1.00, "fixed": 0, "copies": 1},
"sometimes": {"order": 6, "weight": 0.00, "fixed": 0, "copies": 1},
"random": {"order": 7, "weight": 2.00, "fixed": 0, "copies": 1},
"item": {"order": 8, "weight": 1.00, "fixed": 0, "copies": 1},
"song": {"order": 9, "weight": 0.33, "fixed": 0, "copies": 1},
"overworld": {"order": 10, "weight": 0.66, "fixed": 0, "copies": 1},
"dungeon": {"order": 11, "weight": 0.66, "fixed": 0, "copies": 1},
"junk": {"order": 12, "weight": 0.00, "fixed": 0, "copies": 1},
"named-item": {"order": 13, "weight": 0.00, "fixed": 0, "copies": 1}
}
}

View File

@ -0,0 +1,32 @@
{
"name": "tournament",
"gui_name": "Tournament",
"description": "Tournament hints used for OoTR Season 4. Gossip stones in grottos are disabled. 4 WotH, 2 Barren, remainder filled with Sometimes. Always, WotH, and Barren hints duplicated.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [{"types": ["woth"], "item": "Zeldas Lullaby"}],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 1},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 0.0, "fixed": 4, "copies": 2},
"barren": {"order": 4, "weight": 0.0, "fixed": 2, "copies": 2},
"entrance": {"order": 5, "weight": 0.0, "fixed": 4, "copies": 1},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 99, "copies": 1},
"random": {"order": 7, "weight": 9.0, "fixed": 0, "copies": 1},
"item": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
"song": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
"overworld": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
"dungeon": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
"junk": {"order": 0, "weight": 0.0, "fixed": 0, "copies": 1},
"named-item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 1}
},
"groups": [],
"disabled": [
"HC (Storms Grotto)", "HF (Cow Grotto)", "HF (Near Market Grotto)", "HF (Southeast Grotto)", "HF (Open Grotto)", "Kak (Open Grotto)", "ZR (Open Grotto)", "KF (Storms Grotto)", "LW (Near Shortcuts Grotto)", "DMT (Storms Grotto)", "DMC (Upper Grotto)"
]
}

View File

@ -0,0 +1,28 @@
{
"name": "useless",
"gui_name": "Useless",
"description": "Only junk hints.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 2,
"dungeons_barren_limit": 1,
"named_items_required": false,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 0},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 0},
"woth": {"order": 3, "weight": 0.0, "fixed": 0, "copies": 0},
"barren": {"order": 4, "weight": 0.0, "fixed": 0, "copies": 0},
"entrance": {"order": 5, "weight": 0.0, "fixed": 0, "copies": 0},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 0, "copies": 0},
"random": {"order": 7, "weight": 0.0, "fixed": 0, "copies": 0},
"item": {"order": 8, "weight": 0.0, "fixed": 0, "copies": 0},
"song": {"order": 9, "weight": 0.0, "fixed": 0, "copies": 0},
"overworld": {"order": 10, "weight": 0.0, "fixed": 0, "copies": 0},
"dungeon": {"order": 11, "weight": 0.0, "fixed": 0, "copies": 0},
"junk": {"order": 12, "weight": 9.0, "fixed": 0, "copies": 1},
"named-item": {"order": 13, "weight": 0.0, "fixed": 0, "copies": 0}
}
}

View File

@ -0,0 +1,28 @@
{
"name": "very_strong",
"gui_name": "Very Strong",
"description": "Many powerful hints.",
"add_locations": [],
"remove_locations": [],
"add_items": [],
"remove_items": [],
"dungeons_woth_limit": 40,
"dungeons_barren_limit": 40,
"named_items_required": true,
"vague_named_items": false,
"distribution": {
"trial": {"order": 1, "weight": 0.0, "fixed": 0, "copies": 1},
"always": {"order": 2, "weight": 0.0, "fixed": 0, "copies": 2},
"woth": {"order": 3, "weight": 3.0, "fixed": 0, "copies": 2},
"barren": {"order": 4, "weight": 3.0, "fixed": 0, "copies": 1},
"entrance": {"order": 5, "weight": 2.0, "fixed": 0, "copies": 1},
"sometimes": {"order": 6, "weight": 0.0, "fixed": 0, "copies": 1},
"random": {"order": 7, "weight": 0.0, "fixed": 0, "copies": 1},
"item": {"order": 8, "weight": 1.0, "fixed": 0, "copies": 1},
"song": {"order": 9, "weight": 0.5, "fixed": 0, "copies": 1},
"overworld": {"order": 10, "weight": 1.5, "fixed": 0, "copies": 1},
"dungeon": {"order": 11, "weight": 1.5, "fixed": 0, "copies": 1},
"junk": {"order": 12, "weight": 0.0, "fixed": 0, "copies": 1},
"named-item": {"order": 13, "weight": 0.0, "fixed": 0, "copies": 1}
}
}

View File

@ -0,0 +1,114 @@
{
# Rules here define replacement "functions" or item aliases.
# The alias itself must always be alphanumeric only--string items bypass the aliasing.
# Rules should be simple python and can invoke other aliases or even the
# at/here metarules.
"Hookshot": "Progressive_Hookshot",
"Longshot": "(Progressive_Hookshot, 2)",
"Silver_Gauntlets": "(Progressive_Strength_Upgrade, 2)",
"Golden_Gauntlets": "(Progressive_Strength_Upgrade, 3)",
"Scarecrow": "Progressive_Hookshot and can_play(Scarecrow_Song)",
"Distant_Scarecrow": "(Progressive_Hookshot, 2) and can_play(Scarecrow_Song)",
# Items can be directly aliased, so all occurrences of that item name get replaced,
# but make sure that inside the rule you use the string 'item' form
# to avoid infinite recursion.
"Goron_Tunic": "'Goron Tunic' or Buy_Goron_Tunic",
"Zora_Tunic": "'Zora Tunic' or Buy_Zora_Tunic",
# Refill aliases. If we start considering Bow etc refills we can enable these aliases
# without editing all logic files.
# "Bow": "'Bow'",
# "Slingshot": "'Slingshot'",
"Bombs": "Bomb_Bag",
"Deku_Shield": "Buy_Deku_Shield",
"Hylian_Shield": "Buy_Hylian_Shield",
"Nuts": "Buy_Deku_Nut_5 or Buy_Deku_Nut_10 or Deku_Nut_Drop",
"Sticks": "Buy_Deku_Stick_1 or Deku_Stick_Drop",
"Bugs": "'Bugs' or Buy_Bottle_Bug",
"Blue_Fire": "'Blue Fire' or Buy_Blue_Fire",
"Fish": "'Fish' or Buy_Fish",
"Fairy": "'Fairy' or Buy_Fairys_Spirit",
"Big_Poe": "'Big Poe'",
"has_bombchus": "(Buy_Bombchu_5 or Buy_Bombchu_10 or Buy_Bombchu_20 or Bombchu_Drop) and (bombchus_in_logic or Bomb_Bag)",
"found_bombchus": "(bombchus_in_logic and (Bombchus or Bombchus_5 or Bombchus_10 or Bombchus_20)) or (not bombchus_in_logic and Bomb_Bag)",
"has_explosives": "Bombs or (bombchus_in_logic and has_bombchus)",
"is_child": "current_spot_child_access",
"is_adult": "current_spot_adult_access",
"is_starting_age": "current_spot_starting_age_access",
"is_glitched": "logic_rules != 'glitchless'",
"can_blast_or_smash": "has_explosives or can_use(Megaton_Hammer)",
"can_child_attack": "is_child and (Slingshot or Boomerang or Sticks or Kokiri_Sword or has_explosives or can_use(Dins_Fire))",
"can_child_damage": "is_child and (Slingshot or Sticks or Kokiri_Sword or has_explosives or can_use(Dins_Fire))",
"can_cut_shrubs": "is_adult or Sticks or Kokiri_Sword or Boomerang or has_explosives",
"can_dive": "Progressive_Scale",
"can_leave_forest": "open_forest != 'closed' or is_adult or is_glitched or Deku_Tree_Clear",
"can_plant_bugs": "is_child and Bugs",
"can_ride_epona": "is_adult and Epona and (can_play(Eponas_Song) or (is_glitched and can_hover))",
"can_stun_deku": "is_adult or (Slingshot or Boomerang or Sticks or Kokiri_Sword or has_explosives or can_use(Dins_Fire) or Nuts or Deku_Shield)",
"can_summon_gossip_fairy": "Ocarina and (Zeldas_Lullaby or Eponas_Song or Song_of_Time or Suns_Song)",
"can_summon_gossip_fairy_without_suns": "Ocarina and (Zeldas_Lullaby or Eponas_Song or Song_of_Time)",
"can_take_damage": "damage_multiplier != 'ohko' or Fairy or can_use(Nayrus_Love)",
"can_plant_bean": "is_child and (Magic_Bean or Magic_Bean_Pack)",
"can_play(song)": "Ocarina and song",
"can_open_bomb_grotto": "can_blast_or_smash and (Stone_of_Agony or logic_grottos_without_agony)",
"can_open_storm_grotto": "can_play(Song_of_Storms) and (Stone_of_Agony or logic_grottos_without_agony)",
# The last case in a conditional still needs a check, to prevent failure from falling through
# into what should be the 'else' case.
"can_use_projectile": "has_explosives or (is_adult and (Bow or Hookshot)) or (is_child and (Slingshot or Boomerang))",
"has_projectile(for_age)": "has_explosives
or (for_age == child and (Slingshot or Boomerang))
or (for_age == adult and (Bow or Hookshot))
or (for_age == both and (Slingshot or Boomerang) and (Bow or Hookshot))
or (for_age == either and (Slingshot or Boomerang or Bow or Hookshot))",
# can_use and helpers
# The parser reduces this to smallest form based on item category.
# Note that can_use(item) is False for any item not covered here.
"can_use(item)": "(_is_magic_item(item) and item and Magic_Meter)
or (_is_adult_item(item) and is_adult and item)
or (_is_magic_arrow(item) and is_adult and item and Bow and Magic_Meter)
or (_is_child_item(item) and is_child and item)",
"_is_magic_item(item)": "item == Dins_Fire or item == Farores_Wind or item == Nayrus_Love or item == Lens_of_Truth",
"_is_adult_item(item)": "item == Bow or item == Megaton_Hammer or item == Iron_Boots or item == Hover_Boots or item == Hookshot or item == Longshot or item == Silver_Gauntlets or item == Golden_Gauntlets or item == Goron_Tunic or item == Zora_Tunic or item == Scarecrow or item == Distant_Scarecrow or item == Mirror_Shield",
"_is_child_item(item)": "item == Slingshot or item == Boomerang or item == Kokiri_Sword or item == Sticks or item == Deku_Shield",
"_is_magic_arrow(item)": "item == Fire_Arrows or item == Light_Arrows",
# Biggoron's trade path
# ER with certain settings disables timers and prevents items from reverting on save warp.
# Otherwise, to get to Biggoron requires: a trick, clearing boulders on DMT, or Darunia's Chamber
"guarantee_trade_path": "disable_trade_revert or can_blast_or_smash or 'Stop GC Rolling Goron as Adult' or (logic_dmt_climb_hovers and can_use(Hover_Boots)) or (logic_biggoron_bolero and not warp_songs and can_play(Bolero_of_Fire) and at('DMC Central Local', can_use(Hookshot) or can_use(Hover_Boots) or can_plant_bean))",
"guarantee_hint": "(hints == 'mask' and Mask_of_Truth) or (hints == 'agony' and Stone_of_Agony) or (hints != 'mask' and hints != 'agony')",
"has_fire_source": "can_use(Dins_Fire) or can_use(Fire_Arrows)",
"has_fire_source_with_torch": "has_fire_source or (is_child and Sticks)",
# Gerudo Fortress
"can_finish_GerudoFortress": "(gerudo_fortress == 'normal' and (Small_Key_Gerudo_Fortress, 4) and (is_adult or Kokiri_Sword or is_glitched) and (is_adult and (Bow or Hookshot or Hover_Boots) or Gerudo_Membership_Card or logic_gerudo_kitchen or is_glitched))
or (gerudo_fortress == 'fast' and Small_Key_Gerudo_Fortress and (is_adult or Kokiri_Sword or is_glitched))
or (gerudo_fortress != 'normal' and gerudo_fortress != 'fast')",
# Mirror shield does not count because it cannot reflect scrub attack.
"has_shield": "(is_adult and Hylian_Shield) or (is_child and Deku_Shield)",
"can_shield": "(is_adult and (Hylian_Shield or Mirror_Shield)) or (is_child and Deku_Shield)",
"can_mega": "has_explosives and can_shield",
"can_isg": "can_shield and (is_adult or Sticks or Kokiri_Sword)",
"can_hover": "can_mega and can_isg",
"can_weirdshot": "can_mega and can_use(Hookshot)",
"can_jumpslash": "is_adult or Sticks or Kokiri_Sword",
# Bridge Requirements
"has_all_stones": "Kokiri_Emerald and Goron_Ruby and Zora_Sapphire",
"has_all_medallions": "Forest_Medallion and Fire_Medallion and Water_Medallion and Shadow_Medallion and Spirit_Medallion and Light_Medallion",
"can_build_rainbow_bridge": "(
(bridge == 'open') or
(bridge == 'vanilla' and Shadow_Medallion and Spirit_Medallion and Light_Arrows) or
(bridge == 'stones' and _oot_has_stones(bridge_stones)) or
(bridge == 'medallions' and _oot_has_medallions(bridge_medallions)) or
(bridge == 'dungeons' and _oot_has_dungeon_rewards(bridge_rewards)) or
(bridge == 'tokens' and (Gold_Skulltula_Token, bridge_tokens)))",
"can_trigger_lacs": "(
(lacs_condition == 'vanilla' and Shadow_Medallion and Spirit_Medallion) or
(lacs_condition == 'stones' and _oot_has_stones(lacs_stones)) or
(lacs_condition == 'medallions' and _oot_has_medallions(lacs_medallions)) or
(lacs_condition == 'dungeons' and _oot_has_dungeon_rewards(lacs_rewards)) or
(lacs_condition == 'tokens' and (Gold_Skulltula_Token, lacs_tokens)))"
}

View File

@ -0,0 +1,44 @@
[
{
"region_name": "Bottom of the Well",
"dungeon": "Bottom of the Well",
"exits": {
"Kakariko Village": "True",
"Bottom of the Well Perimeter": "is_child"
}
},
{
"region_name": "Bottom of the Well Perimeter",
"dungeon": "Bottom of the Well",
"locations": {
"Bottom of the Well MQ Compass Chest": "
Kokiri_Sword or (Sticks and logic_child_deadhand)",
"Bottom of the Well MQ Dead Hand Freestanding Key": "
has_explosives or (logic_botw_mq_dead_hand_key and Boomerang)",
"Bottom of the Well MQ GS Basement": "can_child_attack",
"Bottom of the Well MQ GS Coffin Room": "
can_child_attack and (Small_Key_Bottom_of_the_Well, 2)",
"Wall Fairy": "has_bottle and Slingshot" # The fairy pot is obsolete
},
"exits": {
"Bottom of the Well": "True",
"Bottom of the Well Middle": "
can_play(Zeldas_Lullaby) or (logic_botw_mq_pits and has_explosives)"
}
},
{
"region_name": "Bottom of the Well Middle",
"dungeon": "Bottom of the Well",
"locations": {
"Bottom of the Well MQ Map Chest": "True",
"Bottom of the Well MQ Lens of Truth Chest": "
has_explosives and (Small_Key_Bottom_of_the_Well, 2)",
"Bottom of the Well MQ East Inner Room Freestanding Key": "True",
"Bottom of the Well MQ GS West Inner Room": "
can_child_attack and (logic_botw_mq_pits or has_explosives)"
},
"exits": {
"Bottom of the Well Perimeter": "True"
}
}
]

View File

@ -0,0 +1,53 @@
[
{
"region_name": "Bottom of the Well",
"dungeon": "Bottom of the Well",
"exits": {
"Kakariko Village": "True",
"Bottom of the Well Main Area" : "is_child and (can_child_attack or Nuts)"
}
},
{
"region_name": "Bottom of the Well Main Area",
"dungeon": "Bottom of the Well",
"locations": {
"Bottom of the Well Front Left Fake Wall Chest": "logic_lens_botw or can_use(Lens_of_Truth)",
"Bottom of the Well Front Center Bombable Chest": "has_explosives",
"Bottom of the Well Right Bottom Fake Wall Chest": "logic_lens_botw or can_use(Lens_of_Truth)",
"Bottom of the Well Compass Chest": "logic_lens_botw or can_use(Lens_of_Truth)",
"Bottom of the Well Center Skulltula Chest": "logic_lens_botw or can_use(Lens_of_Truth)",
"Bottom of the Well Back Left Bombable Chest": "has_explosives and (logic_lens_botw or can_use(Lens_of_Truth))",
"Bottom of the Well Freestanding Key": "Sticks or can_use(Dins_Fire)",
"Bottom of the Well Lens of Truth Chest": "
can_play(Zeldas_Lullaby) and
(Kokiri_Sword or (Sticks and logic_child_deadhand))",
#Sword not strictly necessary but frankly being forced to do this with sticks isn't fair
"Bottom of the Well Invisible Chest": "can_play(Zeldas_Lullaby) and (logic_lens_botw or can_use(Lens_of_Truth))",
"Bottom of the Well Underwater Front Chest": "can_play(Zeldas_Lullaby)",
"Bottom of the Well Underwater Left Chest": "can_play(Zeldas_Lullaby)",
"Bottom of the Well Map Chest": "
has_explosives or
((((Small_Key_Bottom_of_the_Well, 3) and (logic_lens_botw or can_use(Lens_of_Truth))) or
can_use(Dins_Fire) or (logic_botw_basement and Sticks)) and
Progressive_Strength_Upgrade)",
"Bottom of the Well Fire Keese Chest": "
(Small_Key_Bottom_of_the_Well, 3) and (logic_lens_botw or can_use(Lens_of_Truth))", #These pits are really unfair.
"Bottom of the Well Like Like Chest": "
(Small_Key_Bottom_of_the_Well, 3) and (logic_lens_botw or can_use(Lens_of_Truth))",
"Bottom of the Well GS West Inner Room": "
Boomerang and (logic_lens_botw or can_use(Lens_of_Truth)) and
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well GS East Inner Room": "
Boomerang and (logic_lens_botw or can_use(Lens_of_Truth)) and
(Small_Key_Bottom_of_the_Well, 3)",
"Bottom of the Well GS Like Like Cage": "
Boomerang and (logic_lens_botw or can_use(Lens_of_Truth)) and
(Small_Key_Bottom_of_the_Well, 3)",
"Stick Pot": "True",
"Nut Pot": "True"
},
"exits": {
"Bottom of the Well" : "True"
}
}
]

View File

@ -0,0 +1,107 @@
[
{
"region_name": "Deku Tree Lobby",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Map Chest": "True",
"Deku Tree MQ Slingshot Chest": "is_adult or can_child_attack",
"Deku Tree MQ Slingshot Room Back Chest": "has_fire_source_with_torch or can_use(Bow)",
"Deku Tree MQ Basement Chest": "has_fire_source_with_torch or can_use(Bow)",
"Deku Tree MQ GS Lobby": "is_adult or can_child_attack",
"Deku Baba Sticks": "is_adult or Kokiri_Sword or Boomerang",
"Deku Baba Nuts": "
is_adult or Slingshot or Sticks or
has_explosives or Kokiri_Sword or can_use(Dins_Fire)"
},
"exits": {
"KF Outside Deku Tree": "True",
"Deku Tree Compass Room": "
here(can_use(Slingshot) or can_use(Bow)) and
here(has_fire_source_with_torch or can_use(Bow))",
"Deku Tree Basement Water Room Front": "
here(can_use(Slingshot) or can_use(Bow)) and here(has_fire_source_with_torch)",
"Deku Tree Basement Ledge": "logic_deku_b1_skip or here(is_adult)"
}
},
{
"region_name": "Deku Tree Compass Room",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Compass Chest": "True",
"Deku Tree MQ GS Compass Room": "
(can_use(Hookshot) or can_use(Boomerang)) and
here(has_bombchus or
(Bombs and (can_play(Song_of_Time) or is_adult)) or
(can_use(Hammer) and (can_play(Song_of_Time) or logic_deku_mq_compass_gs)))"
},
"exits": {
"Deku Tree Lobby": "True"
}
},
{
"region_name": "Deku Tree Basement Water Room Front",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Before Spinning Log Chest": "True"
},
"exits": {
"Deku Tree Basement Water Room Back": "
logic_deku_mq_log or (is_child and (Deku_Shield or Hylian_Shield)) or
can_use(Longshot) or (can_use(Hookshot) and can_use(Iron_Boots))",
"Deku Tree Lobby": "True"
}
},
{
"region_name": "Deku Tree Basement Water Room Back",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ After Spinning Log Chest": "can_play(Song_of_Time)"
},
"exits": {
"Deku Tree Basement Back Room": "
(here(can_use(Sticks) or can_use(Dins_Fire)) or
at('Deku Tree Basement Water Room Front', can_use(Fire_Arrows))) and
here(is_adult or Kokiri_Sword or can_use_projectile or (Nuts and Sticks))",
"Deku Tree Basement Water Room Front": "True"
}
},
{
"region_name": "Deku Tree Basement Back Room",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ GS Basement Graves Room": "
can_use(Longshot) or
(can_play(Song_of_Time) and (can_use(Boomerang) or can_use(Hookshot)))",
"Deku Tree MQ GS Basement Back Room": "
here(has_fire_source_with_torch) and
(can_use(Hookshot) or can_use(Boomerang))"
},
"exits": {
"Deku Tree Basement Ledge": "is_child",
"Deku Tree Basement Water Room Back": "
can_use(Kokiri_Sword) or can_use_projectile or (Nuts and can_use(Sticks))"
}
},
{
"region_name": "Deku Tree Basement Ledge",
"dungeon": "Deku Tree",
"locations": {
"Deku Tree MQ Deku Scrub": "can_stun_deku",
"Deku Tree Queen Gohma Heart": "
here(has_fire_source_with_torch) and here(has_shield) and
(is_adult or Kokiri_Sword or Sticks)",
"Queen Gohma": "
here(has_fire_source_with_torch) and here(has_shield) and
(is_adult or Kokiri_Sword or Sticks)"
},
"events": {
"Deku Tree Clear": "
here(has_fire_source_with_torch) and here(has_shield) and
(is_adult or Kokiri_Sword or Sticks)"
},
"exits" : {
"Deku Tree Basement Back Room": "is_child",
"Deku Tree Lobby": "True"
}
}
]

Some files were not shown because too many files have changed in this diff Show More