Merge pull request #141 from espeon65536/oot

Ocarina of Time updates
This commit is contained in:
Fabian Dill 2021-11-25 17:57:31 +00:00 committed by GitHub
commit 81397936ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 9647 additions and 9061 deletions

View File

@ -494,7 +494,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
handle_option(ret, game_weights, option_key, option) handle_option(ret, game_weights, option_key, option)
if "items" in plando_options: if "items" in plando_options:
ret.plando_items = roll_item_plando(world_type, game_weights) ret.plando_items = roll_item_plando(world_type, game_weights)
if ret.game == "Minecraft": if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now # bad hardcoded behavior to make this work for now
ret.plando_connections = [] ret.plando_connections = []
if "connections" in plando_options: if "connections" in plando_options:
@ -504,7 +504,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.plando_connections.append(PlandoConnection( ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement), get_choice("entrance", placement),
get_choice("exit", placement), get_choice("exit", placement),
get_choice("direction", placement, "both") get_choice("direction", placement)
)) ))
elif ret.game == "A Link to the Past": elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options) roll_alttp_settings(ret, game_weights, plando_options)

237
OoTAdjuster.py Normal file
View File

@ -0,0 +1,237 @@
import tkinter as tk
import argparse
import logging
import random
import os
from itertools import chain
from BaseClasses import MultiWorld
from Options import Choice, Range, Toggle
from worlds.oot import OOTWorld
from worlds.oot.Cosmetics import patch_cosmetics
from worlds.oot.Options import cosmetic_options, sfx_options
from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file
from Utils import local_path
logger = logging.getLogger('OoTAdjuster')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--rom', default='',
help='Path to an OoT randomized ROM to adjust.')
parser.add_argument('--vanilla_rom', default='',
help='Path to a vanilla OoT ROM for patching.')
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
parser.add_argument('--'+name, default=None,
help=option.__doc__)
parser.add_argument('--is_glitched', default=False, action='store_true',
help='Setting this to true will enable protection on kokiri tunic colors for weirdshot.')
parser.add_argument('--deathlink',
help='Enable DeathLink system', action='store_true')
args = parser.parse_args()
if not os.path.isfile(args.rom):
adjustGUI()
else:
adjust(args)
def adjustGUI():
from tkinter import Tk, LEFT, BOTTOM, TOP, E, W, \
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
OptionMenu, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
window = tk.Tk()
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
set_icon(window)
opts = Namespace()
# Select ROM
romDialogFrame = Frame(window)
romLabel = Label(romDialogFrame, text='Rom/patch to adjust')
vanillaLabel = Label(romDialogFrame, text='OoT Base Rom')
opts.rom = StringVar()
opts.vanilla_rom = StringVar(value="The Legend of Zelda - Ocarina of Time.z64")
romEntry = Entry(romDialogFrame, textvariable=opts.rom)
vanillaEntry = Entry(romDialogFrame, textvariable=opts.vanilla_rom)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".z64", ".n64", ".apz5")), ("All Files", "*")])
opts.rom.set(rom)
def VanillaSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".z64", ".n64")), ("All Files", "*")])
opts.vanilla_rom.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
vanillaSelectButton = Button(romDialogFrame, text='Select Rom', command=VanillaSelect)
romDialogFrame.pack(side=TOP, expand=True, fill=X)
romLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT)
vanillaLabel.pack(side=LEFT)
vanillaEntry.pack(side=LEFT, expand=True, fill=X)
vanillaSelectButton.pack(side=LEFT)
# Cosmetic options
romSettingsFrame = Frame(window)
def dropdown_option(type, option_name, row, column):
if type == 'cosmetic':
option = cosmetic_options[option_name]
elif type == 'sfx':
option = sfx_options[option_name]
optionFrame = Frame(romSettingsFrame)
optionFrame.grid(row=row, column=column, sticky=E)
optionLabel = Label(optionFrame, text=option.displayname)
optionLabel.pack(side=LEFT)
setattr(opts, option_name, StringVar())
getattr(opts, option_name).set(option.name_lookup[option.default])
optionMenu = OptionMenu(optionFrame, getattr(opts, option_name), *option.name_lookup.values())
optionMenu.pack(side=LEFT)
dropdown_option('cosmetic', 'default_targeting', 0, 0)
dropdown_option('cosmetic', 'display_dpad', 0, 1)
dropdown_option('cosmetic', 'correct_model_colors', 0, 2)
dropdown_option('cosmetic', 'background_music', 1, 0)
dropdown_option('cosmetic', 'fanfares', 1, 1)
dropdown_option('cosmetic', 'ocarina_fanfares', 1, 2)
dropdown_option('cosmetic', 'kokiri_color', 2, 0)
dropdown_option('cosmetic', 'goron_color', 2, 1)
dropdown_option('cosmetic', 'zora_color', 2, 2)
dropdown_option('cosmetic', 'silver_gauntlets_color', 3, 0)
dropdown_option('cosmetic', 'golden_gauntlets_color', 3, 1)
dropdown_option('cosmetic', 'mirror_shield_frame_color', 3, 2)
dropdown_option('cosmetic', 'navi_color_default_inner', 4, 0)
dropdown_option('cosmetic', 'navi_color_default_outer', 4, 1)
dropdown_option('cosmetic', 'navi_color_enemy_inner', 5, 0)
dropdown_option('cosmetic', 'navi_color_enemy_outer', 5, 1)
dropdown_option('cosmetic', 'navi_color_npc_inner', 6, 0)
dropdown_option('cosmetic', 'navi_color_npc_outer', 6, 1)
dropdown_option('cosmetic', 'navi_color_prop_inner', 7, 0)
dropdown_option('cosmetic', 'navi_color_prop_outer', 7, 1)
# sword_trail_duration, 8, 2
dropdown_option('cosmetic', 'sword_trail_color_inner', 8, 0)
dropdown_option('cosmetic', 'sword_trail_color_outer', 8, 1)
dropdown_option('cosmetic', 'bombchu_trail_color_inner', 9, 0)
dropdown_option('cosmetic', 'bombchu_trail_color_outer', 9, 1)
dropdown_option('cosmetic', 'boomerang_trail_color_inner', 10, 0)
dropdown_option('cosmetic', 'boomerang_trail_color_outer', 10, 1)
dropdown_option('cosmetic', 'heart_color', 11, 0)
dropdown_option('cosmetic', 'magic_color', 12, 0)
dropdown_option('cosmetic', 'a_button_color', 11, 1)
dropdown_option('cosmetic', 'b_button_color', 11, 2)
dropdown_option('cosmetic', 'c_button_color', 12, 1)
dropdown_option('cosmetic', 'start_button_color', 12, 2)
dropdown_option('sfx', 'sfx_navi_overworld', 14, 0)
dropdown_option('sfx', 'sfx_navi_enemy', 14, 1)
dropdown_option('sfx', 'sfx_low_hp', 14, 2)
dropdown_option('sfx', 'sfx_menu_cursor', 15, 0)
dropdown_option('sfx', 'sfx_menu_select', 15, 1)
dropdown_option('sfx', 'sfx_nightfall', 15, 2)
dropdown_option('sfx', 'sfx_horse_neigh', 16, 0)
dropdown_option('sfx', 'sfx_hover_boots', 16, 1)
dropdown_option('sfx', 'sfx_ocarina', 16, 2)
# Special cases
# Sword trail duration is a range
option = cosmetic_options['sword_trail_duration']
optionFrame = Frame(romSettingsFrame)
optionFrame.grid(row=8, column=2, sticky=E)
optionLabel = Label(optionFrame, text=option.displayname)
optionLabel.pack(side=LEFT)
setattr(opts, 'sword_trail_duration', StringVar())
getattr(opts, 'sword_trail_duration').set(option.default)
optionMenu = OptionMenu(optionFrame, getattr(opts, 'sword_trail_duration'), *range(4, 21))
optionMenu.pack(side=LEFT)
# Glitched is a checkbox
opts.is_glitched = IntVar(value=0)
glitched_checkbox = Checkbutton(romSettingsFrame, text="Glitched Logic?", variable=opts.is_glitched)
glitched_checkbox.grid(row=17, column=0, sticky=W)
# Deathlink is a checkbox
opts.deathlink = IntVar(value=0)
deathlink_checkbox = Checkbutton(romSettingsFrame, text="DeathLink (Team Deaths)", variable=opts.deathlink)
deathlink_checkbox.grid(row=17, column=1, sticky=W)
romSettingsFrame.pack(side=TOP)
def adjustRom():
try:
guiargs = Namespace()
options = vars(opts)
for o in options:
result = options[o].get()
if result == 'true':
result = True
if result == 'false':
result = False
setattr(guiargs, o, result)
guiargs.sword_trail_duration = int(guiargs.sword_trail_duration)
path = adjust(guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
# Adjust button
bottomFrame = Frame(window)
adjustButton = Button(bottomFrame, text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 5))
bottomFrame.pack(side=BOTTOM, pady=(5, 5))
window.mainloop()
def set_icon(window):
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
window.tk.call('wm', 'iconphoto', window._w, logo)
def adjust(args):
# Create a fake world and OOTWorld to use as a base
world = MultiWorld(1)
world.slot_seeds = {1: random}
ootworld = OOTWorld(world, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
result = getattr(args, name, None)
if result is None:
if issubclass(option, Choice):
result = option.name_lookup[option.default]
elif issubclass(option, Range) or issubclass(option, Toggle):
result = option.default
else:
raise Exception("Unsupported option type")
setattr(ootworld, name, result)
ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless'
ootworld.death_link = args.deathlink
if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']:
# Load up the ROM
rom = Rom(file=args.rom, force_use=True)
elif os.path.splitext(args.rom)[-1] == '.apz5':
# Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True)
# Patch file
apply_patch_file(rom, args.rom)
else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
# Call patch_cosmetics
patch_cosmetics(ootworld, rom)
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
# Output new file
path_pieces = os.path.splitext(args.rom)
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
comp_path = path_pieces[0] + '-adjusted.n64'
rom.write_to_file(decomp_path)
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
return comp_path
if __name__ == '__main__':
main()

View File

@ -61,6 +61,7 @@ Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Se
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs] [Dirs]
@ -82,6 +83,7 @@ Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: igno
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
;minecraft temp files ;minecraft temp files

View File

@ -61,6 +61,7 @@ Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Se
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs] [Dirs]
@ -82,6 +83,7 @@ Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: igno
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
;minecraft temp files ;minecraft temp files

View File

@ -80,6 +80,8 @@ scripts = {
"FactorioClient.py": ("ArchipelagoFactorioClient", True, icon), "FactorioClient.py": ("ArchipelagoFactorioClient", True, icon),
# Minecraft # Minecraft
"MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon), "MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon),
# Ocarina of Time
"OoTAdjuster.py": ("ArchipelagoOoTAdjuster", True, icon),
} }
exes = [] exes = []

View File

@ -1,6 +1,6 @@
class Dungeon(object): class Dungeon(object):
def __init__(self, world, name, hint, boss_key, small_keys, dungeon_items): def __init__(self, world, name, hint, font_color, boss_key, small_keys, dungeon_items):
def to_array(obj): def to_array(obj):
if obj == None: if obj == None:
return [] return []
@ -12,6 +12,7 @@ class Dungeon(object):
self.world = world self.world = world
self.name = name self.name = name
self.hint_text = hint self.hint_text = hint
self.font_color = font_color
self.regions = [] self.regions = []
self.boss_key = to_array(boss_key) self.boss_key = to_array(boss_key)
self.small_keys = to_array(small_keys) self.small_keys = to_array(small_keys)
@ -28,7 +29,7 @@ class Dungeon(object):
new_small_keys = [item.copy(new_world) for item in self.small_keys] 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_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) new_dungeon = Dungeon(new_world, self.name, self.hint_text, self.font_color, new_boss_key, new_small_keys, new_dungeon_items)
return new_dungeon return new_dungeon

View File

@ -7,6 +7,8 @@ from .Utils import data_path
dungeon_table = [ dungeon_table = [
{ {
'name': 'Deku Tree', 'name': 'Deku Tree',
'hint': 'the Deku Tree',
'font_color': 'Green',
'boss_key': 0, 'boss_key': 0,
'small_key': 0, 'small_key': 0,
'small_key_mq': 0, 'small_key_mq': 0,
@ -15,6 +17,7 @@ dungeon_table = [
{ {
'name': 'Dodongos Cavern', 'name': 'Dodongos Cavern',
'hint': 'Dodongo\'s Cavern', 'hint': 'Dodongo\'s Cavern',
'font_color': 'Red',
'boss_key': 0, 'boss_key': 0,
'small_key': 0, 'small_key': 0,
'small_key_mq': 0, 'small_key_mq': 0,
@ -23,6 +26,7 @@ dungeon_table = [
{ {
'name': 'Jabu Jabus Belly', 'name': 'Jabu Jabus Belly',
'hint': 'Jabu Jabu\'s Belly', 'hint': 'Jabu Jabu\'s Belly',
'font_color': 'Blue',
'boss_key': 0, 'boss_key': 0,
'small_key': 0, 'small_key': 0,
'small_key_mq': 0, 'small_key_mq': 0,
@ -30,6 +34,8 @@ dungeon_table = [
}, },
{ {
'name': 'Forest Temple', 'name': 'Forest Temple',
'hint': 'the Forest Temple',
'font_color': 'Green',
'boss_key': 1, 'boss_key': 1,
'small_key': 5, 'small_key': 5,
'small_key_mq': 6, 'small_key_mq': 6,
@ -37,6 +43,8 @@ dungeon_table = [
}, },
{ {
'name': 'Bottom of the Well', 'name': 'Bottom of the Well',
'hint': 'the Bottom of the Well',
'font_color': 'Pink',
'boss_key': 0, 'boss_key': 0,
'small_key': 3, 'small_key': 3,
'small_key_mq': 2, 'small_key_mq': 2,
@ -44,6 +52,8 @@ dungeon_table = [
}, },
{ {
'name': 'Fire Temple', 'name': 'Fire Temple',
'hint': 'the Fire Temple',
'font_color': 'Red',
'boss_key': 1, 'boss_key': 1,
'small_key': 8, 'small_key': 8,
'small_key_mq': 5, 'small_key_mq': 5,
@ -51,6 +61,8 @@ dungeon_table = [
}, },
{ {
'name': 'Ice Cavern', 'name': 'Ice Cavern',
'hint': 'the Ice Cavern',
'font_color': 'Blue',
'boss_key': 0, 'boss_key': 0,
'small_key': 0, 'small_key': 0,
'small_key_mq': 0, 'small_key_mq': 0,
@ -58,6 +70,8 @@ dungeon_table = [
}, },
{ {
'name': 'Water Temple', 'name': 'Water Temple',
'hint': 'the Water Temple',
'font_color': 'Blue',
'boss_key': 1, 'boss_key': 1,
'small_key': 6, 'small_key': 6,
'small_key_mq': 2, 'small_key_mq': 2,
@ -65,6 +79,8 @@ dungeon_table = [
}, },
{ {
'name': 'Shadow Temple', 'name': 'Shadow Temple',
'hint': 'the Shadow Temple',
'font_color': 'Pink',
'boss_key': 1, 'boss_key': 1,
'small_key': 5, 'small_key': 5,
'small_key_mq': 6, 'small_key_mq': 6,
@ -72,6 +88,8 @@ dungeon_table = [
}, },
{ {
'name': 'Gerudo Training Grounds', 'name': 'Gerudo Training Grounds',
'hint': 'the Gerudo Training Grounds',
'font_color': 'Yellow',
'boss_key': 0, 'boss_key': 0,
'small_key': 9, 'small_key': 9,
'small_key_mq': 3, 'small_key_mq': 3,
@ -79,6 +97,8 @@ dungeon_table = [
}, },
{ {
'name': 'Spirit Temple', 'name': 'Spirit Temple',
'hint': 'the Spirit Temple',
'font_color': 'Yellow',
'boss_key': 1, 'boss_key': 1,
'small_key': 5, 'small_key': 5,
'small_key_mq': 7, 'small_key_mq': 7,
@ -100,6 +120,7 @@ def create_dungeons(ootworld):
for dungeon_info in dungeon_table: for dungeon_info in dungeon_table:
name = dungeon_info['name'] name = dungeon_info['name']
hint = dungeon_info['hint'] if 'hint' in dungeon_info else name hint = dungeon_info['hint'] if 'hint' in dungeon_info else name
font_color = dungeon_info['font_color'] if 'font_color' in dungeon_info else 'White'
if ootworld.logic_rules == 'glitchless': if ootworld.logic_rules == 'glitchless':
if not ootworld.dungeon_mq[name]: if not ootworld.dungeon_mq[name]:
@ -125,5 +146,5 @@ def create_dungeons(ootworld):
for item in dungeon_items: for item in dungeon_items:
item.priority = True item.priority = True
ootworld.dungeons.append(Dungeon(ootworld, name, hint, boss_keys, small_keys, dungeon_items)) ootworld.dungeons.append(Dungeon(ootworld, name, hint, font_color, boss_keys, small_keys, dungeon_items))

View File

@ -1,7 +1,7 @@
from itertools import chain from itertools import chain
import logging import logging
from worlds.generic.Rules import set_rule from worlds.generic.Rules import set_rule, add_rule
from .Hints import get_hint_area, HintAreaNotFound from .Hints import get_hint_area, HintAreaNotFound
from .Regions import TimeOfDay from .Regions import TimeOfDay
@ -29,12 +29,13 @@ def assume_entrance_pool(entrance_pool, ootworld):
assumed_pool = [] assumed_pool = []
for entrance in entrance_pool: for entrance in entrance_pool:
assumed_forward = entrance.assume_reachable() assumed_forward = entrance.assume_reachable()
if entrance.reverse != None: if entrance.reverse != None and not ootworld.decouple_entrances:
assumed_return = entrance.reverse.assume_reachable() assumed_return = entrance.reverse.assume_reachable()
if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \ if not (ootworld.mix_entrance_pools != 'off' and (ootworld.shuffle_overworld_entrances or ootworld.shuffle_special_interior_entrances)):
(entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances): if (entrance.type in ('Dungeon', 'Grotto', 'Grave') and entrance.reverse.name != 'Spirit Temple Lobby -> Desert Colossus From Spirit Lobby') or \
# In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region (entrance.type == 'Interior' and ootworld.shuffle_special_interior_entrances):
set_rule(assumed_return, lambda state, **kwargs: False) # In most cases, Dungeon, Grotto/Grave and Simple Interior exits shouldn't be assumed able to give access to their parent region
set_rule(assumed_return, lambda state, **kwargs: False)
assumed_forward.bind_two_way(assumed_return) assumed_forward.bind_two_way(assumed_return)
assumed_pool.append(assumed_forward) assumed_pool.append(assumed_forward)
return assumed_pool return assumed_pool
@ -308,6 +309,8 @@ entrance_shuffle_table = [
('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }), ('Overworld', ('ZD Behind King Zora -> Zoras Fountain', { 'index': 0x0225 }),
('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })), ('Zoras Fountain -> ZD Behind King Zora', { 'index': 0x01A1 })),
('Overworld', ('GV Lower Stream -> Lake Hylia', { 'index': 0x0219 })),
('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })), ('OwlDrop', ('LH Owl Flight -> Hyrule Field', { 'index': 0x027E, 'addresses': [0xAC9F26] })),
('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })), ('OwlDrop', ('DMT Owl Flight -> Kak Impas Rooftop', { 'index': 0x0554, 'addresses': [0xAC9EF2] })),
@ -376,15 +379,24 @@ def shuffle_random_entrances(ootworld):
entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True) entrance_pools['Dungeon'] = ootworld.get_shufflable_entrances(type='Dungeon', only_primary=True)
if ootworld.open_forest == 'closed': if ootworld.open_forest == 'closed':
entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player)) entrance_pools['Dungeon'].remove(world.get_entrance('KF Outside Deku Tree -> Deku Tree Lobby', player))
if ootworld.decouple_entrances:
entrance_pools['DungeonReverse'] = [entrance.reverse for entrance in entrance_pools['Dungeon']]
if ootworld.shuffle_interior_entrances != 'off': if ootworld.shuffle_interior_entrances != 'off':
entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True) entrance_pools['Interior'] = ootworld.get_shufflable_entrances(type='Interior', only_primary=True)
if ootworld.shuffle_special_interior_entrances: if ootworld.shuffle_special_interior_entrances:
entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True) entrance_pools['Interior'] += ootworld.get_shufflable_entrances(type='SpecialInterior', only_primary=True)
if ootworld.decouple_entrances:
entrance_pools['InteriorReverse'] = [entrance.reverse for entrance in entrance_pools['Interior']]
if ootworld.shuffle_grotto_entrances: if ootworld.shuffle_grotto_entrances:
entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True) entrance_pools['GrottoGrave'] = ootworld.get_shufflable_entrances(type='Grotto', only_primary=True)
entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True) entrance_pools['GrottoGrave'] += ootworld.get_shufflable_entrances(type='Grave', only_primary=True)
if ootworld.decouple_entrances:
entrance_pools['GrottoGraveReverse'] = [entrance.reverse for entrance in entrance_pools['GrottoGrave']]
if ootworld.shuffle_overworld_entrances: if ootworld.shuffle_overworld_entrances:
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld') exclude_overworld_reverse = ootworld.mix_entrance_pools == 'all' and not ootworld.decouple_entrances
entrance_pools['Overworld'] = ootworld.get_shufflable_entrances(type='Overworld', only_primary=exclude_overworld_reverse)
if not ootworld.decouple_entrances:
entrance_pools['Overworld'].remove(world.get_entrance('GV Lower Stream -> Lake Hylia', player))
# Mark shuffled entrances # Mark shuffled entrances
for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())): for entrance in chain(chain.from_iterable(one_way_entrance_pools.values()), chain.from_iterable(entrance_pools.values())):
@ -392,6 +404,16 @@ def shuffle_random_entrances(ootworld):
if entrance.reverse: if entrance.reverse:
entrance.reverse.shuffled = True entrance.reverse.shuffled = True
# Combine all entrance pools if mixing
if ootworld.mix_entrance_pools == 'all':
entrance_pools = {'Mixed': list(chain.from_iterable(entrance_pools.values()))}
elif ootworld.mix_entrance_pools == 'indoor':
if ootworld.shuffle_overworld_entrances:
ow_pool = entrance_pools['Overworld']
entrance_pools = {'Mixed': list(filter(lambda entrance: entrance.type != 'Overworld', chain.from_iterable(entrance_pools.values())))}
if ootworld.shuffle_overworld_entrances:
entrance_pools['Overworld'] = ow_pool
# Build target entrance pools # Build target entrance pools
one_way_target_entrance_pools = {} one_way_target_entrance_pools = {}
for pool_type, entrance_pool in one_way_entrance_pools.items(): for pool_type, entrance_pool in one_way_entrance_pools.items():
@ -403,7 +425,9 @@ def shuffle_random_entrances(ootworld):
elif pool_type in {'Spawn', 'WarpSong'}: elif pool_type in {'Spawn', 'WarpSong'}:
valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra') valid_target_types = ('Spawn', 'WarpSong', 'OwlDrop', 'Overworld', 'Interior', 'SpecialInterior', 'Extra')
one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types) one_way_target_entrance_pools[pool_type] = build_one_way_targets(ootworld, valid_target_types)
# Ensure that the last entrance doesn't assume the rest of the targets are reachable? # Ensure that the last entrance doesn't assume the rest of the targets are reachable
for target in one_way_target_entrance_pools[pool_type]:
add_rule(target, (lambda entrances=entrance_pool: (lambda state: any(entrance.connected_region == None for entrance in entrances)))())
# Disconnect one-way entrances for priority placement # Disconnect one-way entrances for priority placement
for entrance in chain.from_iterable(one_way_entrance_pools.values()): for entrance in chain.from_iterable(one_way_entrance_pools.values()):
entrance.disconnect() entrance.disconnect()
@ -419,7 +443,52 @@ def shuffle_random_entrances(ootworld):
if item_tuple[1] == player: if item_tuple[1] == player:
none_state.prog_items[item_tuple] = 0 none_state.prog_items[item_tuple] = 0
# Plando entrances? # Plando entrances
if world.plando_connections[player]:
rollbacks = []
all_targets = {**one_way_target_entrance_pools, **target_entrance_pools}
for conn in world.plando_connections[player]:
try:
entrance = ootworld.get_entrance(conn.entrance)
exit = ootworld.get_entrance(conn.exit)
if entrance is None:
raise EntranceShuffleError(f"Could not find entrance to plando: {conn.entrance}")
if exit is None:
raise EntranceShuffleError(f"Could not find entrance to plando: {conn.exit}")
target_region = exit.name.split(' -> ')[1]
target_parent = exit.parent_region.name
pool_type = entrance.type
matched_targets_to_region = list(filter(lambda target: target.connected_region and target.connected_region.name == target_region,
all_targets[pool_type]))
target = next(filter(lambda target: target.replaces.parent_region.name == target_parent, matched_targets_to_region))
replace_entrance(ootworld, entrance, target, rollbacks, locations_to_ensure_reachable, all_state, none_state)
if conn.direction == 'both' and entrance.reverse and ootworld.decouple_entrances:
replace_entrance(ootworld, entrance.reverse, target.reverse, rollbacks, locations_to_ensure_reachable, all_state, none_state)
except EntranceShuffleError as e:
raise RuntimeError(f"Failed to plando OoT entrances. Reason: {e}")
except StopIteration:
raise RuntimeError(f"Could not find entrance to plando: {conn.entrance} => {conn.exit}")
finally:
for (entrance, target) in rollbacks:
confirm_replacement(entrance, target)
# Check placed one way entrances and trim.
# The placed entrances are already pointing at their new regions.
placed_entrances = [entrance for entrance in chain.from_iterable(one_way_entrance_pools.values())
if entrance.replaces is not None]
replaced_entrances = [entrance.replaces for entrance in placed_entrances]
# Remove replaced entrances so we don't place two in one target.
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
if remaining_target.replaces and remaining_target.replaces in replaced_entrances:
delete_target_entrance(remaining_target)
# Remove priority targets if any placed entrances point at their region(s).
for key, (regions, _) in priority_entrance_table.items():
if key in one_way_priorities:
for entrance in placed_entrances:
if entrance.connected_region and entrance.connected_region.name in regions:
del one_way_priorities[key]
break
# Place priority entrances # Place priority entrances
shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools, locations_to_ensure_reachable, all_state, none_state, retry_count=2) shuffle_one_way_priority_entrances(ootworld, one_way_priorities, one_way_entrance_pools, one_way_target_entrance_pools, locations_to_ensure_reachable, all_state, none_state, retry_count=2)
@ -619,24 +688,25 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
time_travel_state.collect(ootworld.create_item('Time Travel'), event=True) time_travel_state.collect(ootworld.create_item('Time Travel'), event=True)
time_travel_state._oot_update_age_reachable_regions(player) time_travel_state._oot_update_age_reachable_regions(player)
# For various reasons, we don't want the player to end up through certain entrances as the wrong age # Unless entrances are decoupled, we don't want the player to end up through certain entrances as the wrong age
# This means we need to hard check that none of the relevant entrances are ever reachable as that age # This means we need to hard check that none of the relevant entrances are ever reachable as that age
# This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop) # This is mostly relevant when shuffling special interiors (such as windmill or kak potion shop)
# Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well # Warp Songs and Overworld Spawns can also end up inside certain indoors so those need to be handled as well
CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side'] CHILD_FORBIDDEN = ['OGC Great Fairy Fountain -> Castle Grounds', 'GV Carpenter Tent -> GV Fortress Side']
ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds'] ADULT_FORBIDDEN = ['HC Great Fairy Fountain -> Castle Grounds', 'HC Storms Grotto -> Castle Grounds']
for entrance in ootworld.get_shufflable_entrances(): if not ootworld.decouple_entrances:
if entrance.shuffled and entrance.replaces: for entrance in ootworld.get_shufflable_entrances():
if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]): if entrance.shuffled and entrance.replaces:
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access') if entrance.replaces.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.replaces.reverse]):
if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]): raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential child access')
raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access') if entrance.replaces.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.replaces.reverse]):
else: raise EntranceShuffleError(f'{entrance.replaces.name} replaced by an entrance with potential adult access')
if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]): else:
raise EntranceShuffleError(f'{entrance.name} potentially accessible as child') if entrance.name in CHILD_FORBIDDEN and not entrance_unreachable_as(entrance, 'child', already_checked=[entrance.reverse]):
if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]): raise EntranceShuffleError(f'{entrance.name} potentially accessible as child')
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult') if entrance.name in ADULT_FORBIDDEN and not entrance_unreachable_as(entrance, 'adult', already_checked=[entrance.reverse]):
raise EntranceShuffleError(f'{entrance.name} potentially accessible as adult')
# Check if all locations are reachable if not beatable-only or game is not yet complete # Check if all locations are reachable if not beatable-only or game is not yet complete
if locations_to_ensure_reachable: if locations_to_ensure_reachable:
@ -645,7 +715,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
if not all_state.can_reach(loc, 'Location', player): if not all_state.can_reach(loc, 'Location', player):
raise EntranceShuffleError(f'{loc} is unreachable') raise EntranceShuffleError(f'{loc} is unreachable')
if ootworld.shuffle_interior_entrances and (entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']): if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints # Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player) potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player) potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
@ -733,14 +804,14 @@ def get_entrance_replacing(region, entrance_name, player):
def change_connections(entrance, target): def change_connections(entrance, target):
entrance.connect(target.disconnect()) entrance.connect(target.disconnect())
entrance.replaces = target.replaces entrance.replaces = target.replaces
if entrance.reverse: if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
target.replaces.reverse.connect(entrance.reverse.assumed.disconnect()) target.replaces.reverse.connect(entrance.reverse.assumed.disconnect())
target.replaces.reverse.replaces = entrance.reverse target.replaces.reverse.replaces = entrance.reverse
def restore_connections(entrance, target): def restore_connections(entrance, target):
target.connect(entrance.disconnect()) target.connect(entrance.disconnect())
entrance.replaces = None entrance.replaces = None
if entrance.reverse: if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
entrance.reverse.assumed.connect(target.replaces.reverse.disconnect()) entrance.reverse.assumed.connect(target.replaces.reverse.disconnect())
target.replaces.reverse.replaces = None target.replaces.reverse.replaces = None
@ -757,7 +828,7 @@ def check_entrances_compatibility(entrance, target, rollbacks):
def confirm_replacement(entrance, target): def confirm_replacement(entrance, target):
delete_target_entrance(target) delete_target_entrance(target)
logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}') logging.getLogger('').debug(f'Connected {entrance} to {entrance.connected_region}')
if entrance.reverse: if entrance.reverse and not entrance.world.worlds[entrance.player].decouple_entrances:
replaced_reverse = target.replaces.reverse replaced_reverse = target.replaces.reverse
delete_target_entrance(entrance.reverse.assumed) delete_target_entrance(entrance.reverse.assumed)
logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}') logging.getLogger('').debug(f'Connected {replaced_reverse} to {replaced_reverse.connected_region}')

View File

@ -11,7 +11,7 @@ import json
from enum import Enum from enum import Enum
from .HintList import getHint, getHintGroup, Hint, hintExclusions from .HintList import getHint, getHintGroup, Hint, hintExclusions
from .Messages import update_message_by_id from .Messages import COLOR_MAP, update_message_by_id
from .TextBox import line_wrap from .TextBox import line_wrap
from .Utils import data_path, read_json from .Utils import data_path, read_json
@ -266,17 +266,6 @@ def getSimpleHintNoPrefix(item):
def colorText(gossip_text): def colorText(gossip_text):
colorMap = {
'White': '\x40',
'Red': '\x41',
'Green': '\x42',
'Blue': '\x43',
'Light Blue': '\x44',
'Pink': '\x45',
'Yellow': '\x46',
'Black': '\x47',
}
text = gossip_text.text text = gossip_text.text
colors = list(gossip_text.colors) if gossip_text.colors is not None else [] colors = list(gossip_text.colors) if gossip_text.colors is not None else []
color = 'White' color = 'White'
@ -292,7 +281,7 @@ def colorText(gossip_text):
splitText[1] = splitText[1][len(prefix):] splitText[1] = splitText[1][len(prefix):]
break break
splitText[1] = '\x05' + colorMap[color] + splitText[1] + '\x05\x40' splitText[1] = '\x05' + COLOR_MAP[color] + splitText[1] + '\x05\x40'
text = ''.join(splitText) text = ''.join(splitText)
return text return text
@ -649,9 +638,9 @@ def buildWorldGossipHints(world, checkedLocations=None):
if checkedLocations is None: if checkedLocations is None:
checkedLocations = {player: set() for player in world.world.player_ids} checkedLocations = {player: set() for player in world.world.player_ids}
# If Ganondorf can be reached without Light Arrows, add to checkedLocations to prevent extra hinting # If Ganondorf hints Light Arrows and is reachable without them, add to checkedLocations to prevent extra hinting
# Can only be forced with vanilla bridge or trials # Can only be forced with vanilla bridge or trials
if world.bridge != 'vanilla' and world.trials == 0: if world.bridge != 'vanilla' and world.trials == 0 and world.misc_hints:
try: try:
light_arrow_location = world.world.find_item("Light Arrows", world.player) light_arrow_location = world.world.find_item("Light Arrows", world.player)
checkedLocations[light_arrow_location.player].add(light_arrow_location.name) checkedLocations[light_arrow_location.player].add(light_arrow_location.name)

View File

@ -1329,9 +1329,10 @@ def get_pool_core(world):
# We can resolve this by starting with some extra keys # We can resolve this by starting with some extra keys
if world.dungeon_mq['Spirit Temple']: if world.dungeon_mq['Spirit Temple']:
# Yes somehow you need 3 keys. This dungeon is bonkers # Yes somehow you need 3 keys. This dungeon is bonkers
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)')) items = [world.create_item('Small Key (Spirit Temple)') for i in range(3)]
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)')) for item in items:
world.world.push_precollected(world.create_item('Small Key (Spirit Temple)')) world.world.push_precollected(item)
world.remove_from_start_inventory.append(item.name)
#if not world.dungeon_mq['Fire Temple']: #if not world.dungeon_mq['Fire Temple']:
# world.state.collect(ItemFactory('Small Key (Fire Temple)')) # world.state.collect(ItemFactory('Small Key (Fire Temple)'))
if world.shuffle_bosskeys == 'vanilla': if world.shuffle_bosskeys == 'vanilla':

View File

@ -1,5 +1,6 @@
# text details: https://wiki.cloudmodding.com/oot/Text_Format # text details: https://wiki.cloudmodding.com/oot/Text_Format
import logging
import random import random
from .TextBox import line_wrap from .TextBox import line_wrap
@ -316,6 +317,17 @@ KEYSANITY_MESSAGES = {
0x00A9: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09", 0x00A9: "\x13\x77\x08You found a \x05\x41Small Key\x05\x40\x01for the \x05\x45Shadow Temple\x05\x40!\x09",
} }
COLOR_MAP = {
'White': '\x40',
'Red': '\x41',
'Green': '\x42',
'Blue': '\x43',
'Light Blue': '\x44',
'Pink': '\x45',
'Yellow': '\x46',
'Black': '\x47',
}
MISC_MESSAGES = { MISC_MESSAGES = {
0x507B: (bytearray( 0x507B: (bytearray(
b"\x08I tell you, I saw him!\x04" \ b"\x08I tell you, I saw him!\x04" \
@ -995,3 +1007,30 @@ def shuffle_messages(messages, except_hints=True, always_allow_skip=True):
])) ]))
return permutation return permutation
# Update warp song text boxes for ER
def update_warp_song_text(messages, ootworld):
msg_list = {
0x088D: 'Minuet of Forest Warp -> Sacred Forest Meadow',
0x088E: 'Bolero of Fire Warp -> DMC Central Local',
0x088F: 'Serenade of Water Warp -> Lake Hylia',
0x0890: 'Requiem of Spirit Warp -> Desert Colossus',
0x0891: 'Nocturne of Shadow Warp -> Graveyard Warp Pad Region',
0x0892: 'Prelude of Light Warp -> Temple of Time',
}
for id, entr in msg_list.items():
destination = ootworld.world.get_entrance(entr, ootworld.player).connected_region
if destination.pretty_name:
destination_name = destination.pretty_name
elif destination.hint_text:
destination_name = destination.hint_text
elif destination.dungeon:
destination_name = destination.dungeon.hint
else:
destination_name = destination.name
color = COLOR_MAP[destination.font_color or 'White']
new_msg = f"\x08\x05{color}Warp to {destination_name}?\x05\40\x09\x01\x01\x1b\x05{color}OK\x01No\x05\40"
update_message_by_id(messages, id, new_msg)

View File

@ -96,6 +96,7 @@ class StartingAge(Choice):
class InteriorEntrances(Choice): class InteriorEntrances(Choice):
"""Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop.""" """Shuffles interior entrances. "Simple" shuffles houses and Great Fairies; "All" includes Windmill, Link's House, Temple of Time, and Kak potion shop."""
displayname = "Shuffle Interior Entrances"
option_off = 0 option_off = 0
option_simple = 1 option_simple = 1
option_all = 2 option_all = 2
@ -105,26 +106,46 @@ class InteriorEntrances(Choice):
class GrottoEntrances(Toggle): class GrottoEntrances(Toggle):
"""Shuffles grotto and grave entrances.""" """Shuffles grotto and grave entrances."""
displayname = "Shuffle Grotto/Grave Entrances"
class DungeonEntrances(Toggle): class DungeonEntrances(Toggle):
"""Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages.""" """Shuffles dungeon entrances, excluding Ganon's Castle. Opens Deku, Fire and BotW to both ages."""
displayname = "Shuffle Dungeon Entrances"
class OverworldEntrances(Toggle): class OverworldEntrances(Toggle):
"""Shuffles overworld loading zones.""" """Shuffles overworld loading zones."""
displayname = "Shuffle Overworld Entrances"
class OwlDrops(Toggle): class OwlDrops(Toggle):
"""Randomizes owl drops from Lake Hylia or Death Mountain Trail as child.""" """Randomizes owl drops from Lake Hylia or Death Mountain Trail as child."""
displayname = "Randomize Owl Drops"
class WarpSongs(Toggle): class WarpSongs(Toggle):
"""Randomizes warp song destinations.""" """Randomizes warp song destinations."""
displayname = "Randomize Warp Songs"
class SpawnPositions(Toggle): class SpawnPositions(Toggle):
"""Randomizes the starting position on loading a save. Consistent between savewarps.""" """Randomizes the starting position on loading a save. Consistent between savewarps."""
displayname = "Randomize Spawn Positions"
class MixEntrancePools(Choice):
"""Shuffles entrances into a mixed pool instead of separate ones. "indoor" keeps overworld entrances separate; "all" mixes them in."""
displayname = "Mix Entrance Pools"
option_off = 0
option_indoor = 1
option_all = 2
alias_false = 0
class DecoupleEntrances(Toggle):
"""Decouple entrances when shuffling them. Also adds the one-way entrance from Gerudo Valley to Lake Hylia if overworld is shuffled."""
displayname = "Decouple Entrances"
class TriforceHunt(Toggle): class TriforceHunt(Toggle):
@ -170,6 +191,8 @@ world_options: typing.Dict[str, type(Option)] = {
"owl_drops": OwlDrops, "owl_drops": OwlDrops,
"warp_songs": WarpSongs, "warp_songs": WarpSongs,
"spawn_positions": SpawnPositions, "spawn_positions": SpawnPositions,
"mix_entrance_pools": MixEntrancePools,
"decouple_entrances": DecoupleEntrances,
"triforce_hunt": TriforceHunt, "triforce_hunt": TriforceHunt,
"triforce_goal": TriforceGoal, "triforce_goal": TriforceGoal,
"extra_triforce_percentage": ExtraTriforces, "extra_triforce_percentage": ExtraTriforces,
@ -540,6 +563,11 @@ class Hints(Choice):
alias_false = 0 alias_false = 0
class MiscHints(DefaultOnToggle):
"""Controls whether the Temple of Time altar gives dungeon prize info and whether Ganondorf hints the Light Arrows."""
displayname = "Misc Hints"
class HintDistribution(Choice): class HintDistribution(Choice):
"""Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.""" """Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc."""
displayname = "Hint Distribution" displayname = "Hint Distribution"
@ -607,6 +635,7 @@ class RupeeStart(Toggle):
misc_options: typing.Dict[str, type(Option)] = { misc_options: typing.Dict[str, type(Option)] = {
"correct_chest_sizes": CSMC, "correct_chest_sizes": CSMC,
"hints": Hints, "hints": Hints,
"misc_hints": MiscHints,
"hint_dist": HintDistribution, "hint_dist": HintDistribution,
"text_shuffle": TextShuffle, "text_shuffle": TextShuffle,
"damage_multiplier": DamageMultiplier, "damage_multiplier": DamageMultiplier,

View File

@ -9,7 +9,7 @@ from .LocationList import business_scrubs
from .Hints import writeGossipStoneHints, buildAltarHints, \ from .Hints import writeGossipStoneHints, buildAltarHints, \
buildGanonText, getSimpleHintNoPrefix buildGanonText, getSimpleHintNoPrefix
from .Utils import data_path from .Utils import data_path
from .Messages import read_messages, update_message_by_id, read_shop_items, \ from .Messages import read_messages, update_message_by_id, read_shop_items, update_warp_song_text, \
write_shop_items, remove_unused_messages, make_player_message, \ write_shop_items, remove_unused_messages, make_player_message, \
add_item_messages, repack_messages, shuffle_messages, \ add_item_messages, repack_messages, shuffle_messages, \
get_message_by_id get_message_by_id
@ -1007,6 +1007,12 @@ def patch_rom(world, rom):
# Archipelago forces this item to be local so it can always be given to the player. Usually it's a song so it's no problem. # Archipelago forces this item to be local so it can always be given to the player. Usually it's a song so it's no problem.
item = world.get_location('Song from Impa').item item = world.get_location('Song from Impa').item
save_context.give_raw_item(item.name) save_context.give_raw_item(item.name)
if item.name == 'Slingshot':
save_context.give_raw_item("Deku Seeds (30)")
elif item.name == 'Bow':
save_context.give_raw_item("Arrows (30)")
elif item.name == 'Bomb Bag':
save_context.give_raw_item("Bombs (20)")
save_context.write_bits(0x0ED7, 0x04) # "Obtained Malon's Item" save_context.write_bits(0x0ED7, 0x04) # "Obtained Malon's Item"
save_context.write_bits(0x0ED7, 0x08) # "Woke Talon in castle" save_context.write_bits(0x0ED7, 0x08) # "Woke Talon in castle"
save_context.write_bits(0x0ED7, 0x10) # "Talon has fled castle" save_context.write_bits(0x0ED7, 0x10) # "Talon has fled castle"
@ -1634,7 +1640,7 @@ def patch_rom(world, rom):
rom.write_int16(chest_address + 2, 0x0190) # X pos rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos rom.write_int16(chest_address + 6, 0xFABC) # Z pos
else: else:
if location.item.advancement: if not location.item.advancement:
rom.write_int16(chest_address + 2, 0x0190) # X pos rom.write_int16(chest_address + 2, 0x0190) # X pos
rom.write_int16(chest_address + 6, 0xFABC) # Z pos rom.write_int16(chest_address + 6, 0xFABC) # Z pos
@ -1650,7 +1656,7 @@ def patch_rom(world, rom):
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
else: else:
if location.item.advancement: if not location.item.advancement:
rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos rom.write_int16(chest_address_0 + 6, 0x0172) # Z pos
rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos rom.write_int16(chest_address_2 + 6, 0x0172) # Z pos
@ -1741,6 +1747,10 @@ def patch_rom(world, rom):
elif world.text_shuffle == 'complete': elif world.text_shuffle == 'complete':
permutation = shuffle_messages(messages, except_hints=False) permutation = shuffle_messages(messages, except_hints=False)
# If Warp Song ER is on, update text boxes
if world.warp_songs:
update_warp_song_text(messages, world)
repack_messages(rom, messages, permutation) repack_messages(rom, messages, permutation)
# output a text dump, for testing... # output a text dump, for testing...

View File

@ -38,6 +38,8 @@ class OOTRegion(Region):
self.provides_time = TimeOfDay.NONE self.provides_time = TimeOfDay.NONE
self.scene = None self.scene = None
self.dungeon = None self.dungeon = None
self.pretty_name = None
self.font_color = None
def get_scene(self): def get_scene(self):
if self.scene: if self.scene:

View File

@ -17,7 +17,7 @@ double_cache_prevention = threading.Lock()
class Rom(BigStream): class Rom(BigStream):
original = None original = None
def __init__(self, file=None): def __init__(self, file=None, force_use=False):
super().__init__([]) super().__init__([])
self.changed_address = {} self.changed_address = {}
@ -34,22 +34,25 @@ class Rom(BigStream):
self.symbols = {name: int(addr, 16) for name, addr in symbols.items()} self.symbols = {name: int(addr, 16) for name, addr in symbols.items()}
# If decompressed file already exists, read from it # If decompressed file already exists, read from it
if os.path.exists(decomp_file): if not force_use:
file = decomp_file if os.path.exists(decomp_file):
file = decomp_file
if file == '': if file == '':
# if not specified, try to read from the previously decompressed rom # if not specified, try to read from the previously decompressed rom
file = decomp_file file = decomp_file
try: 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) self.read_rom(file)
except FileNotFoundError:
# could not find the decompressed rom either
raise FileNotFoundError('Must specify path to base ROM')
else: else:
self.read_rom(file) self.read_rom(file)
# decompress rom, or check if it's already decompressed # decompress rom, or check if it's already decompressed
self.decompress_rom_file(file, decomp_file) self.decompress_rom_file(file, decomp_file, force_use)
# Add file to maximum size # Add file to maximum size
self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer)))) self.buffer.extend(bytearray([0x00] * (0x4000000 - len(self.buffer))))
@ -69,7 +72,7 @@ class Rom(BigStream):
new_rom.force_patch = copy.copy(self.force_patch) new_rom.force_patch = copy.copy(self.force_patch)
return new_rom return new_rom
def decompress_rom_file(self, file, decomp_file): def decompress_rom_file(self, file, decomp_file, skip_crc_check):
validCRC = [ validCRC = [
[0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed [0xEC, 0x70, 0x11, 0xB7, 0x76, 0x16, 0xD7, 0x2B], # Compressed
[0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed [0x70, 0xEC, 0xB7, 0x11, 0x16, 0x76, 0x2B, 0xD7], # Byteswap compressed
@ -79,7 +82,7 @@ class Rom(BigStream):
# Validate ROM file # Validate ROM file
file_name = os.path.splitext(file) file_name = os.path.splitext(file)
romCRC = list(self.buffer[0x10:0x18]) romCRC = list(self.buffer[0x10:0x18])
if romCRC not in validCRC: if romCRC not in validCRC and not skip_crc_check:
# Bad CRC validation # Bad CRC validation
raise RuntimeError('ROM file %s is not a valid OoT 1.0 US ROM.' % file) 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', elif len(self.buffer) < 0x2000000 or len(self.buffer) > (0x4000000) or file_name[1].lower() not in ['.z64',

View File

@ -4,7 +4,7 @@ import subprocess
import Utils import Utils
from functools import lru_cache from functools import lru_cache
__version__ = Utils.__version__ + ' f.LUM' __version__ = '6.1.0 f.LUM'
def data_path(*args): def data_path(*args):

View File

@ -191,7 +191,6 @@ class OOTWorld(World):
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld'] self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
# Hint stuff # 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.clearer_hints = True # this is being enforced since non-oot items do not have non-clear hint text
self.gossip_hints = {} self.gossip_hints = {}
self.required_locations = [] self.required_locations = []
@ -276,6 +275,10 @@ class OOTWorld(World):
for region in region_json: for region in region_json:
new_region = OOTRegion(region['region_name'], RegionType.Generic, None, self.player) new_region = OOTRegion(region['region_name'], RegionType.Generic, None, self.player)
new_region.world = self.world new_region.world = self.world
if 'pretty_name' in region:
new_region.pretty_name = region['pretty_name']
if 'font_color' in region:
new_region.font_color = region['font_color']
if 'scene' in region: if 'scene' in region:
new_region.scene = region['scene'] new_region.scene = region['scene']
if 'hint' in region: if 'hint' in region:
@ -513,20 +516,6 @@ class OOTWorld(World):
else: else:
break break
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
set_rules(self) set_rules(self)
set_entrances_based_rules(self) set_entrances_based_rules(self)
@ -790,6 +779,24 @@ class OOTWorld(World):
create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5')) create_patch_file(rom, output_path(output_directory, outfile_name + '.apz5'))
rom.restore() rom.restore()
# Write entrances to spoiler log
all_entrances = self.get_shuffled_entrances()
all_entrances.sort(key=lambda x: x.name)
all_entrances.sort(key=lambda x: x.type)
if not self.decouple_entrances:
for loadzone in all_entrances:
if loadzone.primary:
entrance = loadzone
else:
entrance = loadzone.reverse
if entrance.reverse is not None:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'both', self.player)
else:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
else:
for entrance in all_entrances:
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
# Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations. # Gathers hint data for OoT. Loops over all world locations for woth, barren, and major item locations.
@classmethod @classmethod
def stage_generate_output(cls, world: MultiWorld, output_directory: str): def stage_generate_output(cls, world: MultiWorld, output_directory: str):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"AUDIO_THREAD_INFO": "03482FC0", "AUDIO_THREAD_INFO": "03482FC0",
"AUDIO_THREAD_INFO_MEM_SIZE": "03482FDC", "AUDIO_THREAD_INFO_MEM_SIZE": "03482FDC",
"AUDIO_THREAD_INFO_MEM_START": "03482FD8", "AUDIO_THREAD_INFO_MEM_START": "03482FD8",
"AUDIO_THREAD_MEM_START": "0348EF50", "AUDIO_THREAD_MEM_START": "0348EF80",
"BOMBCHUS_IN_LOGIC": "03480CBC", "BOMBCHUS_IN_LOGIC": "03480CBC",
"CFG_A_BUTTON_COLOR": "03480854", "CFG_A_BUTTON_COLOR": "03480854",
"CFG_A_NOTE_COLOR": "03480872", "CFG_A_NOTE_COLOR": "03480872",
@ -38,7 +38,7 @@
"CFG_TEXT_CURSOR_COLOR": "03480866", "CFG_TEXT_CURSOR_COLOR": "03480866",
"CHAIN_HBA_REWARDS": "03483950", "CHAIN_HBA_REWARDS": "03483950",
"CHEST_SIZE_MATCH_CONTENTS": "034826F0", "CHEST_SIZE_MATCH_CONTENTS": "034826F0",
"COMPLETE_MASK_QUEST": "0348B1D1", "COMPLETE_MASK_QUEST": "0348B201",
"COOP_CONTEXT": "03480020", "COOP_CONTEXT": "03480020",
"COOP_VERSION": "03480020", "COOP_VERSION": "03480020",
"COSMETIC_CONTEXT": "03480844", "COSMETIC_CONTEXT": "03480844",
@ -47,13 +47,13 @@
"DEATH_LINK": "0348002A", "DEATH_LINK": "0348002A",
"DEBUG_OFFSET": "034828A0", "DEBUG_OFFSET": "034828A0",
"DISABLE_TIMERS": "03480CDC", "DISABLE_TIMERS": "03480CDC",
"DPAD_TEXTURE": "0348D750", "DPAD_TEXTURE": "0348D780",
"DUNGEONS_SHUFFLED": "03480CDE", "DUNGEONS_SHUFFLED": "03480CDE",
"EXTENDED_OBJECT_TABLE": "03480C9C", "EXTENDED_OBJECT_TABLE": "03480C9C",
"EXTERN_DAMAGE_MULTIPLYER": "03482CB1", "EXTERN_DAMAGE_MULTIPLYER": "03482CB1",
"FAST_BUNNY_HOOD_ENABLED": "03480CE0", "FAST_BUNNY_HOOD_ENABLED": "03480CE0",
"FAST_CHESTS": "03480CD6", "FAST_CHESTS": "03480CD6",
"FONT_TEXTURE": "0348C288", "FONT_TEXTURE": "0348C2B8",
"FREE_SCARECROW_ENABLED": "03480CCC", "FREE_SCARECROW_ENABLED": "03480CCC",
"GET_CHEST_OVERRIDE_COLOR_WRAPPER": "03482720", "GET_CHEST_OVERRIDE_COLOR_WRAPPER": "03482720",
"GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826F4", "GET_CHEST_OVERRIDE_SIZE_WRAPPER": "034826F4",
@ -69,17 +69,17 @@
"LACS_CONDITION_COUNT": "03480CD2", "LACS_CONDITION_COUNT": "03480CD2",
"MALON_GAVE_ICETRAP": "0348368C", "MALON_GAVE_ICETRAP": "0348368C",
"MALON_TEXT_ID": "03480CDB", "MALON_TEXT_ID": "03480CDB",
"MAX_RUPEES": "0348B1D3", "MAX_RUPEES": "0348B203",
"MOVED_ADULT_KING_ZORA": "03482FFC", "MOVED_ADULT_KING_ZORA": "03482FFC",
"NO_ESCAPE_SEQUENCE": "0348B19C", "NO_ESCAPE_SEQUENCE": "0348B1CC",
"NO_FOG_STATE": "03480CDD", "NO_FOG_STATE": "03480CDD",
"OCARINAS_SHUFFLED": "03480CD5", "OCARINAS_SHUFFLED": "03480CD5",
"OPEN_KAKARIKO": "0348B1D2", "OPEN_KAKARIKO": "0348B202",
"OUTGOING_ITEM": "03480030", "OUTGOING_ITEM": "03480030",
"OUTGOING_KEY": "0348002C", "OUTGOING_KEY": "0348002C",
"OUTGOING_PLAYER": "03480032", "OUTGOING_PLAYER": "03480032",
"OVERWORLD_SHUFFLED": "03480CDF", "OVERWORLD_SHUFFLED": "03480CDF",
"PAYLOAD_END": "0348EF50", "PAYLOAD_END": "0348EF80",
"PAYLOAD_START": "03480000", "PAYLOAD_START": "03480000",
"PLAYED_WARP_SONG": "03481210", "PLAYED_WARP_SONG": "03481210",
"PLAYER_ID": "03480024", "PLAYER_ID": "03480024",
@ -97,88 +97,88 @@
"SPEED_MULTIPLIER": "03482760", "SPEED_MULTIPLIER": "03482760",
"START_TWINROVA_FIGHT": "0348307C", "START_TWINROVA_FIGHT": "0348307C",
"TIME_TRAVEL_SAVED_EQUIPS": "03481A64", "TIME_TRAVEL_SAVED_EQUIPS": "03481A64",
"TRIFORCE_ICON_TEXTURE": "0348DF50", "TRIFORCE_ICON_TEXTURE": "0348DF80",
"TWINROVA_ACTION_TIMER": "03483080", "TWINROVA_ACTION_TIMER": "03483080",
"WINDMILL_SONG_ID": "03480CD9", "WINDMILL_SONG_ID": "03480CD9",
"WINDMILL_TEXT_ID": "03480CDA", "WINDMILL_TEXT_ID": "03480CDA",
"a_button": "0348B160", "a_button": "0348B190",
"a_note_b": "0348B14C", "a_note_b": "0348B17C",
"a_note_font_glow_base": "0348B134", "a_note_font_glow_base": "0348B164",
"a_note_font_glow_max": "0348B130", "a_note_font_glow_max": "0348B160",
"a_note_g": "0348B150", "a_note_g": "0348B180",
"a_note_glow_base": "0348B13C", "a_note_glow_base": "0348B16C",
"a_note_glow_max": "0348B138", "a_note_glow_max": "0348B168",
"a_note_r": "0348B154", "a_note_r": "0348B184",
"active_item_action_id": "0348B1B4", "active_item_action_id": "0348B1E4",
"active_item_fast_chest": "0348B1A4", "active_item_fast_chest": "0348B1D4",
"active_item_graphic_id": "0348B1A8", "active_item_graphic_id": "0348B1D8",
"active_item_object_id": "0348B1AC", "active_item_object_id": "0348B1DC",
"active_item_row": "0348B1B8", "active_item_row": "0348B1E8",
"active_item_text_id": "0348B1B0", "active_item_text_id": "0348B1E0",
"active_override": "0348B1C0", "active_override": "0348B1F0",
"active_override_is_outgoing": "0348B1BC", "active_override_is_outgoing": "0348B1EC",
"b_button": "0348B15C", "b_button": "0348B18C",
"beating_dd": "0348B168", "beating_dd": "0348B198",
"beating_no_dd": "0348B170", "beating_no_dd": "0348B1A0",
"c_button": "0348B158", "c_button": "0348B188",
"c_note_b": "0348B140", "c_note_b": "0348B170",
"c_note_font_glow_base": "0348B124", "c_note_font_glow_base": "0348B154",
"c_note_font_glow_max": "0348B120", "c_note_font_glow_max": "0348B150",
"c_note_g": "0348B144", "c_note_g": "0348B174",
"c_note_glow_base": "0348B12C", "c_note_glow_base": "0348B15C",
"c_note_glow_max": "0348B128", "c_note_glow_max": "0348B158",
"c_note_r": "0348B148", "c_note_r": "0348B178",
"cfg_dungeon_info_enable": "0348B0EC", "cfg_dungeon_info_enable": "0348B11C",
"cfg_dungeon_info_mq_enable": "0348B190", "cfg_dungeon_info_mq_enable": "0348B1C0",
"cfg_dungeon_info_mq_need_map": "0348B18C", "cfg_dungeon_info_mq_need_map": "0348B1BC",
"cfg_dungeon_info_reward_enable": "0348B0E8", "cfg_dungeon_info_reward_enable": "0348B118",
"cfg_dungeon_info_reward_need_altar": "0348B184", "cfg_dungeon_info_reward_need_altar": "0348B1B4",
"cfg_dungeon_info_reward_need_compass": "0348B188", "cfg_dungeon_info_reward_need_compass": "0348B1B8",
"cfg_dungeon_is_mq": "0348B1F0", "cfg_dungeon_is_mq": "0348B220",
"cfg_dungeon_rewards": "03489EE4", "cfg_dungeon_rewards": "03489F14",
"cfg_file_select_hash": "0348B198", "cfg_file_select_hash": "0348B1C8",
"cfg_item_overrides": "0348B244", "cfg_item_overrides": "0348B274",
"defaultDDHeart": "0348B174", "defaultDDHeart": "0348B1A4",
"defaultHeart": "0348B17C", "defaultHeart": "0348B1AC",
"dpad_sprite": "0348A058", "dpad_sprite": "0348A088",
"dummy_actor": "0348B1C8", "dummy_actor": "0348B1F8",
"dungeon_count": "0348B0F0", "dungeon_count": "0348B120",
"dungeons": "03489F08", "dungeons": "03489F38",
"empty_dlist": "0348B108", "empty_dlist": "0348B138",
"extern_ctxt": "03489FA4", "extern_ctxt": "03489FD4",
"font_sprite": "0348A068", "font_sprite": "0348A098",
"freecam_modes": "03489C60", "freecam_modes": "03489C90",
"hash_sprites": "0348B0FC", "hash_sprites": "0348B12C",
"hash_symbols": "03489FB8", "hash_symbols": "03489FE8",
"heap_next": "0348B1EC", "heap_next": "0348B21C",
"heart_sprite": "03489FF8", "heart_sprite": "0348A028",
"icon_sprites": "03489E24", "icon_sprites": "03489E54",
"item_digit_sprite": "0348A018", "item_digit_sprite": "0348A048",
"item_overrides_count": "0348B1CC", "item_overrides_count": "0348B1FC",
"item_table": "0348A0E0", "item_table": "0348A110",
"items_sprite": "0348A088", "items_sprite": "0348A0B8",
"key_rupee_clock_sprite": "0348A028", "key_rupee_clock_sprite": "0348A058",
"last_fog_distance": "0348B0F4", "last_fog_distance": "0348B124",
"linkhead_skull_sprite": "0348A008", "linkhead_skull_sprite": "0348A038",
"medal_colors": "03489EF4", "medal_colors": "03489F24",
"medals_sprite": "0348A098", "medals_sprite": "0348A0C8",
"normal_dd": "0348B164", "normal_dd": "0348B194",
"normal_no_dd": "0348B16C", "normal_no_dd": "0348B19C",
"object_slots": "0348C244", "object_slots": "0348C274",
"pending_freezes": "0348B1D0", "pending_freezes": "0348B200",
"pending_item_queue": "0348B22C", "pending_item_queue": "0348B25C",
"quest_items_sprite": "0348A078", "quest_items_sprite": "0348A0A8",
"rupee_colors": "03489E30", "rupee_colors": "03489E60",
"satisified_pending_frames": "0348B1A0", "satisified_pending_frames": "0348B1D0",
"scene_fog_distance": "0348B0F8", "scene_fog_distance": "0348B128",
"setup_db": "0348A0B8", "setup_db": "0348A0E8",
"song_note_sprite": "0348A038", "song_note_sprite": "0348A068",
"stones_sprite": "0348A0A8", "stones_sprite": "0348A0D8",
"text_cursor_border_base": "0348B114", "text_cursor_border_base": "0348B144",
"text_cursor_border_max": "0348B110", "text_cursor_border_max": "0348B140",
"text_cursor_inner_base": "0348B11C", "text_cursor_inner_base": "0348B14C",
"text_cursor_inner_max": "0348B118", "text_cursor_inner_max": "0348B148",
"triforce_hunt_enabled": "0348B1E0", "triforce_hunt_enabled": "0348B210",
"triforce_pieces_requied": "0348B182", "triforce_pieces_requied": "0348B1B2",
"triforce_sprite": "0348A048" "triforce_sprite": "0348A078"
} }