import os import random from collections import defaultdict from pathlib import Path import Utils from settings import get_settings from .Constants import * from .Util import * from .asm import asm_files from ..Options import OracleOfSeasonsOldMenShuffle, OracleOfSeasonsGoal, OracleOfSeasonsAnimalCompanion, \ OracleOfSeasonsMasterKeys, OracleOfSeasonsFoolsOre, OracleOfSeasonsShowDungeonsWithEssence, OracleOfSeasonsLinkedHerosCave from ..World import OracleOfSeasonsWorld from ..common.patching.RomData import RomData from ..common.patching.Util import get_available_random_colors_from_sprite_name, simple_hex from ..common.patching.text import normalize_text from ..common.patching.z80asm.Assembler import Z80Assembler from ..common.patching.z80asm.Util import parse_hex_string_to_value from ..data.Constants import * from ..data.Locations import LOCATIONS_DATA from ..generation.Hints import make_hint_texts def define_foreign_item_data(assembler: Z80Assembler, texts: dict[str, str], patch_data: dict[str, Any]) -> dict[str, dict[str, Any]]: # Register all foreign items and save their text as TX_0cxx, id 0x41, subid xx item_data = ITEMS_DATA.copy() current_subid = 0 foreign_item_data = [] all_locations = patch_data["locations"] for location in all_locations: location_content = all_locations[location] if "player" not in location_content: continue texts[f"TX_0c{simple_hex(current_subid)}"] = normalize_text(f"You got a 🟥{location_content["item"]}⬜ for 🟦{location_content["player"]}⬜!") foreign_item_data.extend([ 0x00, # grab mode, doesn't really matter current_subid, # parameter, not sure it matters current_subid, # text id, will need special handling 0x52 if location_content["progression"] else 0x53 # sprite ]) item_data[f"{location_content["item"]}|{location_content["player"]}"] = { "id": 0x41, "subid": current_subid, } current_subid += 1 assembler.add_floating_chunk("archipelago_items", foreign_item_data) return item_data def get_asm_files(patch_data: dict[str, Any]) -> list[str]: files = list(asm_files["base"]) if patch_data["options"]["quick_flute"]: files += asm_files["quick_flute"] if patch_data["options"]["shuffle_old_men"] == OracleOfSeasonsOldMenShuffle.option_turn_into_locations: files += asm_files["old_men_as_locations"] if patch_data["options"]["remove_d2_alt_entrance"]: files += asm_files["remove_d2_alt_entrance"] if patch_data["dungeon_entrances"]["d3"] == "d0": files += asm_files["prevent_drowning_d0_warp"] elif patch_data["dungeon_entrances"]["d3"] == "d2": files += asm_files["prevent_drowning_d2_warp"] if patch_data["options"]["goal"] == OracleOfSeasonsGoal.option_beat_ganon: files += asm_files["ganon_goal"] if patch_data["options"]["rosa_quick_unlock"]: files += asm_files["instant_rosa"] if get_settings()["tloz_oos_options"]["remove_music"]: files += asm_files["mute_music"] if patch_data["options"]["cross_items"]: files += asm_files["cross_items"] if patch_data["options"]["secret_locations"]: files += asm_files["secret_locations"] if patch_data["options"]["linked_heros_cave"]: files += asm_files["d11"] if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.samasa: files += asm_files["d11_in_samasa"] elif patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.heros_cave: files += asm_files["d11_in_d0"] if patch_data["options"]["randomize_puzzles"]: files += asm_files["random_puzzles"] return files def write_chest_contents(rom: RomData, patch_data: dict[str, Any], item_data: dict[str, dict[str, Any]]) -> None: """ 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. """ locations_data = patch_data["locations"] for location_name, location_data in LOCATIONS_DATA.items(): if location_data.get("collect", COLLECT_TOUCH) != COLLECT_CHEST and not location_data.get("is_chest", False) or location_name not in locations_data: continue chest_addr = rom.get_chest_addr(location_data["room"], 0x15, 0x4f6c) item = locations_data[location_name] item_id, item_subid = get_item_id_and_subid(item_data, item) rom.write_byte(chest_addr, item_id) rom.write_byte(chest_addr + 1, item_subid) def define_samasa_combination(assembler: Z80Assembler, patch_data: dict[str, Any]) -> None: samasa_combination = [int(number) for number in patch_data["samasa_gate_sequence"].split(" ")] # 1) Define the combination itself and its length for the gate check assembler.add_floating_chunk("samasaCombination", samasa_combination) assembler.define_byte("samasaCombinationLengthMinusOne", len(samasa_combination) - 1) # 2) Build a cutscene for the Piratian to show the new sequence cutscene = [MOVE_UP, 0x15] # Add a fake last press on button 1 to make the pirate go back to its original position sequence = samasa_combination + [1] current_position = 1 for i, button_to_press in enumerate(sequence): # If current button is at a different position than the current one, # make the pirate move if button_to_press != current_position: if button_to_press < current_position: distance_to_move = 0x8 * (current_position - button_to_press) + 1 cutscene.extend([MOVE_LEFT, distance_to_move]) else: distance_to_move = 0x8 * (button_to_press - current_position) + 1 cutscene.extend([MOVE_RIGHT, distance_to_move]) current_position = button_to_press # Close the cupboard to mimic a button press on the gate by calling # the "closeOpenCupboard" subscript. Don't do it if it's the last movement # (which was only added to make the pirate go back to its initial position) if i < len(sequence) - 1: cutscene.extend([CALL_SCRIPT, 0x59, 0x5e]) # Add some termination to the script cutscene.extend([ MOVE_DOWN, 0x15, WRITE_OBJECT_BYTE, 0x7c, 0x00, DELAY_6, SHOW_TEXT_LOW_INDEX, 0x0d, ENABLE_ALL_OBJECTS, 0x5e, 0x4b # jump back to script start ]) if len(cutscene) > 0xFE: raise Exception("Samasa gate sequence is too long") assembler.add_floating_chunk("showSamasaCutscene", cutscene) def define_compass_rooms_table(assembler: Z80Assembler, patch_data: dict[str, Any], item_data: dict[str, dict[str, Any]]) -> None: table = [] for location_name, item in patch_data["locations"].items(): item_id, item_subid = get_item_id_and_subid(item_data, item) dungeon = 0xff if item_id == 0x30: # Small Key or Master Key dungeon = item_subid elif item_id == 0x31: # Boss Key dungeon = item_subid + 1 if dungeon != 0xff: location_data = LOCATIONS_DATA[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: dict[str, Any], item_data: dict[str, dict[str, Any]]) -> None: """ 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 in patch_data["locations"].items(): location_data = LOCATIONS_DATA[location_name] if "collect" not in location_data: continue mode = location_data["collect"] # Use no pickup animation for drop or diving small keys item_id, _ = get_item_id_and_subid(item_data, item) if item_id == 0x30 and (mode == COLLECT_DROP or mode == COLLECT_DIVE): 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]) # Specific case for D6 fake rupee table.extend([0x04, 0xc5, TREASURE_SPAWN_POOF | TREASURE_GRAB_INSTANT | TREASURE_SET_ITEM_ROOM_FLAG]) # Maku Tree gate opening cutscene table.extend([0x00, 0xd9, TREASURE_SPAWN_INSTANT | TREASURE_GRAB_SPIN_SLASH]) # End of d11 table.extend([0x05, 0x27, TREASURE_SPAWN_CHEST]) table.append(0xff) assembler.add_floating_chunk("collectPropertiesTable", table) def define_additional_tile_replacements(assembler: Z80Assembler, patch_data: dict[str, Any]) -> None: """ Define a list of entries following the format of `tileReplacementsTable` (see ASM for more info) which end up being tile replacements on various rooms in the game. """ table = [] # Remove Gasha spots when harvested once if deterministic Gasha locations are enabled if patch_data["options"]["deterministic_gasha_locations"] > 0: table.extend([ 0x00, 0xa6, 0x20, 0x54, 0xe1, # North Horon: Gasha Spot Above Impa 0x00, 0xc8, 0x20, 0x67, 0xe1, # Horon Village: Gasha Spot Near Mayor's House 0x00, 0xac, 0x20, 0x27, 0xe1, # Eastern Suburbs: Gasha Spot 0x00, 0x95, 0x20, 0x32, 0xe1, # Holodrum Plain: Gasha Spot Near Mrs. Ruul's House 0x00, 0x75, 0x20, 0x34, 0xe1, # Holodrum Plain: Gasha Spot on Island Above D1 0x00, 0x80, 0x20, 0x53, 0xe1, # Spool Swamp: Gasha Spot Near Floodgate Keyhole 0x00, 0xc0, 0x20, 0x61, 0xe1, # Spool Swamp: Gasha Spot Near Portal 0x00, 0x3f, 0x20, 0x44, 0xe1, # Sunken City: Gasha Spot 0x00, 0x1f, 0x20, 0x21, 0xe1, # Mt. Cucco: Gasha Spot 0x00, 0x38, 0x20, 0x25, 0xe1, # Goron Mountain: Gasha Spot Left of Entrance 0x00, 0x3b, 0x20, 0x53, 0xe1, # Goron Mountain: Gasha Spot Right of Entrance 0x00, 0x89, 0x20, 0x24, 0xe1, # Eyeglass Lake: Gasha Spot Near D5 0x00, 0x22, 0x20, 0x45, 0xe1, # Tarm Ruins: Gasha Spot 0x00, 0xf0, 0x20, 0x22, 0xe1, # Western Coast: Gasha Spot South of Graveyard 0x00, 0xef, 0x20, 0x66, 0xe1, # Samasa Desert: Gasha Spot 0x00, 0x44, 0x20, 0x44, 0xe1, # Path to Onox Castle: Gasha Spot ]) assembler.add_floating_chunk("additionalTileReplacements", table) def define_location_constants(assembler: Z80Assembler, patch_data: dict[str, Any], item_data: dict[str, dict[str, Any]]): # If "Enforce potion in shop" is enabled, put a Potion in a specific location in Horon Shop that was # disabled at generation time to prevent trackers from tracking it if patch_data["options"]["enforce_potion_in_shop"]: patch_data["locations"]["Horon Village: Shop #3"] = {"item": "Potion"} # If golden ore spots are not shuffled, they are still reachable nonetheless, so we need to enforce their # vanilla item for systems to work if not patch_data["options"]["shuffle_golden_ore_spots"]: for location_name in SUBROSIA_HIDDEN_DIGGING_SPOTS_LOCATIONS: patch_data["locations"][location_name] = {"item": "Ore Chunks (50)"} # Define shop prices as constants for symbolic_name, price in patch_data["shop_prices"].items(): assembler.define_byte(f"shopPrices.{symbolic_name}", RUPEE_VALUES[price]) 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 = patch_data["locations"][location_name] else: # Put a fake item for disabled locations, since they are unreachable anwyway item = {"item": "Friendship Ring"} item_id, item_subid = get_item_id_and_subid(item_data, item) 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) # Process deterministic Gasha Nut locations to define a table deterministic_gasha_table = [] for i in range(int(patch_data["options"]["deterministic_gasha_locations"])): item = patch_data["locations"][f"Gasha Nut #{i + 1}"] item_id, item_subid = get_item_id_and_subid(item_data, item) deterministic_gasha_table.extend([item_id, item_subid]) assembler.add_floating_chunk("deterministicGashaLootTable", deterministic_gasha_table) def define_option_constants(assembler: Z80Assembler, patch_data: dict[str, Any]) -> None: options = patch_data["options"] assembler.define_byte("option.startingGroup", 0x00) assembler.define_byte("option.startingRoom", 0xb6) assembler.define_byte("option.startingPosY", 0x58) assembler.define_byte("option.startingPosX", 0x58) assembler.define_byte("option.warpingGroup", 0x00) assembler.define_byte("option.warpingRoom", 0xb6) assembler.define_byte("option.warpingPosY", 0x58) assembler.define_byte("option.warpingPosX", 0x58) assembler.define_byte("option.warpingPos", 0x55) assembler.define_byte("option.warpingSeason", patch_data["default_seasons"]["EYEGLASS_LAKE"]) 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.goldenBeastsRequirement", options["golden_beasts_requirement"]) assembler.define_byte("option.treehouseOldManRequirement", options["treehouse_old_man_requirement"]) assembler.define_byte("option.tarmGateRequiredJewels", options["tarm_gate_required_jewels"]) assembler.define_byte("option.signGuyRequirement", options["sign_guy_requirement"]) assembler.define_byte("option.removeD0AltEntrance", options["remove_d0_alt_entrance"]) assembler.define_byte("option.deterministicGashaLootCount", options["deterministic_gasha_locations"]) fools_ore_damage = 3 if options["fools_ore"] == OracleOfSeasonsFoolsOre.option_balanced else 12 assembler.define_byte("option.foolsOreDamage", (-1 * fools_ore_damage + 0x100)) 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"] == OracleOfSeasonsMasterKeys.option_all_dungeon_keys assembler.define_byte("option.smallKeySprite", 0x43 if master_keys_as_boss_keys else 0x42) scrubs_all_refill = not patch_data["options"]["shuffle_business_scrubs"] assembler.define_byte("var.spoolSwampScrubSubid", 0x04 if scrubs_all_refill else 0x00) assembler.define_byte("var.samasaCaveScrubSubid", 0x04 if scrubs_all_refill else 0x01) assembler.define_byte("var.d2ScrubSubid", 0x04 if scrubs_all_refill else 0x02) assembler.define_byte("var.d4ScrubSubid", 0x04 if scrubs_all_refill else 0x03) # This adds water tiles in d3 inside as long as the floodgate isn't open if patch_data["dungeon_entrances"]["d3"] == "d0": assembler.define_byte("d3Entrance", 0x04) elif patch_data["dungeon_entrances"]["d3"] == "d2": assembler.define_byte("d3Entrance", 0x39) elif patch_data["dungeon_entrances"]["d3"] == "d11": assembler.define_byte("d11Drowning", 0x01) if patch_data["options"]["linked_heros_cave"]: assembler.define_byte("d11", 0x01) if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.samasa: assembler.define_byte("d11InSamasa", 0x01) chest_dict = { "d0": 0x75, "d1": 0x66, "d2": 0x5b, "d3": 0x43, "d4": 0x3b, "d5": 0x59, "d6": 0x23, "d7": 0x73, "d8": 0x35, "d11": 0x7b, } dungeon_entrances = patch_data["dungeon_entrances"] inverted_dungeon_entrances = {dungeon_entrances[key]: key for key in dungeon_entrances} dungeons_in_order_for_d11_puzzle = [] for i in range(1, 9): dungeons_in_order_for_d11_puzzle.append(chest_dict[inverted_dungeon_entrances[f"d{i}"]]) assembler.add_floating_chunk("dungeonsInOrderForD11Puzzle", list(dungeons_in_order_for_d11_puzzle)) dungeons_in_order_for_d11_puzzle.sort() assembler.add_floating_chunk("dungeonsForD11Puzzle", dungeons_in_order_for_d11_puzzle) if patch_data["options"]["show_dungeons_with_map"]: assembler.define_byte("showDungeonWithMap", 0x01) def define_season_constants(assembler: Z80Assembler, patch_data: dict[str, Any]) -> None: for region_name, season_byte in patch_data["default_seasons"].items(): assembler.define_byte(f"defaultSeason.{region_name}", season_byte) def define_lost_woods_sequences(assembler: Z80Assembler, texts: dict[str, str], patch_data: dict[str, Any]) -> None: pedestal_sequence = patch_data["lost_woods_item_sequence"] pedestal_bytes, pedestal_text = process_lost_woods_sequence(pedestal_sequence) assembler.add_floating_chunk("lostWoodsPedestalSequence", pedestal_bytes) travel_index = texts["TX_0b50"].index("If temperatures") texts["TX_0b50"] = texts["TX_0b50"][:travel_index] + pedestal_text main_sequence = patch_data["lost_woods_main_sequence"] main_bytes, main_text = process_lost_woods_sequence(main_sequence) assembler.add_floating_chunk("lostWoodsMainSequence", main_bytes) texts["TX_3604"] = "" # Unused travel_index = texts["TX_4500"].index("\ntravel west") texts["TX_4500"] = texts["TX_4500"][:travel_index] + "\\stop\n" + main_text def process_lost_woods_sequence(sequence: list[list[int]]) -> tuple[list[int], str]: """ Process a sequence of directions + seasons, and outputs two byte arrays: - one to use as a technical data array to check the sequence being done - one to use as text hint """ sequence_bytes: list[int] = [] text = "" for i in range(4): direction = sequence[i][0] season = sequence[i][1] sequence_bytes.extend(sequence[i]) text += DIRECTION_STRINGS[direction] text += SEASON_STRINGS[season] if i == 1: text += "\\stop" if i != 3: text += "\n" return sequence_bytes, text def define_tree_sprites(assembler: Z80Assembler, patch_data: dict[str, Any], item_data: dict[str, dict[str, Any]]) -> None: tree_data = { # Name: (map, position) "Horon Village: Seed Tree": (0xf8, 0x48), "Woods of Winter: Seed Tree": (0x9e, 0x88), "Holodrum Plain: Seed Tree": (0x67, 0x88), "Spool Swamp: Seed Tree": (0x72, 0x88), "Sunken City: Seed Tree": (0x5f, 0x86), "Tarm Ruins: Seed Tree": (0x10, 0x48), } i = 1 for tree_name in tree_data: seed = patch_data["locations"][tree_name] if seed["item"] == "Ember Seeds": continue seed_id, _ = get_item_id_and_subid(item_data, seed) assembler.define_byte(f"seedTree{i}.map", tree_data[tree_name][0]) assembler.define_byte(f"seedTree{i}.position", tree_data[tree_name][1]) assembler.define_byte(f"seedTree{i}.gfx", seed_id - 26) assembler.define(f"seedTree{i}.rectangle", f"treeRect{seed_id}") i += 1 if i == 5: # Duplicate ember, we have to blank some data assembler.define_byte("seedTree5.enabled", 0x0e) assembler.define_byte("seedTree5.map", 0xff) assembler.define_byte("seedTree5.position", 0) assembler.define_byte("seedTree5.gfx", 0) assembler.define_word("seedTree5.rectangle", 0) else: assembler.define_byte("seedTree5.enabled", 0x0d) def get_treasure_addr(rom: RomData, item_name: str, item_data: dict[str, dict[str, Any]]) -> int: item_id, item_subid = get_item_id_and_subid(item_data, {"item": item_name}) addr = 0x55129 + (item_id * 4) if rom.read_byte(addr) & 0x80 != 0: addr = 0x50000 + rom.read_word(addr + 1) return addr + (item_subid * 4) def set_treasure_data(rom: RomData, item_data: dict[str, dict[str, Any]], item_name: str, text_id: int | None, sprite_id: int | None = None, param_value: int | None = None) -> None: addr = get_treasure_addr(rom, item_name, item_data) 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 set_player_start_inventory(assembler: Z80Assembler, patch_data: dict[str, Any]) -> None: obtained_treasures_address = parse_hex_string_to_value(DEFINES["wObtainedTreasureFlags"]) start_inventory_changes = defaultdict(int) # ###### Base changes ############################################## start_inventory_changes[parse_hex_string_to_value(DEFINES["wIsLinkedGame"])] = 0x00 # No linked gaming start_inventory_changes[parse_hex_string_to_value(DEFINES["wAnimalTutorialFlags"])] = 0xff # Animal vars # Remove the requirement to go in the screen under Sunken City tree to make Dimitri bullies appear start_inventory_changes[parse_hex_string_to_value(DEFINES["wDimitriState"])] = 0x20 # Give L-3 ring box start_inventory_changes[0xc697] = 0x10 start_inventory_changes[parse_hex_string_to_value(DEFINES["wRingBoxLevel"])] = 0x03 # Starting map/compass if patch_data["options"]["starting_maps_compasses"]: dungeon_compass = parse_hex_string_to_value(DEFINES["wDungeonCompasses"]) for i in range(dungeon_compass, dungeon_compass + 4): start_inventory_changes[i] = 0xff start_inventory_data: dict[str, int] = patch_data["start_inventory"] # Handle leveled items if "Progressive Shield" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wShieldLevel"])] \ = start_inventory_data["Progressive Shield"] bombs = 0 if "Bombs (10)" in start_inventory_data: bombs += start_inventory_data["Bombs (10)"] * 0x10 if "Bombs (20)" in start_inventory_data: bombs += start_inventory_data["Bombs (20)"] * 0x20 if bombs > 0: start_inventory_changes[parse_hex_string_to_value(DEFINES["wCurrentBombs"])] \ = start_inventory_changes[parse_hex_string_to_value(DEFINES["wMaxBombs"])] \ = min(bombs, 0x99) # The bomb amounts are stored in decimal if "Progressive Sword" in start_inventory_data: start_inventory_changes[0xc6ac] = start_inventory_data["Progressive Sword"] if "Progressive Boomerang" in start_inventory_data: start_inventory_changes[0xc6b1] = start_inventory_data["Progressive Boomerang"] # Boomerang level if "Ricky's Flute" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wFluteIcon"])] = 0x01 # Flute icon start_inventory_changes[0xc643] |= 0x80 # Ricky State if "Dimitri's Flute" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wFluteIcon"])] = 0x02 # Flute icon start_inventory_changes[0xc644] |= 0x80 # Dimitri State if "Moosh's Flute" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wFluteIcon"])] = 0x03 # Flute icon start_inventory_changes[0xc645] |= 0x20 # Moosh State if "Progressive Feather" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wFeatherLevel"])] \ = start_inventory_data["Progressive Feather"] if "Switch Hook" in start_inventory_data: start_inventory_changes[parse_hex_string_to_value(DEFINES["wSwitchHookLevel"])] \ = start_inventory_data["Switch Hook"] bombchus = 0 if "Bombchus (10)" in start_inventory_data: bombchus += start_inventory_data["Bombchus (10)"] * 0x10 if "Bombchus (20)" in start_inventory_data: bombchus += start_inventory_data["Bombchus (20)"] * 0x20 if bombchus > 0: start_inventory_changes[parse_hex_string_to_value(DEFINES["wNumBombchus"])] \ = start_inventory_changes[parse_hex_string_to_value(DEFINES["wMaxBombchus"])] \ = min(bombchus, 0x99) # The bombchus amounts are stored in decimal seed_amount = 0 if "Progressive Slingshot" in start_inventory_data: start_inventory_changes[0xc6b3] = start_inventory_data["Progressive Slingshot"] # Slingshot level seed_amount = 0x20 if "Seed Shooter" in start_inventory_data: seed_amount = 0x20 if "Seed Satchel" in start_inventory_data: satchel_level = start_inventory_data["Seed Satchel"] start_inventory_changes[parse_hex_string_to_value(DEFINES["wSeedSatchelLevel"])] = satchel_level if satchel_level == 1: seed_amount = 0x20 elif satchel_level == 2: seed_amount = 0x50 else: seed_amount = 0x99 if seed_amount: start_inventory_data[SEED_ITEMS[patch_data["options"]["default_seed"]]] = 1 # Add seeds to the start inventory # Inventory obtained flags current_inventory_index = parse_hex_string_to_value(DEFINES["wInventoryB"]) for item in start_inventory_data: item_id = ITEMS_DATA[item]["id"] item_address = obtained_treasures_address + item_id // 8 item_mask = 0x01 << (item_id % 8) start_inventory_changes[item_address] |= item_mask if item_id < 0x20: # items prior to 0x20 are all usable if item == "Biggoron's Sword": # Biggoron needs special care since it occupies both hands if current_inventory_index == parse_hex_string_to_value(DEFINES["wInventoryB"]): start_inventory_changes[current_inventory_index] \ = start_inventory_changes[current_inventory_index + 1] \ = item_id current_inventory_index += 2 elif current_inventory_index == parse_hex_string_to_value(DEFINES["wInventoryB"]) + 1: current_inventory_index += 1 start_inventory_changes[current_inventory_index] = item_id current_inventory_index += 1 else: start_inventory_changes[current_inventory_index] = item_id # Place the item in the inventory current_inventory_index += 1 if item_id == 0x07: # Rod of Seasons season = ITEMS_DATA[item]["subid"] - 2 start_inventory_changes[0xc6b0] |= 0x01 << season elif item_id == 0x28: # Rupees amount = int(item.split("(")[1][:-1]) # Find the value in the item name start_inventory_changes[0xc6a5] += amount * start_inventory_data[item] elif item_id == 0x37: # Ore Chunks amount = int(item.split("(")[1][:-1]) # Find the value in the item name start_inventory_changes[0xc6a7] += amount * start_inventory_data[item] elif item_id == 0x30: # Small keys subid = ITEMS_DATA[item]["subid"] % 0x80 start_inventory_changes[0xc66e + subid] += start_inventory_data[item] elif item_id == 0x31: # Boss keys subid = ITEMS_DATA[item]["subid"] start_inventory_changes[0xc67a + subid // 8] |= 0x01 << subid % 8 elif item_id == 0x32: # Compasses subid = ITEMS_DATA[item]["subid"] start_inventory_changes[0xc67c + subid // 8] |= 0x01 << subid % 8 elif item_id == 0x33: # Maps subid = ITEMS_DATA[item]["subid"] start_inventory_changes[0xc67e + subid // 8] |= 0x01 << subid % 8 elif item_id == 0x2d: # Rings subid = ITEMS_DATA[item]["subid"] - 4 start_inventory_changes[parse_hex_string_to_value(DEFINES["wRingsObtained"]) + subid // 8] |= 0x01 << subid % 8 elif item_id == 0x40: # Essences subid = ITEMS_DATA[item]["subid"] start_inventory_changes[parse_hex_string_to_value(DEFINES["wEssencesObtained"])] |= 0x01 << subid % 8 elif 0x20 <= item_id <= 0x24: # Seeds seed_address = parse_hex_string_to_value(DEFINES["wNumEmberSeeds"]) + item_id - 0x20 start_inventory_changes[seed_address] = seed_amount if 0xc6a5 in start_inventory_changes: hex_rupee_count = parse_hex_string_to_value(f"${start_inventory_changes[0xc6a5]}") start_inventory_changes[0xc6a5] = hex_rupee_count % 0x100 start_inventory_changes[0xc6a6] = hex_rupee_count // 0x100 if 0xc6a7 in start_inventory_changes: hex_ore_count = parse_hex_string_to_value(f"${start_inventory_changes[0xc6a7]}") start_inventory_changes[0xc6a7] = hex_ore_count % 0x100 start_inventory_changes[0xc6a8] = hex_ore_count // 0x100 if obtained_treasures_address in start_inventory_changes: start_inventory_changes[obtained_treasures_address] |= 1 << 2 # Add treasure punch flag heart_pieces = (start_inventory_data.get("Piece of Heart", 0) + start_inventory_data.get("Rare Peach Stone", 0)) additional_hearts = (start_inventory_data.get("Heart Container", 0) + heart_pieces // 4) if additional_hearts: start_inventory_changes[0xc6a2] = start_inventory_changes[0xc6a3] = 12 + additional_hearts * 4 if heart_pieces % 4: start_inventory_changes[0xc6a4] = heart_pieces % 4 if "Gasha Seed" in start_inventory_data: start_inventory_changes[0xc6ba] = start_inventory_data["Gasha Seed"] # Make the list used in asm start_inventory = [] for address in start_inventory_changes: start_inventory.append(address // 0x100) start_inventory.append(address % 0x100) start_inventory.append(start_inventory_changes[address]) start_inventory.append(0x00) # End of the list assembler.add_floating_chunk("startingInventory", start_inventory) def alter_treasure_types(rom: RomData, item_data: dict[str, dict[str, Any]]) -> None: # Some treasures don't exist as interactions in base game, we need to add # text & sprite references for them to work properly in a randomized context set_treasure_data(rom, item_data, "Fool's Ore", 0x36, 0x4a) set_treasure_data(rom, item_data, "Rare Peach Stone", None, 0x3f) set_treasure_data(rom, item_data, "Ribbon", 0x41, 0x4c) set_treasure_data(rom, item_data, "Treasure Map", 0x6c, 0x49) set_treasure_data(rom, item_data, "Member's Card", 0x45, 0x48) set_treasure_data(rom, item_data, "Potion", 0x6d, 0x4b) # Make bombs increase max carriable quantity when obtained from treasures, # not drops (see asm/seasons/bomb_bag_behavior) set_treasure_data(rom, item_data, "Bombs (10)", None, None, 0x90) set_treasure_data(rom, item_data, "Bombs (20)", 0x94, None, 0xa0) set_treasure_data(rom, item_data, "Bombchus (10)", None, None, 0x90) set_treasure_data(rom, item_data, "Bombchus (20)", None, None, 0xa0) # Colored Rod of Seasons to make them recognizable set_treasure_data(rom, item_data, "Rod of Seasons (Spring)", None, 0x4f) set_treasure_data(rom, item_data, "Rod of Seasons (Autumn)", None, 0x50) set_treasure_data(rom, item_data, "Rod of Seasons (Winter)", None, 0x51) def set_old_men_rupee_values(rom: RomData, patch_data: dict[str, Any]) -> None: if patch_data["options"]["shuffle_old_men"] == OracleOfSeasonsOldMenShuffle.option_turn_into_locations: return for i, name in enumerate(OLD_MAN_RUPEE_VALUES.keys()): if name in patch_data["old_man_rupee_values"]: value = patch_data["old_man_rupee_values"][name] value_byte = RUPEE_VALUES[abs(value)] rom.write_byte(0x56233 + i, value_byte) if abs(value) == value: rom.write_word(0x2987b + (i * 2), 0x7472) # Give rupees else: rom.write_word(0x2987b + (i * 2), 0x7488) # Take rupees def apply_miscellaneous_options(rom: RomData, patch_data: dict[str, Any]) -> None: # If companion is Dimitri, allow calling him using the Flute inside Sunken City if patch_data["options"]["animal_companion"] == OracleOfSeasonsAnimalCompanion.option_dimitri: rom.write_byte(GameboyAddress(0x09, 0x4f39).address_in_rom(), 0xa7) rom.write_byte(GameboyAddress(0x09, 0x4f3b).address_in_rom(), 0xe7) # If horon shop 3 is set to be a renewable Potion, manually edit the shop flag for # that slot to zero to make it stay after buying if patch_data["options"]["enforce_potion_in_shop"]: rom.write_byte(GameboyAddress(0x08, 0x4cfb).address_in_rom(), 0x00) if patch_data["options"]["master_keys"] != OracleOfSeasonsMasterKeys.option_disabled: # Remove small key consumption on keydoor opened rom.write_byte(GameboyAddress(0x06, 0x4357).address_in_rom(), 0x00) if patch_data["options"]["master_keys"] == OracleOfSeasonsMasterKeys.option_all_dungeon_keys: # Remove boss key consumption on boss keydoor opened rom.write_word(GameboyAddress(0x06, 0x434f).address_in_rom(), 0x0000) rom.write_byte(GameboyAddress(0x0a, 0x46ed).address_in_rom(), patch_data["options"]["gasha_nut_kill_requirement"]) rom.write_byte(GameboyAddress(0x04, 0x6a31).address_in_rom(), patch_data["options"]["gasha_nut_kill_requirement"] // 2) def set_fixed_subrosia_seaside_location(rom: RomData, patch_data: dict[str, Any]) -> None: """ Make the location for Subrosia Seaside fixed among the 4 possible locations from the vanilla game. This is done to compensate for the poor in-game randomness and potential unfairness in races. """ spots_data = [rom.read_word(addr) for addr in range(0x222D3, 0x222DB, 0x02)] spot = spots_data[patch_data["subrosia_seaside_location"]] for addr in range(0x222D3, 0x222DB, 0x02): rom.write_word(addr, spot) def set_file_select_text(assembler: Z80Assembler, slot_name: str) -> None: 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"AP {OracleOfSeasonsWorld.version()}" .center(16, " ")] row_2 = [char_to_tile(c) for c in slot_name.replace("-", " ").upper().center(16, " ")] 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 process_item_name_for_shop_text(item: dict[str, str | bool]) -> str: if "player" in item: player_name = item["player"] if len(player_name) > 14: player_name = player_name[:13] + "." item_name = f"🟦{player_name}⬜'s 🟥" else: item_name = "🟥" item_name += item["item"] item_name = normalize_text(item_name) item_name += "⬜\\stop\n" return item_name def make_text_data(assembler: Z80Assembler, text: dict[str, str], patch_data: dict[str, Any]) -> None: # Process shops OVERWORLD_SHOPS = [ "Horon Village: Shop", "Horon Village: Member's Shop", "Sunken City: Syrup Shop", "Horon Village: Advance Shop" ] tx_indices = { "horonShop1": "TX_0e04", "horonShop2": "TX_0e03", "horonShop3": "TX_0e02", "memberShop1": "TX_0e1c", "memberShop2": "TX_0e1d", "memberShop3": "TX_0e1e", "syrupShop1": "TX_0d0a", "syrupShop2": "TX_0d01", "syrupShop3": "TX_0d05", "advanceShop1": "TX_0e22", "advanceShop2": "TX_0e23", "advanceShop3": "TX_0e25", "subrosianMarket1": "TX_2b00", "subrosianMarket2": "TX_2b01", "subrosianMarket3": "TX_2b05", "subrosianMarket4": "TX_2b06", "subrosianMarket5": "TX_2b10", "spoolSwampScrub": "TX_4509", "samasaCaveScrub": "TX_450b", "d4Scrub": "TX_450c", "d2Scrub": "TX_450d", } 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"] if location_name not in patch_data["locations"]: continue item_text = process_item_name_for_shop_text(patch_data["locations"][location_name]) item_text += (" \\num1 Rupees\n" " \\optOK \\optNo thanks\\cmd(0f)") text[tx_indices[symbolic_name]] = item_text for market_slot in range(1, 6): location_name = f"Subrosia: Market #{market_slot}" symbolic_name = LOCATIONS_DATA[location_name]["symbolic_name"] if location_name not in patch_data["locations"]: continue item_text = process_item_name_for_shop_text(patch_data["locations"][location_name]) if market_slot == 1: item_text += ("I'll trade for\n" "🟥Star-Shaped Ore⬜.\n" "\\jump(0b)") else: item_text += ("I'll trade for\n" "🟥\\num1 Ore Chunks⬜.\n" "\\jump(0b)") text[tx_indices[symbolic_name]] = item_text BUSINESS_SCRUBS = [ "Spool Swamp: Business Scrub", "Samasa Desert: Business Scrub", "Snake's Remains: Business Scrub", "Dancing Dragon Dungeon (1F): Business Scrub" ] for location_name in BUSINESS_SCRUBS: symbolic_name = LOCATIONS_DATA[location_name]["symbolic_name"] if location_name not in patch_data["locations"]: continue # Scrub string asking the player if they want to buy the item item_text = ("\\sfx(c6)Greetings!\n" + process_item_name_for_shop_text(patch_data["locations"][location_name]) + f"for 🟩{patch_data['shop_prices'][symbolic_name]} Rupees⬜\n" " \\optOK \\optNo thanks") text[tx_indices[symbolic_name]] = item_text # Cross items assembler.define_byte("text.hook1.treasure", 0x3b) assembler.define_byte("text.hook2.treasure", 0x51) assembler.define_byte("text.cane.treasure", 0x53) assembler.define_byte("text.shooter.treasure", 0x54) assembler.define_byte("text.hook1.inventory", 0x1e) assembler.define_byte("text.hook2.inventory", 0x17) assembler.define_byte("text.cane.inventory", 0x1d) assembler.define_byte("text.shooter.inventory", 0x2e) # Default satchel seed seed_name = SEED_ITEMS[patch_data["options"]["default_seed"]].replace(" ", "\n") text["TX_002d"] = text["TX_002d"].replace("Ember\nSeeds", seed_name) # Misc if patch_data["options"]["rosa_quick_unlock"]: text["TX_2904"] = ("Since you're so\n" "nice, I unlocked\n" "all the doors\n" "here for you.") text["TX_3e1b"] = ("You've broken\n🟩\\num1 signs⬜!\n" "You'd better not\n" "break more than\n" f"🟩{patch_data['options']['sign_guy_requirement']}⬜" ", or else...") # Inform the player of how many gashas are good wife_text_index = text["TX_3101"].index("The place") num_seeds = patch_data["options"]["deterministic_gasha_locations"] if num_seeds == 0: seed_text = ("nuts will not\n" "contain anything\n" "useful.") elif num_seeds == 16: seed_text = ("every nut can\n" "hold something\n" "useful.") else: seed_text = ("only your first\n" f"🟩{num_seeds}⬜ nuts can\n" "contain anything\n" "useful.") text["TX_3101"] = (text["TX_3101"][:wife_text_index] + "\\stop\n" "You should know\n" + seed_text) # Golden beasts golden_beasts_requirement = patch_data["options"]["golden_beasts_requirement"] if golden_beasts_requirement == 0: # Just a funny text for killing no golden beasts golden_beast_reward_text = text["TX_1f05"] post_congratulation_index = golden_beast_reward_text.index("Sir") text["TX_1f04"] = "" text["TX_1f05"] = ("You did nothing!\n" "Truly, " + golden_beast_reward_text[post_congratulation_index:]) elif golden_beasts_requirement < 4: number = ["one", "two", "three"][golden_beasts_requirement - 1] text["TX_1f04"] = text["TX_1f04"].replace("the four", number) text["TX_1f05"] = text["TX_1f05"].replace("all four", number) if golden_beasts_requirement == 1: text["TX_1f04"] = text["TX_1f04"].replace("beasts", "beast") text["TX_1f05"] = text["TX_1f05"].replace("beasts", "beast") # Maku tree sign essence_count = patch_data["options"]["required_essences"] text["TX_2e00"] = (f"Find 🟥{essence_count} essence{'s' if essence_count != 1 else ''}⬜\n" "to get the seed!") # Tarm ruins sign jewel_count = patch_data["options"]["tarm_gate_required_jewels"] text["TX_2e12"] = (f"Bring 🟩{jewel_count}⬜ jewel{'s' if jewel_count != 1 else ''}\n" "for the door\n" "to open.") # Tree house old man essence_count = patch_data["options"]["treehouse_old_man_requirement"] text["TX_3601"] = text["TX_3601"].replace("knows many\n🟥essences⬜...", f"has 🟥{essence_count} essence{'s' if essence_count != 1 else ''}⬜!") # With quick rosa, the escort code is disabled if patch_data["options"]["rosa_quick_unlock"]: text["TX_2906"] = normalize_text("Not me. Maybe ask someone else?") make_hint_texts(text, patch_data) def set_heart_beep_interval_from_settings(rom: RomData) -> None: heart_beep_interval = get_settings()["tloz_oos_options"]["heart_beep_interval"] if heart_beep_interval == "half": rom.write_byte(0x9116, 0x3f * 2) elif heart_beep_interval == "quarter": rom.write_byte(0x9116, 0x3f * 4) elif heart_beep_interval == "disabled": rom.write_bytes(0x9116, [0x00, 0xc9]) # Put a return to avoid beeping entirely def set_character_sprite_from_settings(rom: RomData) -> None: sprite = get_settings()["tloz_oos_options"]["character_sprite"] sprite_dir = Path(Utils.local_path(os.path.join("data", "sprites", "oos_ooa"))) if sprite == "random": sprite_weights = {f: 1 for f in os.listdir(sprite_dir) if sprite_dir.joinpath(f).is_file() and f.endswith(".bin")} elif isinstance(sprite, str): sprite_weights = {sprite: 1} else: sprite_weights = sprite weights = random.randrange(sum(sprite_weights.values())) for sprite, weight in sprite_weights.items(): weights -= weight if weights < 0: break palette_option = get_settings()["tloz_oos_options"]["character_palette"] if palette_option == "random": palette_weights = {palette: 1 for palette in get_available_random_colors_from_sprite_name(sprite)} elif isinstance(palette_option, str): palette_weights = {palette_option: 1} else: valid_palettes = get_available_random_colors_from_sprite_name(sprite) palette_weights = {} for palette, weight in palette_option.items(): splitted_palette = palette.split("|") if len(splitted_palette) == 2 and splitted_palette[1] != sprite: continue palette_name = splitted_palette[0] if palette_name == "random": for valid_palette in valid_palettes: palette_weights[valid_palette] = weight elif palette_name in valid_palettes: palette_weights[palette_name] = weight if len(palette_weights) == 0: palette_weights["green"] = 1 weights = random.randrange(sum(palette_weights.values())) for palette, weight in palette_weights.items(): weights -= weight if weights < 0: break if 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) # noinspection PyUnboundLocalVariable if palette == "green": return # Nothing to change # noinspection PyUnboundLocalVariable 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(0x141cc, 0x141df, 2): rom.write_byte(addr, 0x08 | palette_byte) # Link palette restored after Medusa Head / Ganon stun attacks rom.write_byte(0x1516d, 0x08 | palette_byte) # Link standing still in file select (fileSelectDrawLink:@sprites0) rom.write_byte(0x8d46, palette_byte) rom.write_byte(0x8d4a, palette_byte) # Link animated in file select (@sprites1 & @sprites2) rom.write_byte(0x8d4f, palette_byte) rom.write_byte(0x8d53, palette_byte) rom.write_byte(0x8d58, 0x20 | palette_byte) rom.write_byte(0x8d5c, 0x20 | palette_byte) def inject_slot_name(rom: RomData, slot_name: str) -> None: 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 set_dungeon_warps(rom: RomData, patch_data: dict[str, Any], dungeon_entrances: dict[str, Any], dungeon_exits: dict[str, Any]) -> None: 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(): entrance_addr = dungeon_entrances[from_name]["addr"] exit_addr = dungeon_exits[to_name] rom.write_word(entrance_addr, enter_values[to_name]) rom.write_word(exit_addr, exit_values[from_name]) # Build a map dungeon => entrance (useful for essence warps) entrance_map = dict((v, k) for k, v in warp_matchings.items()) # D0 Chest Warp (hardcoded warp using a specific format) d0_new_entrance = dungeon_entrances[entrance_map["d0"]] rom.write_bytes(0x2bbe4, [ d0_new_entrance["group"] | 0x80, d0_new_entrance["room"], 0x00, d0_new_entrance["position"] ]) # D1-D8 Essence Warps (hardcoded in one array using a unified format) for i in range(8): entrance = dungeon_entrances[entrance_map[f"d{i + 1}"]] rom.write_bytes(0x24b59 + (i * 4), [ entrance["group"] | 0x80, entrance["room"], entrance["position"] ]) # 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(0xaa19 + map_tile, 0x81 | (dungeon_index << 3)) # Dungeon 8 specific case (since it's in Subrosia) dungeon_index = int(warp_matchings["d8"][1:]) rom.write_byte(0xab19, 0x81 | (dungeon_index << 3)) if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.samasa: # Change Minimap popups entrance_name = "d11" dungeon_index = int(warp_matchings[entrance_name][1:]) map_tile = dungeon_entrances[entrance_name]["map_tile"] rom.write_byte(0xaa19 + map_tile, 0x81 | (dungeon_index << 3)) if patch_data["options"]["linked_heros_cave"] & OracleOfSeasonsLinkedHerosCave.heros_cave: rom.write_word(dungeon_exits["d11"], exit_values[entrance_map["d0"]]) def set_portal_warps(rom: RomData, patch_data: dict[str, Any]) -> None: warp_matchings = patch_data["subrosia_portals"] values = {} for portal_1, portal_2 in PORTAL_CONNECTIONS.items(): values[portal_1] = rom.read_word(PORTAL_WARPS[portal_2]["addr"]) values[portal_2] = rom.read_word(PORTAL_WARPS[portal_1]["addr"]) # Apply warp matchings expressed in the patch for name_1, name_2 in warp_matchings.items(): portal_1 = PORTAL_WARPS[name_1] portal_2 = PORTAL_WARPS[name_2] # Set warp destinations for both portals rom.write_word(portal_1["addr"], values[name_2]) rom.write_word(portal_2["addr"], values[name_1]) # Set portal text in map menu for both portals portal_text_addr = 0xab19 if portal_1["in_subrosia"] else 0xaa19 portal_text_addr += portal_1["map_tile"] rom.write_byte(portal_text_addr, 0x80 | (portal_2["text_index"] << 3)) portal_text_addr = 0xab19 if portal_2["in_subrosia"] else 0xaa19 portal_text_addr += portal_2["map_tile"] rom.write_byte(portal_text_addr, 0x80 | (portal_1["text_index"] << 3)) def define_dungeon_items_text_constants(texts: dict[str, str], patch_data: dict[str, Any]) -> None: base_id = 0x73 for i in range(10): if i == 0: dungeon_precision = " for\nHero's Cave" elif i == 9: dungeon_precision = " for\nLinked Hero's\nCave" else: dungeon_precision = f" for\nDungeon {i}" # ###### Small keys ############################################## small_key_text = "You found a\n🟥" if patch_data["options"]["master_keys"]: small_key_text += "Master Key" else: small_key_text += "Small Key" if patch_data["options"]["keysanity_small_keys"]: small_key_text += dungeon_precision small_key_text += "⬜!" texts[f"TX_00{simple_hex(base_id + i)}"] = small_key_text # Hero's Cave only has Small Keys, so skip other texts if i == 0 or i == 9: continue # ###### Boss keys ############################################## boss_key_text = "You found the\n🟥Boss Key" if patch_data["options"]["keysanity_boss_keys"]: boss_key_text += dungeon_precision boss_key_text += "⬜!" texts[f"TX_00{simple_hex(base_id + i + 9)}"] = boss_key_text # ###### Dungeon maps ############################################## dungeon_map_text = "You found the\n🟥" if patch_data["options"]["keysanity_maps_compasses"]: dungeon_map_text += "Map" dungeon_map_text += dungeon_precision else: dungeon_map_text += "Dungeon Map" dungeon_map_text += "⬜!" texts[f"TX_00{simple_hex(base_id + i + 17)}"] = dungeon_map_text # ###### Compasses ############################################## compasses_text = "You found the\n🟥Compass" if patch_data["options"]["keysanity_maps_compasses"]: compasses_text += dungeon_precision compasses_text += "⬜!" texts[f"TX_00{simple_hex(base_id + i + 25)}"] = compasses_text if patch_data["options"]["master_keys"]: texts["TX_001a"] = texts["TX_001a"].replace("Small", "Master") def define_essence_sparkle_constants(assembler: Z80Assembler, patch_data: dict[str, Any], dungeon_entrances: dict[str, Any]) -> None: byte_array = [] show_dungeons_with_essence = patch_data["options"]["show_dungeons_with_essence"] essence_pedestals = [k for k, v in LOCATIONS_DATA.items() if v.get("essence", False)] if show_dungeons_with_essence and not patch_data["options"]["shuffle_essences"]: for i, pedestal in enumerate(essence_pedestals): if patch_data["locations"][pedestal]["item"] not in ITEM_GROUPS["Essences"]: byte_array.extend([0xF0, 0x00]) # Nonexistent room, for padding continue # Find where dungeon entrance is located, and place the sparkle hint there dungeon = f"d{i + 1}" dungeon_entrance = [k for k, v in patch_data["dungeon_entrances"].items() if v == dungeon][0] entrance_data = dungeon_entrances[dungeon_entrance] byte_array.extend([entrance_data["group"], entrance_data["room"]]) assembler.add_floating_chunk("essenceLocationsTable", byte_array) require_compass = show_dungeons_with_essence == OracleOfSeasonsShowDungeonsWithEssence.option_with_compass assembler.define_byte("option.essenceSparklesRequireCompass", 1 if require_compass else 0) def set_faq_trap(assembler: Z80Assembler) -> None: assembler.define_byte("option.startingGroup", 0x04, True) assembler.define_byte("option.startingRoom", 0xec, True) assembler.define_byte("option.startingPosY", 0x50, True) assembler.define_byte("option.startingPosX", 0x78, True) def randomize_ai_for_april_fools(rom: RomData, seed: int): code_table = 0x2f16 # TODO : properly implement that ? enemy_table = [ # enemy id in (08, 2f), specially for the blade traps since they can't take the beamos AI, or they'd block d6 { 0: [ 0x08, # river zora 0x09, # octorok 0x0c, # arrow moblin 0x0d, # lynel 0x0f, 0x11, # Pokey is too unreliable and laggy 0x12, # gibdo 0x13, # spark 0x14, # spiked beetle 0x15, # bubble 0x18, # buzzblob 0x19, # whisp 0x1a, # crab 0x20, # masked moblin 0x22, 0x23, # pol's voice 0x25, # goponga flower 0x29, 0x2d, 0x2e, 0x2f ], 1: [ 0x0a, # boomerang moblin 0x1c, # iron mask 0x1e, # piranha 0x25, 0x2c, # cheep cheep, will probably break ], 2: [ 0x0b, # leever 0x17, # ghini 0x21, # arrow darknut ], 3: [ 0x1b, # spiny beetle 0x24, # like like, flagged as unkillable as the spawner, and is logically not required 0x2a, ], 4: [ 0x10, # rope ], 5: [ 0x0e, # blade trap 0x2b, ], }, # enemy id in (08, 2f) { 0: [ 0x08, # river zora 0x09, # octorok 0x0c, # arrow moblin 0x0d, # lynel 0x0f, 0x11, # Pokey is too unreliable and laggy 0x12, # gibdo 0x13, # spark 0x14, # spiked beetle 0x15, # bubble 0x16, # beamos 0x18, # buzzblob 0x19, # whisp 0x1a, # crab 0x20, # masked moblin 0x22, 0x23, # pol's voice 0x25, # goponga flower 0x29, 0x2d, 0x2e, 0x2f ], 1: [ 0x0a, # boomerang moblin 0x1c, # iron mask 0x1e, # piranha 0x25, 0x2c, # cheep cheep, will probably break ], 2: [ 0x0b, # leever 0x17, # ghini 0x21, # arrow darknut ], 3: [ 0x1b, # spiny beetle 0x24, # like like, flagged as unkillable as the spawner, and is logically not required 0x2a, ], 4: [ 0x10, # rope ], 5: [ 0x2b, ], }, # enemy id in (08, 2f), killable { 0: [ 0x09, # octorok 0x12, # gibdo 0x14, # spiked beetle 0x18, # buzzblob 0x1a, # crab 0x22, 0x23, # pol's voice 0x0d, # lynel 0x0c, # arrow moblin 0x20, # masked moblin ], 1: [ 0x0a, # boomerang moblin 0x1c, # iron mask 0x1e, # piranha 0x25, ], 2: [ 0x0b, # leever 0x17, # ghini 0x21, # arrow darknut ], 4: [ 0x10, # rope ], }, # enemy id in (30, 60) { 0: [ 0x30, 0x31, 0x33, 0x36, 0x37, 0x38, 0x3b, 0x3c, 0x3d, 0x3e, 0x43, 0x46, 0x48, 0x49, 0x4a, 0x4b, 0x4d, 0x4e, 0x5e, ], 1: [ 0x32, 0x34, 0x39, 0x41, 0x4c, 0x4f, ], 2: [ # 0x40, 0x52, ], 3: [ 0x51, 0x58, ], 4: [ 0x45, # pincer ] }, # enemy id in (30, 60), killable { 0: [ 0x30, 0x31, 0x3c, 0x3d, 0x3e, 0x43, 0x48, 0x49, 0x4a, 0x4b, 0x4d, 0x4e, ], 1: [ 0x32, 0x34, 0x39, 0x4c, 0x4f, ], 2: [ # 0x40 ], } ] r = random.Random(seed) ai_table = {} for bank in enemy_table: for cat in bank: enemies = bank[cat] if isinstance(cat, int) and cat != 0: ais = list(bank[cat]) for cat2 in bank: if isinstance(cat2, int): if cat2 == 0: ais.extend(bank[cat2]) elif cat2 >= cat: ais.extend(bank[cat2]) ais.extend(bank[cat2]) else: ais = list(bank[cat]) r.shuffle(ais) for i in range(len(enemies)): enemy = enemies[i] ai = ais.pop() ai_table[enemy] = ai ai_table[0x2f] = 0x2f # Thwomps have to stay vanilla for platforming for enemy in ai_table: ai = ai_table[enemy] rom.write_word(code_table + enemy * 2, rom.read_word(code_table + ai * 2)) blinkers = { 0x08, 0x0b, 0x10, 0x24, 0x34, 0x40, 0x41 } # Make some enemies hittable without having access to their AI if ai_table[0x08] not in blinkers: rom.write_byte(0xFDD92, 0x8F) # river zora if ai_table[0x0b] not in blinkers: rom.write_byte(0xFDD9E, 0x90) # leever if ai_table[0x10] not in blinkers: rom.write_byte(0xFDDB2, 0x90) # rope if ai_table[0x24] not in blinkers: rom.write_byte(0xFDE02, 0xA2) # like-like if ai_table[0x34] not in blinkers: rom.write_byte(0xFDE42, 0xAA) # zol # if ai_table[0x40] not in blinkers: # rom.write_byte(0xFDE72, 0xAF) # wizzrobes if ai_table[0x41] not in blinkers: rom.write_byte(0xFDE76, 0xB0) # crow if ai_table[0x14] != 0x14: rom.write_byte(0xFDDC2, 0xCE) # Make spiked beetles have the flipped collisions if ai_table[0x1C] != 0x1C: rom.write_byte(0xFDDE2, 0xD0) # Make iron mask have the unmasked collisions if ai_table[0x3e] != 0x3e: rom.write_byte(0xFDE6A, 0xAE) # Make peahats have vulnerable collisions if ai_table[0x24] != 0x24: # make like like deal low knockback instead of softlocking by grabbing him then never releasing him due to the lack of AI rom.write_byte(0x1EED0, 0x01)