forked from mirror/Archipelago
Some checks failed
Analyze modified files / flake8 (push) Failing after 2m28s
Build / build-win (push) Has been cancelled
Build / build-ubuntu2204 (push) Has been cancelled
ctest / Test C++ ubuntu-latest (push) Has been cancelled
ctest / Test C++ windows-latest (push) Has been cancelled
Analyze modified files / mypy (push) Has been cancelled
Build and Publish Docker Images / Push Docker image to Docker Hub (push) Successful in 5m4s
Native Code Static Analysis / scan-build (push) Failing after 5m2s
type check / pyright (push) Successful in 1m7s
unittests / Test Python 3.11.2 ubuntu-latest (push) Failing after 16m23s
unittests / Test Python 3.12 ubuntu-latest (push) Failing after 28m19s
unittests / Test Python 3.13 ubuntu-latest (push) Failing after 14m49s
unittests / Test hosting with 3.13 on ubuntu-latest (push) Successful in 5m0s
unittests / Test Python 3.13 macos-latest (push) Has been cancelled
unittests / Test Python 3.11 windows-latest (push) Has been cancelled
unittests / Test Python 3.13 windows-latest (push) Has been cancelled
452 lines
20 KiB
Python
452 lines
20 KiB
Python
from typing import List
|
|
|
|
import os
|
|
import random
|
|
import Utils
|
|
from settings import get_settings
|
|
from . import RomData
|
|
from .Util import *
|
|
from .z80asm.Assembler import Z80Assembler
|
|
from ..data.Constants import *
|
|
from .Constants import *
|
|
from pathlib import Path
|
|
|
|
from .. import LOCATIONS_DATA, OracleOfAgesMasterKeys, OracleOfAgesGoal
|
|
|
|
|
|
def get_treasure_addr(rom: RomData, item_name: str):
|
|
item_id, item_subid = get_item_id_and_subid(item_name)
|
|
addr = 0x59332 + (item_id * 4)
|
|
if rom.read_byte(addr) & 0x80 != 0:
|
|
addr = 0x54000 + rom.read_word(addr + 1)
|
|
return addr + (item_subid * 4)
|
|
|
|
|
|
def set_treasure_data(rom: RomData,
|
|
item_name: str, text_id: int | None,
|
|
sprite_id: int | None = None,
|
|
param_value: int | None = None):
|
|
addr = get_treasure_addr(rom, item_name)
|
|
if text_id is not None:
|
|
rom.write_byte(addr + 0x02, text_id)
|
|
if sprite_id is not None:
|
|
rom.write_byte(addr + 0x03, sprite_id)
|
|
if param_value is not None:
|
|
rom.write_byte(addr + 0x01, param_value)
|
|
|
|
def alter_treasures(rom: RomData):
|
|
|
|
set_treasure_data(rom, "Potion", 0x6d)
|
|
|
|
# Set data for remote Archipelago items
|
|
set_treasure_data(rom, "Archipelago Item", 0x57, 0x5a)
|
|
set_treasure_data(rom, "Archipelago Progression Item", 0x57, 0x59)
|
|
set_treasure_data(rom, "King Zora's Potion", 0x45, 0x5e)
|
|
|
|
# Make bombs increase max carriable quantity when obtained from treasures,
|
|
# not drops (see asm/seasons/bomb_bag_behavior)
|
|
set_treasure_data(rom, "Bombs (10)", None, None, 0x90)
|
|
|
|
|
|
def get_asm_files(patch_data):
|
|
asm_files = ASM_FILES.copy()
|
|
if get_settings()["tloz_ooa_options"]["qol_quick_flute"]:
|
|
asm_files.append("asm/conditional/quick_flute.yaml")
|
|
if get_settings()["tloz_ooa_options"]["skip_tokkey_dance"]:
|
|
asm_files.append("asm/conditional/skip_dance.yaml")
|
|
if get_settings()["tloz_ooa_options"]["skip_boi_joke"]:
|
|
asm_files.append("asm/conditional/skip_joke.yaml")
|
|
if get_settings()["tloz_ooa_options"]["qol_mermaid_suit"]:
|
|
asm_files.append("asm/conditional/qol_mermaid_suit.yaml")
|
|
if patch_data["options"]["goal"] == OracleOfAgesGoal.option_beat_ganon:
|
|
asm_files.append("asm/conditional/ganon_goal.yaml")
|
|
return asm_files
|
|
|
|
def define_location_constants(assembler: Z80Assembler, patch_data):
|
|
for location_name, location_data in LOCATIONS_DATA.items():
|
|
if "symbolic_name" not in location_data:
|
|
continue
|
|
symbolic_name = location_data["symbolic_name"]
|
|
|
|
if location_name in patch_data["locations"]:
|
|
item_name = patch_data["locations"][location_name]
|
|
else:
|
|
item_name = location_data["vanilla_item"]
|
|
|
|
if item_name == "Flute":
|
|
item_name = COMPANIONS[patch_data["options"]["animal_companion"]] + "'s Flute"
|
|
|
|
item_id, item_subid = get_item_id_and_subid(item_name)
|
|
assembler.define_byte(f"locations.{symbolic_name}.id", item_id)
|
|
assembler.define_byte(f"locations.{symbolic_name}.subid", item_subid)
|
|
assembler.define_word(f"locations.{symbolic_name}", (item_id << 8) + item_subid)
|
|
|
|
|
|
def define_option_constants(assembler: Z80Assembler, patch_data):
|
|
options = patch_data["options"]
|
|
|
|
assembler.define_byte("option.startingGroup", 0x00)
|
|
assembler.define_byte("option.startingRoom", 0x59)
|
|
assembler.define_byte("option.startingPosY", 0x58)
|
|
assembler.define_byte("option.startingPosX", 0x58)
|
|
assembler.define_byte("option.startingPos", 0x55)
|
|
|
|
assembler.define_byte("option.animalCompanion", 0x0b + patch_data["options"]["animal_companion"])
|
|
assembler.define_byte("option.defaultSeedType", 0x20 + patch_data["options"]["default_seed"])
|
|
assembler.define_byte("option.receivedDamageModifier", options["combat_difficulty"])
|
|
assembler.define_byte("option.openAdvanceShop", options["advance_shop"])
|
|
|
|
assembler.define_byte("option.requiredEssences", options["required_essences"])
|
|
assembler.define_byte("option.required_slates", options["required_slates"])
|
|
|
|
assembler.define_byte("option.keysanity_small_keys", patch_data["options"]["keysanity_small_keys"])
|
|
keysanity = patch_data["options"]["keysanity_small_keys"] or patch_data["options"]["keysanity_boss_keys"]
|
|
assembler.define_byte("option.customCompassChimes", 1 if keysanity else 0)
|
|
|
|
master_keys_as_boss_keys = patch_data["options"]["master_keys"] == OracleOfAgesMasterKeys.option_all_dungeon_keys
|
|
assembler.define_byte("option.smallKeySprite", 0x43 if master_keys_as_boss_keys else 0x42)
|
|
|
|
def process_item_name_for_shop_text(item_name: str) -> List[int]:
|
|
words = item_name.split(" ")
|
|
current_line = 0
|
|
lines = [""]
|
|
while len(words) > 0:
|
|
line_with_word = lines[current_line]
|
|
if len(line_with_word) > 0:
|
|
line_with_word += " "
|
|
line_with_word += words[0]
|
|
if len(line_with_word) <= 16:
|
|
lines[current_line] = line_with_word
|
|
else:
|
|
current_line += 1
|
|
lines.append(words[0])
|
|
words = words[1:]
|
|
|
|
result = []
|
|
for line in lines:
|
|
if len(result) > 0:
|
|
result.append(0x01) # Newline
|
|
result.extend(line.encode())
|
|
return result
|
|
|
|
def define_text_constants(assembler: Z80Assembler, patch_data):
|
|
overworld_shops = [
|
|
"Lynna Shop",
|
|
"Hidden Shop",
|
|
"Syrup Shop",
|
|
"Advance Shop",
|
|
]
|
|
|
|
for shop_name in overworld_shops:
|
|
for i in range(1, 4):
|
|
location_name = f"{shop_name} #{i}"
|
|
symbolic_name = LOCATIONS_DATA[location_name]["symbolic_name"]
|
|
text_bytes = []
|
|
if location_name in patch_data["locations"]:
|
|
item_name_bytes = process_item_name_for_shop_text(patch_data["locations"][location_name])
|
|
text_bytes = [0x09, 0x01] + item_name_bytes + [0x09, 0x00, 0x0c, 0x18, 0x01] # Item name
|
|
if shop_name != "Tokay Market":
|
|
text_bytes.extend([0x20, 0x0c, 0x08, 0x20, 0x03, 0x7b, 0x01]) # Price
|
|
text_bytes.extend([0x02, 0x00, 0x00]) # OK / No thanks
|
|
assembler.add_floating_chunk(f"text.{symbolic_name}", text_bytes)
|
|
|
|
#Tokay Market
|
|
shop_name = "Tokay Market"
|
|
for i in range(1, 3):
|
|
location_name = f"{shop_name} #{i}"
|
|
symbolic_name = LOCATIONS_DATA[location_name]["symbolic_name"]
|
|
text_bytes = []
|
|
if location_name in patch_data["locations"]:
|
|
item_name_bytes = process_item_name_for_shop_text(patch_data["locations"][location_name])
|
|
text_bytes = [0x09, 0x01] + item_name_bytes + [0x09, 0x00, 0x0c, 0x18, 0x00] # Item name
|
|
assembler.add_floating_chunk(f"text.{symbolic_name}", text_bytes)
|
|
text_bytes = [0x31, 0x30, 0x20, 0x02, 0x12, 0x01, 0x02, 0x00, 0x00]
|
|
assembler.add_floating_chunk(f"text.tokayMarket1Validation", text_bytes)
|
|
|
|
|
|
def write_chest_contents(rom: RomData, patch_data):
|
|
"""
|
|
Chest locations are packed inside several big tables in the ROM, unlike other more specific locations.
|
|
This puts the item described in the patch data inside each chest in the game.
|
|
"""
|
|
for location_name, location_data in LOCATIONS_DATA.items():
|
|
if ('collect' not in location_data or 'room' not in location_data or location_data['collect'] != COLLECT_CHEST) and location_name != "Ridge Bush Cave":
|
|
continue
|
|
if location_name == "Nuun Highlands Cave":
|
|
chest_addr = rom.get_chest_addr(location_data['room'][patch_data["options"]["animal_companion"]])
|
|
else:
|
|
chest_addr = rom.get_chest_addr(location_data['room'])
|
|
item_name = patch_data["locations"][location_name]
|
|
item_id, item_subid = get_item_id_and_subid(item_name)
|
|
rom.write_byte(chest_addr, item_id)
|
|
rom.write_byte(chest_addr + 1, item_subid)
|
|
|
|
|
|
def define_compass_rooms_table(assembler: Z80Assembler, patch_data):
|
|
table = []
|
|
for location_name, item_name in patch_data["locations"].items():
|
|
_, item_subid = get_item_id_and_subid(item_name)
|
|
dungeon = 0xff
|
|
if item_name.startswith("Small Key") or item_name.startswith("Master Key"):
|
|
dungeon = item_subid
|
|
elif item_name.startswith("Boss Key"):
|
|
dungeon = item_subid + 1
|
|
|
|
if dungeon != 0xff:
|
|
location_data = LOCATIONS_DATA[location_name]
|
|
print(location_name)
|
|
rooms = location_data["room"]
|
|
if not isinstance(rooms, list):
|
|
rooms = [rooms]
|
|
for room in rooms:
|
|
room_id = room & 0xff
|
|
group_id = room >> 8
|
|
table.extend([group_id, room_id, dungeon])
|
|
table.append(0xff) # End of table
|
|
assembler.add_floating_chunk("compassRoomsTable", table)
|
|
|
|
|
|
def define_collect_properties_table(assembler: Z80Assembler, patch_data):
|
|
"""
|
|
Defines a table of (group, room, collect mode) entries for randomized items
|
|
to determine how they spawn, how they are grabbed and whether they set
|
|
a room flag when obtained.
|
|
"""
|
|
table = []
|
|
for location_name, item_name in patch_data["locations"].items():
|
|
location_data = LOCATIONS_DATA[location_name]
|
|
if "collect" not in location_data or "room" not in location_data:
|
|
continue
|
|
mode = location_data["collect"]
|
|
|
|
# Use no pickup animation for falling small keys
|
|
if mode == COLLECT_DROP and item_name.startswith("Small Key"):
|
|
mode &= 0xf8 # Set grab mode to TREASURE_GRAB_INSTANT
|
|
|
|
rooms = location_data["room"]
|
|
if not isinstance(rooms, list):
|
|
rooms = [rooms]
|
|
for room in rooms:
|
|
room_id = room & 0xff
|
|
group_id = room >> 8
|
|
table.extend([group_id, room_id, mode])
|
|
|
|
table.append(0xff)
|
|
assembler.add_floating_chunk("collectPropertiesTable", table)
|
|
|
|
|
|
def inject_slot_name(rom: RomData, slot_name: str):
|
|
slot_name_as_bytes = list(str.encode(slot_name))
|
|
slot_name_as_bytes += [0x00] * (0x40 - len(slot_name_as_bytes))
|
|
rom.write_bytes(0xfffc0, slot_name_as_bytes)
|
|
|
|
|
|
def write_seed_tree_content(rom: RomData, patch_data):
|
|
for _, tree_data in SEED_TREE_DATA.items():
|
|
original_data = rom.read_byte(tree_data["codeAdress"])
|
|
item_name = patch_data["locations"][tree_data["location"]]
|
|
item_id, _ = get_item_id_and_subid(item_name)
|
|
newdata = (original_data & 0x0f) | (item_id - 0x20) << 4
|
|
rom.write_bytes(tree_data["codeAdress"], [newdata])
|
|
|
|
def set_dungeon_warps(rom: RomData, patch_data):
|
|
warp_matchings = patch_data["dungeon_entrances"]
|
|
enter_values = {name: rom.read_word(dungeon["addr"]) for name, dungeon in DUNGEON_ENTRANCES.items()}
|
|
exit_values = {name: rom.read_word(addr) for name, addr in DUNGEON_EXITS.items()}
|
|
|
|
# Apply warp matchings expressed in the patch
|
|
for from_name, to_name in warp_matchings.items():
|
|
default_entrance_of_to_name = [name for name, dungeon in DUNGEON_ENTRANCES.items() if dungeon["default"] == to_name][0]
|
|
default_exit_of_from_name = DUNGEON_ENTRANCES[from_name]["default"]
|
|
entrance_addr = DUNGEON_ENTRANCES[from_name]["addr"]
|
|
exit_addr = DUNGEON_EXITS[to_name]
|
|
rom.write_word(entrance_addr, enter_values[default_entrance_of_to_name])
|
|
rom.write_word(exit_addr, exit_values[default_exit_of_from_name])
|
|
|
|
# Build a map dungeon => entrance (useful for essence warps)
|
|
entrance_map = dict((v, k) for k, v in warp_matchings.items())
|
|
|
|
# D1-D8 Essence Warps (hardcoded in one array using a unified format)
|
|
for i in range(8):
|
|
entrance_name = f"d{i + 1}"
|
|
if i == 5:
|
|
entrance_name += " past"
|
|
entrance = DUNGEON_ENTRANCES[entrance_map[entrance_name]]
|
|
rom.write_bytes(0x2874f + (i * 4), [
|
|
entrance["group"] | 0x80,
|
|
entrance["room"],
|
|
entrance["position"],
|
|
0x0e if entrance["shifted"] else 0x01
|
|
])
|
|
|
|
# # Change Minimap popups to indicate the randomized dungeon's name
|
|
# for i in range(8):
|
|
# entrance_name = f"d{i}"
|
|
# dungeon_index = int(warp_matchings[entrance_name][1:])
|
|
# map_tile = DUNGEON_ENTRANCES[entrance_name]["map_tile"]
|
|
# rom.write_byte(0x???? + map_tile, 0x81 | (dungeon_index << 3))
|
|
|
|
def define_dungeon_items_text_constants(assembler: Z80Assembler, patch_data):
|
|
|
|
for i in range(0, 10): # D0 has no map, no compass, no boss key, and the unique small key use the default text.
|
|
# " for\nDungeon X"
|
|
trueI = i if i != 9 else 6
|
|
dungeon_precision = [0x03, 0x39, 0x44, 0x05, 0xe6, 0x20, (0x30 + trueI)]
|
|
dungeon_tag = f"D{trueI}"
|
|
dungeon_precisionForBossKey = dungeon_precision.copy()
|
|
|
|
if i == 6:
|
|
#\n(present)
|
|
dungeon_precision.extend([0x01, 0x28, 0x03, 0x2e, 0x29])
|
|
dungeon_tag += "Present"
|
|
if i == 9:
|
|
#\n(past)
|
|
dungeon_precision.extend([0x01, 0x28, 0x70, 0x61, 0x73, 0x74, 0x29])
|
|
dungeon_tag += "Past"
|
|
|
|
# ###### Small keys ##############################################
|
|
# "You found a\n\color(RED)"
|
|
small_key_text = [0x02, 0x7c, 0x20, 0x61, 0x01, 0x09, 0x01]
|
|
if patch_data["options"]["master_keys"]:
|
|
# "Master Key"
|
|
small_key_text.extend([0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x03, 0x37])
|
|
else:
|
|
# "Small Key"
|
|
small_key_text.extend([0x53, 0x6d, 0x04, 0xd2, 0x4b, 0x65, 0x79])
|
|
if patch_data["options"]["keysanity_small_keys"]:
|
|
small_key_text.extend(dungeon_precision)
|
|
small_key_text.extend([0x09, 0x00, 0x21, 0x00]) # "\color(WHITE)!(end)"
|
|
assembler.add_floating_chunk(f"text.smallKey{dungeon_tag}", small_key_text)
|
|
|
|
# Hero's Cave only has Small Keys, so skip other texts
|
|
if i == 0:
|
|
continue
|
|
|
|
# ###### Boss keys ##############################################
|
|
# "You found the\n\color(RED)Boss Key"
|
|
if i < 9:
|
|
boss_key_text = [
|
|
0x02, 0x7c, 0x20, 0x05, 0xb4,
|
|
0x09, 0x01, 0x42, 0x6f, 0x73, 0x73, 0x20, 0x4b, 0x65, 0x79
|
|
]
|
|
if patch_data["options"]["keysanity_boss_keys"]:
|
|
boss_key_text.extend(dungeon_precisionForBossKey)
|
|
boss_key_text.extend([0x09, 0x00, 0x21, 0x00]) # "\color(WHITE)!(end)"
|
|
assembler.add_floating_chunk(f"text.bossKeyD{trueI}", boss_key_text)
|
|
|
|
# ###### Dungeon maps ##############################################
|
|
# "You found the\n\color(RED)"
|
|
dungeon_map_text = [0x02, 0x7c, 0x20, 0x05, 0xb4, 0x09, 0x01]
|
|
if patch_data["options"]["keysanity_maps_compasses"]:
|
|
dungeon_map_text.extend([0x4d, 0x61, 0x70]) # "Map"
|
|
dungeon_map_text.extend(dungeon_precision)
|
|
else:
|
|
dungeon_map_text.extend([0x44, 0x05, 0x8a, 0x20, 0x4d, 0x61, 0x70]) # "Dungeon Map"
|
|
dungeon_map_text.extend([0x09, 0x00, 0x21, 0x00]) # "\color(WHITE)!(end)"
|
|
assembler.add_floating_chunk(f"text.dungeonMap{dungeon_tag}", dungeon_map_text)
|
|
|
|
# ###### Compasses ##############################################
|
|
# "You found the\n\color(RED)Compass"
|
|
compasses_text = [
|
|
0x02, 0x7c, 0x20, 0x05, 0xb4, 0x09, 0x01,
|
|
0x09, 0x01, 0x43, 0x6f, 0x6d, 0x05, 0xfe
|
|
]
|
|
if patch_data["options"]["keysanity_maps_compasses"]:
|
|
compasses_text.extend(dungeon_precision)
|
|
compasses_text.extend([0x09, 0x00, 0x21, 0x00]) # "\color(WHITE)!(end)"
|
|
assembler.add_floating_chunk(f"text.compass{dungeon_tag}", compasses_text)
|
|
|
|
def set_file_select_text(assembler: Z80Assembler, slot_name: str):
|
|
def char_to_tile(c: str) -> int:
|
|
if '0' <= c <= '9':
|
|
return ord(c) - 0x20
|
|
if 'A' <= c <= 'Z':
|
|
return ord(c) + 0xa1
|
|
if c == '+':
|
|
return 0xfd
|
|
if c == '-':
|
|
return 0xfe
|
|
if c == '.':
|
|
return 0xff
|
|
else:
|
|
return 0xfc # All other chars are blank spaces
|
|
|
|
row_1 = [char_to_tile(c) for c in f"ARCHIP. {VERSION}"]
|
|
row_1_left_padding = int((16 - len(row_1)) / 2)
|
|
row_1_right_padding = int(16 - row_1_left_padding - len(row_1))
|
|
row_1 = ([0x00] * row_1_left_padding) + row_1 + ([0x00] * row_1_right_padding)
|
|
row_2 = [char_to_tile(c) for c in slot_name.replace("-", " ").upper()]
|
|
row_2_left_padding = int((16 - len(row_2)) / 2)
|
|
row_2_right_padding = int(16 - row_2_left_padding - len(row_2))
|
|
row_2 = ([0x00] * row_2_left_padding) + row_2 + ([0x00] * row_2_right_padding)
|
|
|
|
text_tiles = [0x74, 0x31]
|
|
text_tiles.extend(row_1)
|
|
text_tiles.extend([0x41, 0x40])
|
|
text_tiles.extend([0x02] * 12) # Offscreen tiles
|
|
|
|
text_tiles.extend([0x40, 0x41])
|
|
text_tiles.extend(row_2)
|
|
text_tiles.extend([0x51, 0x50])
|
|
text_tiles.extend([0x02] * 12) # Offscreen tiles
|
|
|
|
assembler.add_floating_chunk("dma_FileSelectStringTiles", text_tiles)
|
|
|
|
|
|
def set_heart_beep_interval_from_settings(rom: RomData):
|
|
heart_beep_interval = get_settings()["tloz_ooa_options"]["heart_beep_interval"]
|
|
if heart_beep_interval == "half":
|
|
rom.write_byte(0x914B, 0x3f * 2)
|
|
elif heart_beep_interval == "quarter":
|
|
rom.write_byte(0x914B, 0x3f * 4)
|
|
elif heart_beep_interval == "disabled":
|
|
rom.write_bytes(0x914B, [0x00, 0xc9]) # Put a return to avoid beeping entirely
|
|
|
|
def set_character_sprite_from_settings(rom: RomData):
|
|
sprite = get_settings()["tloz_ooa_options"]["character_sprite"]
|
|
sprite_dir = Path(Utils.local_path(os.path.join('data', 'sprites', 'oos_ooa')))
|
|
if sprite == "random":
|
|
sprite_filenames = [f for f in os.listdir(sprite_dir) if sprite_dir.joinpath(f).is_file() and f.endswith(".bin")]
|
|
sprite = sprite_filenames[random.randint(0, len(sprite_filenames) - 1)]
|
|
elif not sprite.endswith(".bin"):
|
|
sprite += ".bin"
|
|
if sprite != "link.bin":
|
|
sprite_path = sprite_dir.joinpath(sprite)
|
|
if not (sprite_path.exists() and sprite_path.is_file()):
|
|
raise ValueError(f"Path '{sprite_path}' doesn't exist")
|
|
sprite_bytes = list(Path(sprite_path).read_bytes())
|
|
rom.write_bytes(0x68000, sprite_bytes)
|
|
|
|
palette = get_settings()["tloz_ooa_options"]["character_palette"]
|
|
if palette == "random":
|
|
palette = random.choice(get_available_random_colors_from_sprite_name(sprite))
|
|
|
|
if palette == "green":
|
|
return # Nothing to change
|
|
if palette not in PALETTE_BYTES:
|
|
raise ValueError(f"Palette color '{palette}' doesn't exist (must be 'green', 'blue', 'red' or 'orange')")
|
|
palette_byte = PALETTE_BYTES[palette]
|
|
|
|
# Link in-game
|
|
for addr in range(0x1420e, 0x14221, 2):
|
|
rom.write_byte(addr, 0x08 | palette_byte)
|
|
# Link palette restored after Medusa Head / Ganon stun attacks
|
|
rom.write_byte(0x15271, 0x08 | palette_byte)
|
|
# Link standing still in file select (fileSelectDrawLink:@sprites0)
|
|
rom.write_byte(0x8d86, palette_byte)
|
|
rom.write_byte(0x8d8a, palette_byte)
|
|
# Link animated in file select (@sprites1 & @sprites2)
|
|
rom.write_byte(0x8d8f, palette_byte)
|
|
rom.write_byte(0x8d93, palette_byte)
|
|
rom.write_byte(0x8d98, 0x20 | palette_byte)
|
|
rom.write_byte(0x8d9c, 0x20 | palette_byte)
|
|
|
|
def apply_misc_option(rom: RomData, patch_data):
|
|
if patch_data["options"]["master_keys"] != OracleOfAgesMasterKeys.option_disabled:
|
|
# Remove small key consumption on keydoor opened
|
|
rom.write_byte(0x18366, 0x00)
|
|
# Change obtention text
|
|
rom.write_bytes(0x78247, [0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x20, 0x4b, 0x65, 0x79, 0x09, 0x01, 0x21, 0x00]) # I really wish that the dictionnay of ages would be more useful...
|
|
if patch_data["options"]["master_keys"] == OracleOfAgesMasterKeys.option_all_dungeon_keys:
|
|
# Remove boss key consumption on boss keydoor opened (boss door behave like normal locked door)
|
|
rom.write_word(0x1835e, 0x0000) |