diff --git a/worlds/tloz_oos/Client.py b/worlds/tloz_oos/Client.py index 9e4dfbd803da..314f028af021 100644 --- a/worlds/tloz_oos/Client.py +++ b/worlds/tloz_oos/Client.py @@ -5,7 +5,7 @@ import worlds._bizhawk as bizhawk from worlds._bizhawk.client import BizHawkClient from worlds.tloz_oos import LOCATIONS_DATA, ITEMS_DATA, OracleOfSeasonsGoal -from .Data import build_item_id_to_name_dict, build_location_name_to_id_dict +from .Util import build_item_id_to_name_dict, build_location_name_to_id_dict if TYPE_CHECKING: from worlds._bizhawk.context import BizHawkClientContext @@ -34,7 +34,7 @@ class OracleOfSeasonsClient(BizHawkClient): game = "The Legend of Zelda - Oracle of Seasons" system = "GBC" - patch_suffix = ".apseasons" + patch_suffix = ".apoos" local_checked_locations: Set[int] local_scouted_locations: Set[int] item_id_to_name: Dict[int, str] @@ -149,6 +149,14 @@ async def process_checked_locations(self, ctx: "BizHawkClientContext", flag_byte local_checked_locations.add(location_id) break + # Check how many deterministic Gasha Nuts have been opened, and mark their matching locations as checked + byte_offset = 0xC649 - RAM_ADDRS["location_flags"][0] + gasha_counter = flag_bytes[byte_offset] >> 2 + for i in range(gasha_counter): + name = f"Gasha Nut #{i + 1}" + location_id = self.location_name_to_id[name] + local_checked_locations.add(location_id) + # Send locations if self.local_checked_locations != local_checked_locations: self.local_checked_locations = local_checked_locations diff --git a/worlds/tloz_oos/Options.py b/worlds/tloz_oos/Options.py index 8431fcd0b221..0c8a2e496da7 100644 --- a/worlds/tloz_oos/Options.py +++ b/worlds/tloz_oos/Options.py @@ -66,23 +66,16 @@ class OracleOfSeasonsDefaultSeasons(Choice): default = 1 -class OracleOfSeasonsHoronSeason(Choice): +class OracleOfSeasonsHoronSeason(DefaultOnToggle): """ In the vanilla game, Horon Village default season is chaotic: every time you enter it, it sets a random season. This nullifies every condition where a season is required inside Horon Village, since you can leave and re-enter again and again until you get the season that suits you. - - Vanilla: season changes randomly everytime you enter Horon Village. This makes logic less interesting - and sometimes expects from you to leave and re-enter town a dozen times until you get the right season - - Normalized: Horon Village behaves like any other region in the game (it has a default season that can be changed - using Rod of Seasons) - Setting this option to "Normalized" makes it follow the global behavior defined in "Default Seasons" option + Enabling this option disables that behavior and makes Horon Village behave like any other region in the game. + This means it will have a default season picked at generation time that follows the global behavior defined + in the "Default Seasons" option. """ - display_name = "Horon Village Default Season" - - option_vanilla = 0 - option_normalized = 1 - - default = 1 + display_name = "Normalize Horon Village Season" class OracleOfSeasonsAnimalCompanion(Choice): @@ -113,8 +106,8 @@ class OracleOfSeasonsDefaultSeedType(Choice): option_ember = 0 option_scent = 1 option_pegasus = 2 - option_mystery = 3 - option_gale = 4 + option_gale = 3 + option_mystery = 4 default = 0 @@ -139,28 +132,26 @@ class OracleOfSeasonsDuplicateSeedTree(Choice): default = 5 -class OracleOfSeasonsDungeonShuffle(Choice): +class OracleOfSeasonsDungeonShuffle(Toggle): """ - - Vanilla: each dungeon entrance leads to its intended dungeon - - Shuffle: each dungeon entrance leads to a random dungeon picked at generation time + If enabled, each dungeon entrance will lead to a random dungeon picked at generation time. + Otherwise, all dungeon entrances lead to their dungeon as intended. """ display_name = "Shuffle Dungeons" - option_vanilla = 0 - option_shuffle = 1 - - default = 0 - class OracleOfSeasonsPortalShuffle(Choice): """ - Vanilla: pairs of portals are the same as in the original game - - Shuffle: each portal in Holodrum is connected to a random portal in Subrosia picked at generation time + - Shuffle Outwards: each portal is connected to a random portal in the opposite dimension picked at generation time + - Shuffle: each portal is connected to a random portal, which might be in the same dimension (with the guarantee of + having at least one portal going across dimensions) """ display_name = "Shuffle Subrosia Portals" option_vanilla = 0 - option_shuffle = 1 + option_shuffle_outwards = 1 + option_shuffle = 2 default = 0 @@ -187,23 +178,20 @@ class OracleOfSeasonsOldMenShuffle(Choice): default = 3 -class OracleOfSeasonsGoldenOreSpotsShuffle(Choice): +class OracleOfSeasonsGoldenOreSpotsShuffle(Toggle): """ - Subrosia contains 7 hidden digging spots containing 50 Ore Chunks, this option enables adding them to the pool - of locations and randomizing them like any other location (giving opportunity to find Ore Chunks randomized - somewhere else). - - Vanilla: Spots contain their golden ore chunk just like in the base game - - Shuffled Visible: Spots are randomized, and their tile is replaced with a recognizable "digging spot" tile (like - the one at the end of the hide-and-seek minigame). Pretty handy if that's your first time shuffling those. - - Shuffled Hidden: Spots are randomized but remain hidden as in the original game + This option adds the 7 hidden digging spots in Subrosia (containing 50 Ore Chunks each) to the pool + of randomized locations. """ display_name = "Shuffle Golden Ore Spots" - option_vanilla = 0 - option_shuffled_visible = 1 - option_shuffled_hidden = 2 - default = 0 +class OracleOfSeasonsEssenceSanity(Toggle): + """ + If enabled, essences will be shuffled anywhere in the multiworld instead of being guranteed to be found + at the end their respective dungeons. + """ + display_name = "Shuffle Essences" class OracleOfSeasonsMasterKeys(Choice): @@ -311,31 +299,32 @@ class OracleOfSeasonsSignGuyRequirement(Range): default = 10 -class OracleOfSeasonsLostWoodsItemSequence(Choice): +class OracleOfSeasonsLostWoodsItemSequence(DefaultOnToggle): """ - This option defines how the "secret sequence" (both directions and seasons) leading to the Noble Sword pedestal - is handled by the randomizer. - - Vanilla: the sequence is the same as in the original game - - Randomized: the sequence is randomized, and you need to use the Phonograph on the Deku Scrub to learn the sequence + If enabled, the secret sequence leading to the Noble Sword pedestal will be randomized (both directions to + take and seasons to use). + To know the randomized combination, you will need to bring the Phonograph to the Deku Scrub near the stump, just + like in the vanilla game. """ - display_name = "Lost Woods Item Sequence" + display_name = "Randomize Lost Woods Item Sequence" - option_vanilla = 0 - option_randomized = 1 - default = 1 +class OracleOfSeasonsLostWoodsMainSequence(Toggle): + """ + If enabled, the secret sequence leading to D6 sector will be randomized (both directions to take and + seasons to use). + To know the randomized combination, you will need to stun the Deku Scrub near the jewel gate using a shield, just + like in the vanilla game. + """ + display_name = "Randomize Lost Woods Main Sequence" -class OracleOfSeasonsSamasaGateCode(Choice): +class OracleOfSeasonsSamasaGateCode(Toggle): """ This option defines if the secret combination which opens the gate to Samasa Desert should be randomized. You can then configure the length of the sequence with the next option. """ - display_name = "Samasa Desert Gate Code" - - option_vanilla = 0 - option_randomized = 1 - default = 1 + display_name = "Randomize Samasa Desert Gate Code" class OracleOfSeasonsSamasaGateCodeLength(Range): @@ -350,18 +339,27 @@ class OracleOfSeasonsSamasaGateCodeLength(Range): default = 8 -class OracleOfSeasonsRingQuality(Choice): +class OracleOfSeasonsGashaLocations(Range): """ - Defines the quality of the rings that will be shuffled in your seed: - - Any: any ring can potentially be shuffled (including literally useless ones) - - Only Useful: only useful rings will be shuffled + When set to a non-zero value, planting a Gasha tree on a unique soil gives a deterministic item which is taken + into account by logic. Once an item has been obtained this way, the soil disappears forever to avoid any chance + of softlocking by wasting several Gasha Seeds on the same soil. + The value of this option is the number of items that can be obtained that way, the maximum value expecting you + to plant a tree on each one of the 16 Gasha spots in the game. """ - display_name = "Rings Quality" + display_name = "Deterministic Gasha Locations" + + range_start = 0 + range_end = 16 + default = 0 - option_any = 0 - option_only_useful = 1 - default = 1 +class OracleOfSeasonsRingQuality(DefaultOnToggle): + """ + If enabled, this option prevents useless rings from being shuffled in the item pool. + Both rings with no effect and rings providing maluses are considered useless. + """ + display_name = "Remove Useless Rings" class OracleOfSeasonsPricesFactor(Range): @@ -429,13 +427,13 @@ class OracleOfSeasonsCombatDifficulty(Choice): """ display_name = "Combat Difficulty" - option_peaceful = 0 - option_easier = 1 - option_vanilla = 2 - option_harder = 3 + option_peaceful = -4 + option_easier = -2 + option_vanilla = 0 + option_harder = 2 option_insane = 4 - default = 2 + default = 0 class OracleOfSeasonsQuickFlute(DefaultOnToggle): @@ -445,50 +443,13 @@ class OracleOfSeasonsQuickFlute(DefaultOnToggle): display_name = "Quick Flute" -class OracleOfSeasonsHeartBeepInterval(Choice): +class OracleOfSeasonsStartingMapsCompasses(Toggle): """ - - Default: play the beeping sound at the usual frequency when low on health - - Half: play the beeping sound two times less when low on health - - Quarter: play the beeping sound four times less when low on health - - Disabled: never play the beeping sound when low on health - """ - display_name = "Heart Beep Frequency" - - option_default = 0 - option_half = 1 - option_quarter = 2 - option_disabled = 3 - - default = 0 - - -class OracleOfSeasonsCharacterSprite(Choice): + When enabled, you will start the game with maps and compasses for every dungeon in the game. + This makes navigation easier and removes those items for the pool, which are replaced with random filler items. + Unlike 'start_inventory_from_pool', this is performed instanatly and silently when starting the game. """ - The sprite to use as a character during this seed. - (Sprites extracted from ardnaxelarak's rando) - """ - display_name = "Character Sprite" - - option_link = 0 - option_subrosian = 1 - option_goron = 2 - option_piratian = 3 - - default = 0 - - -class OracleOfSeasonsCharacterPalette(Choice): - """ - The color tint to apply to the character sprite during this seed - """ - display_name = "Character Tint" - - option_green = 0 - option_blue = 1 - option_red = 2 - option_orange = 3 - - default = 0 + display_name = "Start with Dungeon Maps & Compasses" @dataclass @@ -498,7 +459,7 @@ class OracleOfSeasonsOptions(PerGameCommonOptions): logic_difficulty: OracleOfSeasonsLogicDifficulty required_essences: OracleOfSeasonsRequiredEssences default_seasons: OracleOfSeasonsDefaultSeasons - horon_village_season: OracleOfSeasonsHoronSeason + normalize_horon_village_season: OracleOfSeasonsHoronSeason animal_companion: OracleOfSeasonsAnimalCompanion default_seed: OracleOfSeasonsDefaultSeedType duplicate_seed_tree: OracleOfSeasonsDuplicateSeedTree @@ -508,6 +469,7 @@ class OracleOfSeasonsOptions(PerGameCommonOptions): shuffle_portals: OracleOfSeasonsPortalShuffle shuffle_old_men: OracleOfSeasonsOldMenShuffle shuffle_golden_ore_spots: OracleOfSeasonsGoldenOreSpotsShuffle + shuffle_essences: OracleOfSeasonsEssenceSanity master_keys: OracleOfSeasonsMasterKeys keysanity_small_keys: OracleOfSeasonsSmallKeyShuffle keysanity_boss_keys: OracleOfSeasonsBossKeyShuffle @@ -516,10 +478,12 @@ class OracleOfSeasonsOptions(PerGameCommonOptions): tarm_gate_required_jewels: OraclesOfSeasonsTarmGateRequirement golden_beasts_requirement: OraclesOfSeasonsGoldenBeastsRequirement sign_guy_requirement: OracleOfSeasonsSignGuyRequirement - lost_woods_item_sequence: OracleOfSeasonsLostWoodsItemSequence - samasa_gate_code: OracleOfSeasonsSamasaGateCode + randomize_lost_woods_item_sequence: OracleOfSeasonsLostWoodsItemSequence + randomize_lost_woods_main_sequence: OracleOfSeasonsLostWoodsMainSequence + randomize_samasa_gate_code: OracleOfSeasonsSamasaGateCode samasa_gate_code_length: OracleOfSeasonsSamasaGateCodeLength - ring_quality: OracleOfSeasonsRingQuality + deterministic_gasha_locations: OracleOfSeasonsGashaLocations + remove_useless_rings: OracleOfSeasonsRingQuality shop_prices_factor: OracleOfSeasonsPricesFactor advance_shop: OracleOfSeasonsAdvanceShop fools_ore: OracleOfSeasonsFoolsOre @@ -527,7 +491,5 @@ class OracleOfSeasonsOptions(PerGameCommonOptions): enforce_potion_in_shop: OracleOfSeasonsEnforcePotionInShop combat_difficulty: OracleOfSeasonsCombatDifficulty quick_flute: OracleOfSeasonsQuickFlute - heart_beep_interval: OracleOfSeasonsHeartBeepInterval - character_sprite: OracleOfSeasonsCharacterSprite - character_palette: OracleOfSeasonsCharacterPalette + starting_maps_compasses: OracleOfSeasonsStartingMapsCompasses death_link: DeathLink diff --git a/worlds/tloz_oos/PatchWriter.py b/worlds/tloz_oos/PatchWriter.py new file mode 100644 index 000000000000..e57753258150 --- /dev/null +++ b/worlds/tloz_oos/PatchWriter.py @@ -0,0 +1,58 @@ +import yaml + +from typing import TYPE_CHECKING +from BaseClasses import ItemClassification +from worlds.tloz_oos.patching.ProcedurePatch import OoSProcedurePatch +from .data.Constants import * + +if TYPE_CHECKING: + from . import OracleOfSeasonsWorld + + +def oos_create_ap_procedure_patch(world: "OracleOfSeasonsWorld") -> OoSProcedurePatch: + patch = OoSProcedurePatch() + + patch.player = world.player + patch.player_name = world.multiworld.get_player_name(world.player) + + patch_data = { + "version": VERSION, + "options": world.options.as_dict(*[ + "advance_shop", "animal_companion", "combat_difficulty", "default_seed", + "enforce_potion_in_shop", "fools_ore", "goal", "golden_beasts_requirement", "master_keys", + "quick_flute", "remove_d0_alt_entrance", "remove_d2_alt_entrance", "required_essences", + "shuffle_golden_ore_spots", "shuffle_old_men", "sign_guy_requirement", "tarm_gate_required_jewels", + "treehouse_old_man_requirement", "warp_to_start", "starting_maps_compasses", + "keysanity_small_keys", "keysanity_boss_keys", "keysanity_maps_compasses", + "deterministic_gasha_locations", "shuffle_essences" + ]), + "samasa_gate_sequence": ' '.join([str(x) for x in world.samasa_gate_code]), + "lost_woods_item_sequence": world.lost_woods_item_sequence, + "lost_woods_main_sequence": world.lost_woods_main_sequence, + "default_seasons": world.default_seasons, + "old_man_rupee_values": world.old_man_rupee_values, + "dungeon_entrances": {a.replace(" entrance", ""): b.replace("enter ", "") + for a, b in world.dungeon_entrances.items()}, + "locations": {}, + "subrosia_portals": world.portal_connections, + "shop_prices": world.shop_prices, + "subrosia_seaside_location": world.random.randint(0, 3) + } + + for loc in world.multiworld.get_locations(world.player): + # Skip event locations which are not real in-game locations that need to be patched + if loc.address is None: + continue + if loc.item.player == loc.player: + patch_data["locations"][loc.name] = { + "item": loc.item.name + } + else: + patch_data["locations"][loc.name] = { + "item": loc.item.name, + "player": world.multiworld.get_player_name(loc.item.player), + "progression": loc.item.classification in [ItemClassification.progression, ItemClassification.progression_skip_balancing] + } + + patch.write_file("patch.dat", yaml.dump(patch_data).encode('utf-8')) + return patch diff --git a/worlds/tloz_oos/PatcherDataWriter.py b/worlds/tloz_oos/PatcherDataWriter.py deleted file mode 100644 index 0cb727ff2f1a..000000000000 --- a/worlds/tloz_oos/PatcherDataWriter.py +++ /dev/null @@ -1,84 +0,0 @@ -import os - -import yaml - -from BaseClasses import ItemClassification -from .data.Constants import * -from .data.Locations import LOCATIONS_DATA - - -def write_patcherdata_file(world, output_directory: str): - yamlObj = { - "settings": { - "game": "seasons", - "version": VERSION, - "goal": world.options.goal.current_key, - "companion": COMPANIONS[world.options.animal_companion.value], - "warp_to_start": world.options.warp_to_start.current_key, - "required_essences": world.options.required_essences.value, - "fools_ore_damage": 3 if world.options.fools_ore == "balanced" else 12, - "heart_beep_interval": world.options.heart_beep_interval.current_key, - "lost_woods_item_sequence": ' '.join(world.lost_woods_item_sequence), - "samasa_gate_sequence": ' '.join([str(x) for x in world.samasa_gate_code]), - "golden_beasts_requirement": world.options.golden_beasts_requirement.value, - "treehouse_old_man_requirement": world.options.treehouse_old_man_requirement.value, - "sign_guy_requirement": world.options.sign_guy_requirement.value, - "tarm_gate_required_jewels": world.options.tarm_gate_required_jewels.value, - "remove_d0_alt_entrance": world.options.remove_d0_alt_entrance.current_key, - "remove_d2_alt_entrance": world.options.remove_d2_alt_entrance.current_key, - "reveal_golden_ore_tiles": world.options.shuffle_golden_ore_spots == "shuffled_visible", - "master_keys": world.options.master_keys.current_key, - "quick_flute": world.options.quick_flute.current_key, - "renewable_horon_shop_3": world.options.enforce_potion_in_shop.current_key, - "open_advance_shop": world.options.advance_shop.current_key, - "character_sprite": world.options.character_sprite.current_key, - "character_palette": world.options.character_palette.current_key, - "turn_old_men_into_locations": world.options.shuffle_old_men == "turn_into_locations", - "received_damage_modifier": DAMAGE_MODIFIER_VALUES[world.options.combat_difficulty.current_key], - "slot_name": world.multiworld.get_player_name(world.player) - }, - "default seasons": {}, - "old man rupee values": {}, - "locations": {}, - "shop prices": world.shop_prices - } - - for region_name, season in world.default_seasons.items(): - yamlObj["default seasons"][REGIONS_CONVERSION_TABLE[region_name]] = season - if world.options.horon_village_season == "vanilla": - yamlObj["default seasons"][REGIONS_CONVERSION_TABLE["HORON_VILLAGE"]] = "chaotic" - - for region_name, value in world.old_man_rupee_values.items(): - yamlObj["old man rupee values"][region_name] = value - - yamlObj["dungeon entrances"] = {} - for entrance, dungeon in world.dungeon_entrances.items(): - yamlObj["dungeon entrances"][entrance] = dungeon.replace("enter ", "") - - yamlObj["subrosia portals"] = {} - for portal_holo, portal_sub in world.portal_connections.items(): - yamlObj["subrosia portals"][PORTALS_CONVERSION_TABLE[portal_holo]] = PORTALS_CONVERSION_TABLE[portal_sub] - - for loc in world.multiworld.get_locations(world.player): - if loc.address is None: - continue - if loc.item.player == loc.player: - item_name = loc.item.name - elif loc.item.classification in [ItemClassification.progression, ItemClassification.progression_skip_balancing]: - item_name = "Archipelago Progression Item" - else: - item_name = "Archipelago Item" - loc_patcher_name = find_patcher_name_for_location(loc.name) - if loc_patcher_name != "": - yamlObj["locations"][loc_patcher_name] = item_name - - filename = f"{world.multiworld.get_out_file_name_base(world.player)}.patcherdata" - with open(os.path.join(output_directory, filename), 'w') as f: - yaml.dump(yamlObj, f) - - -def find_patcher_name_for_location(pretty_name: str): - for loc_name, data in LOCATIONS_DATA.items(): - if loc_name == pretty_name: - return data["patcher_name"] if "patcher_name" in data else "" - raise Exception("Could not find patcher name for unknown location '" + pretty_name + "'") diff --git a/worlds/tloz_oos/Data.py b/worlds/tloz_oos/Util.py similarity index 96% rename from worlds/tloz_oos/Data.py rename to worlds/tloz_oos/Util.py index 191dbc8020b6..ea2a79721bf2 100644 --- a/worlds/tloz_oos/Data.py +++ b/worlds/tloz_oos/Util.py @@ -1,5 +1,4 @@ -from typing import Dict, List -from BaseClasses import Item +from typing import Dict from .data import BASE_LOCATION_ID, LOCATIONS_DATA, BASE_ITEM_ID, ITEMS_DATA diff --git a/worlds/tloz_oos/__init__.py b/worlds/tloz_oos/__init__.py index 7448a5d9bc07..209b31d07225 100644 --- a/worlds/tloz_oos/__init__.py +++ b/worlds/tloz_oos/__init__.py @@ -1,23 +1,65 @@ import os import logging - -import yaml - -from BaseClasses import Tutorial, Region, Location, LocationProgressType +from typing import List, Union, ClassVar +import settings +from BaseClasses import Tutorial, Region, Location, LocationProgressType, Item, ItemClassification from Fill import fill_restrictive, FillError from Options import Accessibility from worlds.AutoWorld import WebWorld, World -from .Data import * -from worlds.tloz_oos.data.Items import * -from .Logic import create_connections, apply_self_locking_rules + +from .Util import * from .Options import * -from .PatcherDataWriter import write_patcherdata_file +from .Logic import create_connections, apply_self_locking_rules +from .PatchWriter import oos_create_ap_procedure_patch from .data import LOCATIONS_DATA from .data.Constants import * +from .data.Items import ITEMS_DATA from .data.Regions import REGIONS + from .Client import OracleOfSeasonsClient # Unused, but required to register with BizHawkClient +class OracleOfSeasonsSettings(settings.Group): + class RomFile(settings.UserFilePath): + """File name of the Oracle of Seasons US ROM""" + copy_to = "Legend of Zelda, The - Oracle of Seasons (USA).gbc" + description = "OoS ROM File" + md5s = [ROM_HASH] + + class OoSCharacterSprite(str): + """ + The name of the sprite file to use (from "data/sprites/oos_ooa/"). + Putting "link" as a value uses the default game sprite. + Putting "random" as a value randomly picks a sprite from your sprites directory for each generated ROM. + """ + class OoSCharacterPalette(str): + """ + The color palette used for character sprite throughout the game. + Valid values are: "green", "red", "blue", "orange", and "random" + """ + class OoSRevealDiggingSpots(str): + """ + If enabled, hidden digging spots in Subrosia are revealed as diggable tiles. + """ + class OoSHeartBeepInterval(str): + """ + A factor applied to the infamous heart beep sound interval. + Valid values are: "vanilla", "half", "quarter", "disabled" + """ + class OoSRemoveMusic(str): + """ + If true, no music will be played in the game while sound effects remain untouched + """ + + rom_file: RomFile = RomFile(RomFile.copy_to) + rom_start: bool = True + character_sprite: Union[OoSCharacterSprite, str] = "link" + character_palette: Union[OoSCharacterPalette, str] = "green" + reveal_hidden_subrosia_digging_spots: Union[OoSRevealDiggingSpots, bool] = True + heart_beep_interval: Union[OoSHeartBeepInterval, str] = "vanilla" + remove_music: Union[OoSRemoveMusic, bool] = False + + class OracleOfSeasonsWeb(WebWorld): theme = "grass" setup_en = Tutorial( @@ -52,60 +94,55 @@ class OracleOfSeasonsWorld(World): required_client_version = (0, 4, 4) web = OracleOfSeasonsWeb() + settings: ClassVar[OracleOfSeasonsSettings] + settings_key = "tloz_oos_options" + location_name_to_id = build_location_name_to_id_dict() item_name_to_id = build_item_name_to_id_dict() item_name_groups = ITEM_GROUPS location_name_groups = LOCATION_GROUPS - pre_fill_items: List[Item] - dungeon_items: List[Item] - default_seasons: Dict[str, str] - dungeon_entrances: Dict[str, str] - portal_connections: Dict[str, str] - lost_woods_item_sequence: List[str] - old_man_rupee_values: Dict[str, int] - shop_prices: Dict[str, int] - samasa_gate_code: List[int] - def __init__(self, multiworld, player): super().__init__(multiworld, player) - self.pre_fill_items = [] - self.dungeon_items = [] - self.default_seasons = DEFAULT_SEASONS.copy() - self.dungeon_entrances = DUNGEON_ENTRANCES.copy() - self.portal_connections = PORTAL_CONNECTIONS.copy() - self.lost_woods_item_sequence = LOST_WOODS_ITEM_SEQUENCE.copy() - self.old_man_rupee_values = OLD_MAN_RUPEE_VALUES.copy() - self.samasa_gate_code = SAMASA_GATE_CODE.copy() - self.shop_prices = SHOP_PRICES_DIVIDERS.copy() + + self.pre_fill_items: List[Item] = [] + self.default_seasons: Dict[str, str] = DEFAULT_SEASONS.copy() + self.dungeon_entrances: Dict[str, str] = DUNGEON_CONNECTIONS.copy() + self.portal_connections: Dict[str, str] = PORTAL_CONNECTIONS.copy() + self.lost_woods_item_sequence: List[List] = LOST_WOODS_ITEM_SEQUENCE.copy() + self.lost_woods_main_sequence: List[List] = LOST_WOODS_MAIN_SEQUENCE.copy() + self.old_man_rupee_values: Dict[str, int] = OLD_MAN_RUPEE_VALUES.copy() + self.samasa_gate_code: List[int] = SAMASA_GATE_CODE.copy() + self.shop_prices: Dict[str, int] = SHOP_PRICES_DIVIDERS.copy() + self.random_rings_pool: List[str] = [] + self.remaining_progressive_gasha_seeds = 0 def fill_slot_data(self) -> dict: # Put options that are useful to the tracker inside slot data options = ["goal", "death_link", # Logic-impacting options - "logic_difficulty", "horon_village_season", "warp_to_start", - "shuffle_dungeons", "shuffle_portals", "lost_woods_item_sequence", + "logic_difficulty", "normalize_horon_village_season", "warp_to_start", + "shuffle_dungeons", "shuffle_portals", + "randomize_lost_woods_item_sequence", "randomize_lost_woods_main_sequence", "duplicate_seed_tree", "default_seed", "master_keys", "remove_d0_alt_entrance", "remove_d2_alt_entrance", # Locations - "shuffle_golden_ore_spots", "shuffle_old_men", "advance_shop", + "shuffle_golden_ore_spots", "shuffle_old_men", "advance_shop", "shuffle_essences", # Requirements "required_essences", "tarm_gate_required_jewels", "treehouse_old_man_requirement", "sign_guy_requirement", "golden_beasts_requirement", # Tracker QoL - "enforce_potion_in_shop", "keysanity_small_keys", "keysanity_boss_keys", + "enforce_potion_in_shop", "keysanity_small_keys", "keysanity_boss_keys", "starting_maps_compasses" ] slot_data = self.options.as_dict(*options) - slot_data["animal_companion"] = COMPANIONS[self.options.animal_companion.value] + slot_data["animal_companion"] = self.options.animal_companion.current_key.title() slot_data["default_seed"] = SEED_ITEMS[self.options.default_seed.value] slot_data["default_seasons_option"] = self.options.default_seasons.current_key slot_data["default_seasons"] = {} for region_name, season in self.default_seasons.items(): - slot_data["default_seasons"][REGIONS_CONVERSION_TABLE[region_name]] = season - if self.options.horon_village_season == "vanilla": - slot_data["default_seasons"][REGIONS_CONVERSION_TABLE["HORON_VILLAGE"]] = "chaotic" + slot_data["default_seasons"][region_name] = season slot_data["dungeon_entrances"] = self.dungeon_entrances slot_data["portal_connections"] = self.portal_connections @@ -113,29 +150,42 @@ def fill_slot_data(self) -> dict: return slot_data def generate_early(self): + self.remaining_progressive_gasha_seeds = self.options.deterministic_gasha_locations.value + self.restrict_non_local_items() self.randomize_default_seasons() self.randomize_old_men() - if self.options.shuffle_dungeons == "shuffle": + if self.options.shuffle_dungeons: self.shuffle_dungeons() - if self.options.shuffle_portals == "shuffle": + if self.options.shuffle_portals != "vanilla": self.shuffle_portals() - if self.options.lost_woods_item_sequence == "randomized": - # Pick 4 random seasons & directions (no direction can be "right", and last one has to be "left") - authorized_directions = [direction for direction in DIRECTIONS if direction != "right"] + if self.options.randomize_lost_woods_item_sequence: + # Pick 4 random seasons & directions (last one has to be "left") self.lost_woods_item_sequence = [] for i in range(4): - self.lost_woods_item_sequence.append(self.random.choice(SEASONS)) - self.lost_woods_item_sequence.append(self.random.choice(authorized_directions) if i < 3 else "left") + self.lost_woods_item_sequence.append([ + self.random.choice(DIRECTIONS) if i < 3 else DIRECTION_LEFT, + self.random.choice(SEASONS) + ]) + + if self.options.randomize_lost_woods_main_sequence: + # Pick 4 random seasons & directions (last one has to be "up") + self.lost_woods_main_sequence = [] + for i in range(4): + self.lost_woods_main_sequence.append([ + self.random.choice(DIRECTIONS) if i < 3 else DIRECTION_UP, + self.random.choice(SEASONS) + ]) - if self.options.samasa_gate_code == "randomized": + if self.options.randomize_samasa_gate_code: self.samasa_gate_code = [] for i in range(self.options.samasa_gate_code_length.value): self.samasa_gate_code.append(self.random.randint(0, 3)) self.randomize_shop_prices() + self.create_random_rings_pool() def restrict_non_local_items(self): # Restrict non_local_items option in cases where it's incompatible with other options that enforce items @@ -150,14 +200,19 @@ def restrict_non_local_items(self): def randomize_default_seasons(self): if self.options.default_seasons == "randomized": - for region in self.default_seasons: - self.default_seasons[region] = self.random.choice(SEASONS) + seasons_pool = SEASONS elif self.options.default_seasons.current_key.endswith("singularity"): single_season = self.options.default_seasons.current_key.replace("_singularity", "") if single_season == "random": single_season = self.random.choice(SEASONS) - for region in self.default_seasons: - self.default_seasons[region] = single_season + seasons_pool = [single_season] + else: + return + + for region in self.default_seasons: + if region == "HORON_VILLAGE" and not self.options.normalize_horon_village_season: + continue + self.default_seasons[region] = self.random.choice(seasons_pool) def shuffle_dungeons(self): shuffled_dungeons = list(self.dungeon_entrances.values()) @@ -176,7 +231,7 @@ def shuffle_dungeons(self): if d3_dungeon in forbidden_d3_dungeons: # Randomly pick a valid dungeon for D3 entrance, and make the entrance that was going to that dungeon # lead to the problematic dungeon instead - allowed_dungeons = [d for d in DUNGEON_ENTRANCES.values() if d not in forbidden_d3_dungeons] + allowed_dungeons = [d for d in DUNGEON_CONNECTIONS.values() if d not in forbidden_d3_dungeons] dungeon_to_swap = self.random.choice(allowed_dungeons) for k in self.dungeon_entrances.keys(): if self.dungeon_entrances[k] == dungeon_to_swap: @@ -185,9 +240,28 @@ def shuffle_dungeons(self): self.dungeon_entrances["d3 entrance"] = dungeon_to_swap def shuffle_portals(self): - shuffled_portals = list(self.portal_connections.values()) - self.random.shuffle(shuffled_portals) - self.portal_connections = dict(zip(self.portal_connections, shuffled_portals)) + holodrum_portals = list(PORTAL_CONNECTIONS.keys()) + subrosian_portals = list(PORTAL_CONNECTIONS.values()) + if self.options.shuffle_portals == "shuffle_outwards": + # Shuffle Outwards: connect Holodrum portals with random Subrosian portals + self.random.shuffle(subrosian_portals) + self.portal_connections = dict(zip(holodrum_portals, subrosian_portals)) + else: + # Shuffle: connect any portal with any other portal. To keep both dimensions available, we need to ensure + # that at least one Subrosian portal that is not D8 portal is connected to Holodrum + self.random.shuffle(holodrum_portals) + guaranteed_portal_holodrum = holodrum_portals.pop(0) + + self.random.shuffle(subrosian_portals) + if subrosian_portals[0] == "d8 entrance portal": + subrosian_portals[0], subrosian_portals[1] = subrosian_portals[1], subrosian_portals[0] + guaranteed_portal_subrosia = subrosian_portals.pop(0) + + shuffled_portals = holodrum_portals + subrosian_portals + self.random.shuffle(shuffled_portals) + it = iter(shuffled_portals) + self.portal_connections = dict(zip(it, it)) + self.portal_connections[guaranteed_portal_holodrum] = guaranteed_portal_subrosia # If accessibility is not locations, don't perform any check on what was randomly picked if self.options.accessibility != Accessibility.option_locations: @@ -195,12 +269,26 @@ def shuffle_portals(self): # If accessibility IS locations, we need to ensure that Temple Remains upper portal doesn't lead to the volcano # that can be triggered to open Temple Remains cave, since it would make it unreachable forever. - # In that case, just swap it with a random other portal. - if self.portal_connections["temple remains upper portal"] == "subrosia portal 6": - other_portals = [key for key in PORTAL_CONNECTIONS.keys() if key != "temple remains upper portal"] - portal_to_swap = self.random.choice(other_portals) - self.portal_connections["temple remains upper portal"] = self.portal_connections[portal_to_swap] - self.portal_connections[portal_to_swap] = "subrosia portal 6" + # Same goes with D8 <-> Volcanoes west portal in free shuffle mode. + # In that case, just redo the shuffle recursively until we end up with a satisfying shuffle. + if not self.is_volcanoes_west_portal_reachable(): + self.shuffle_portals() + + def are_portals_connected(self, portal_1, portal_2): + if portal_1 in self.portal_connections: + if self.portal_connections[portal_1] == portal_2: + return True + if portal_2 in self.portal_connections: + if self.portal_connections[portal_2] == portal_1: + return True + return False + + def is_volcanoes_west_portal_reachable(self): + if self.are_portals_connected("temple remains upper portal", "volcanoes west portal"): + return False + if self.are_portals_connected("d8 entrance portal", "volcanoes west portal"): + return False + return True def randomize_old_men(self): if self.options.shuffle_old_men == OracleOfSeasonsOldMenShuffle.option_shuffled_values: @@ -220,12 +308,25 @@ def randomize_shop_prices(self): self.random.shuffle(prices_pool) global_prices_factor = self.options.shop_prices_factor.value / 100.0 for key, divider in self.shop_prices.items(): - floating_price = prices_pool.pop() * global_prices_factor / divider + if key == "horonShop3" and self.options.enforce_potion_in_shop: + floating_price = 300 * global_prices_factor / divider + else: + floating_price = prices_pool.pop() * global_prices_factor / divider for i, value in enumerate(VALID_RUPEE_VALUES): if value > floating_price: self.shop_prices[key] = VALID_RUPEE_VALUES[i-1] break + def create_random_rings_pool(self): + # Get a subset of as many rings as needed, with a potential filter on quality depending on chosen options + ring_names = [name for name, idata in ITEMS_DATA.items() if "ring" in idata and idata["ring"] is True] + if self.options.remove_useless_rings: + forbidden_classes = [ItemClassification.filler, ItemClassification.trap] + ring_names = [name for name in ring_names if ITEMS_DATA[name]["classification"] not in forbidden_classes] + + self.random.shuffle(ring_names) + self.random_rings_pool = ring_names + def location_is_active(self, location_name, location_data): if "conditional" not in location_data or location_data["conditional"] is False: return True @@ -234,10 +335,13 @@ def location_is_active(self, location_name, location_data): if region_id == "advance shop": return self.options.advance_shop.value if region_id.startswith("subrosia") and region_id.endswith("digging spot"): - return self.options.shuffle_golden_ore_spots != "vanilla" + return self.options.shuffle_golden_ore_spots if location_name in RUPEE_OLD_MAN_LOCATIONS: return self.options.shuffle_old_men == OracleOfSeasonsOldMenShuffle.option_turn_into_locations - + if location_name == "Horon Village: Shop #3": + return not self.options.enforce_potion_in_shop + if location_name.startswith("Gasha Nut #"): + return int(location_name[11:]) <= self.options.deterministic_gasha_locations return False def create_location(self, region_name: str, location_name: str, local: bool): @@ -264,10 +368,6 @@ def create_regions(self): self.create_events() self.exclude_problematic_locations() - if self.options.enforce_potion_in_shop: - self.get_location("Horon Village: Shop #3").place_locked_item(self.create_item("Potion")) - self.shop_prices["horon shop 3"] = 300 - def create_event(self, region_name, event_item_name): region = self.multiworld.get_region(region_name, self.player) location = Location(self.player, region_name + ".event", None, region) @@ -304,7 +404,7 @@ def create_events(self): # Don't create an event for the triggerable volcano in Subrosia if portals layout make it unreachable, since # events are technically progression and generator doesn't like locked progression. At all. - if self.portal_connections["temple remains upper portal"] != "subrosia portal 6": + if self.is_volcanoes_west_portal_reachable(): self.create_event("bomb temple remains", "_triggered_volcano") self.create_event("golden darknut", "_beat_golden_darknut") @@ -317,6 +417,10 @@ def create_events(self): self.create_event("d7 entrance wild embers", "_wild_ember_seeds") self.create_event("frypolar room wild mystery", "_wild_mystery_seeds") + # Create events for reaching Gasha spots, used + for region_name in GASHA_SPOT_REGIONS: + self.create_event(region_name, f"_reached_{region_name}") + # Create event items to represent rupees obtained from Old Men, unless they are turned into locations if self.options.shuffle_old_men != OracleOfSeasonsOldMenShuffle.option_turn_into_locations: for region_name in self.old_man_rupee_values: @@ -337,7 +441,7 @@ def exclude_problematic_locations(self): # If Temple Remains upper portal is connected to triggerable volcano portal in Subrosia, this makes a check # in the bombable cave of Temple Remains unreachable forever. Exclude it in such conditions. - if self.portal_connections["temple remains upper portal"] == "subrosia portal 6": + if not self.is_volcanoes_west_portal_reachable(): locations_to_exclude.append("Temple Remains: Item in Cave Behind Rockslide") for name in locations_to_exclude: @@ -356,14 +460,23 @@ def create_item(self, name: str) -> Item: progression_items_in_hard_logic = ["Expert's Ring", "Fist Ring", "Swimmer's Ring"] if self.options.logic_difficulty == "hard" and name in progression_items_in_hard_logic: classification = ItemClassification.progression + # As many Gasha Seeds become progression as the number of deterministic Gasha Nuts + if self.remaining_progressive_gasha_seeds > 0 and name == "Gasha Seed": + self.remaining_progressive_gasha_seeds -= 1 + classification = ItemClassification.progression return Item(name, classification, ap_code, self.player) def build_item_pool_dict(self): item_pool_dict = {} + filler_item_count = 0 for loc_name, loc_data in LOCATIONS_DATA.items(): - if "randomized" in loc_data and loc_data["randomized"] is False: + if "essence" in loc_data and loc_data["essence"] is True and not self.options.shuffle_essences: item = self.create_item(loc_data['vanilla_item']) + # If essence location is excluded but we are not in essence-sanity, consider that essence as a filler + # item so logic doesn't expect the player to enter that dungeon + if loc_name in self.options.exclude_locations.value: + item.classification = ItemClassification.filler location = self.multiworld.get_location(loc_name, self.player) location.place_locked_item(item) continue @@ -376,23 +489,51 @@ def build_item_pool_dict(self): if "Ring" in item_name: item_name = "Random Ring" + if item_name == "Filler Item": + filler_item_count += 1 + continue + if self.options.master_keys != OracleOfSeasonsMasterKeys.option_disabled and "Small Key" in item_name: + # Small Keys don't exist if Master Keys are set to replace them + filler_item_count += 1 + continue + if self.options.master_keys == OracleOfSeasonsMasterKeys.option_all_dungeon_keys and "Boss Key" in item_name: + # Boss keys don't exist if Master Keys are set to replace them + filler_item_count += 1 + continue + if self.options.starting_maps_compasses and ("Compass" in item_name or "Dungeon Map" in item_name): + # Compasses and Dungeon Maps don't exist if player starts with them + filler_item_count += 1 + continue + item_pool_dict[item_name] = item_pool_dict.get(item_name, 0) + 1 + # If Master Keys are enabled, put one for every dungeon + if self.options.master_keys != OracleOfSeasonsMasterKeys.option_disabled: + for small_key_name in ITEM_GROUPS["Master Keys"]: + item_pool_dict[small_key_name] = 1 + filler_item_count -= 1 + + # Add as many filler items as required + for _ in range(filler_item_count): + random_filler_item = self.get_filler_item_name() + item_pool_dict[random_filler_item] = item_pool_dict.get(random_filler_item, 0) + 1 + # Perform adjustments on the item pool item_pool_adjustements = [ - ["Flute", COMPANIONS[self.options.animal_companion.value] + "'s Flute"], # Put a specific flute - ["Ricky's Gloves", "Progressive Sword"], # Ricky's gloves are useless in current logic - ["Gasha Seed", "Seed Satchel"], # Add a 3rd satchel that is usually obtained in linked games (99 seeds) - ["Gasha Seed", "Rupees (200)"], # Too many Gasha Seeds in vanilla pool, add more rupees and ore instead + ["Flute", self.options.animal_companion.current_key.title() + "'s Flute"], # Put a specific flute + ["Ricky's Gloves", "Progressive Sword"], # Ricky's gloves are useless in current logic + ["Treasure Map", "Ore Chunks (50)"], # Treasure Map would be non-functional in most cases, just remove it + ["Gasha Seed", "Seed Satchel"], # Add a 3rd satchel that is usually obtained in linked games (99 seeds) + ["Gasha Seed", "Rupees (200)"], # Too many Gasha Seeds in vanilla pool, add more rupees and ore instead ] - for i in range(4): + for _ in range(4): # Replace a few Gasha Seeds by random filler items item_pool_adjustements.append(["Gasha Seed", self.get_filler_item_name()]) fools_ore_item = "Fool's Ore" if self.options.fools_ore == OracleOfSeasonsFoolsOre.option_excluded: fools_ore_item = "Gasha Seed" - item_pool_adjustements.append(["Rod of Seasons", fools_ore_item]) # No lone rod of seasons supported for now + item_pool_adjustements.append(["Rod of Seasons", fools_ore_item]) for i, pair in enumerate(item_pool_adjustements): original_name = pair[0] @@ -400,58 +541,20 @@ def build_item_pool_dict(self): item_pool_dict[original_name] -= 1 item_pool_dict[replacement_name] = item_pool_dict.get(replacement_name, 0) + 1 - # If Master Keys replace Small Keys, remove all Small Keys but one for every dungeon - removed_keys = 0 - if self.options.master_keys != OracleOfSeasonsMasterKeys.option_disabled: - for small_key_name in ITEM_GROUPS["Small Keys"]: - removed_keys += item_pool_dict[small_key_name] - 1 - del item_pool_dict[small_key_name] - for small_key_name in ITEM_GROUPS["Master Keys"]: - item_pool_dict[small_key_name] = 1 - # If Master Keys replace Boss Keys, remove Boss Keys from item pool - if self.options.master_keys == OracleOfSeasonsMasterKeys.option_all_dungeon_keys: - for boss_key_name in ITEM_GROUPS["Boss Keys"]: - removed_keys += 1 - del item_pool_dict[boss_key_name] - for i in range(removed_keys): - random_filler_item = self.get_filler_item_name() - item_pool_dict[random_filler_item] = item_pool_dict.get(random_filler_item, 0) + 1 - - if self.options.enforce_potion_in_shop: - # Remove one Potion from the item pool if it was placed inside Horon Shop - item_pool_dict["Potion"] -= 1 + if "Random Ring" in item_pool_dict: + quantity = item_pool_dict["Random Ring"] + for _ in range(quantity): + ring_name = self.get_random_ring_name() + item_pool_dict[ring_name] = item_pool_dict.get(ring_name, 0) + 1 + del item_pool_dict["Random Ring"] return item_pool_dict def create_items(self): item_pool_dict = self.build_item_pool_dict() - - # Create items following the dictionary that was previously constructed - self.create_rings(item_pool_dict["Random Ring"]) - del item_pool_dict["Random Ring"] - for item_name, quantity in item_pool_dict.items(): - for i in range(quantity): - if ("Small Key" in item_name or "Master Key" in item_name) and not self.options.keysanity_small_keys: - self.dungeon_items.append(self.create_item(item_name)) - elif "Boss Key" in item_name and not self.options.keysanity_boss_keys: - self.dungeon_items.append(self.create_item(item_name)) - elif ("Compass" in item_name or "Dungeon Map" in item_name) and not self.options.keysanity_maps_compasses: - self.dungeon_items.append(self.create_item(item_name)) - else: - self.multiworld.itempool.append(self.create_item(item_name)) - - def create_rings(self, amount): - # Get a subset of as many rings as needed, with a potential filter on quality depending on chosen options - ring_names = [name for name, idata in ITEMS_DATA.items() if "ring" in idata and idata["ring"] is True] - if self.options.ring_quality == "only_useful": - forbidden_classes = [ItemClassification.filler, ItemClassification.trap] - ring_names = [name for name in ring_names if ITEMS_DATA[name]["classification"] not in forbidden_classes] - - self.random.shuffle(ring_names) - del ring_names[amount:] - for ring_name in ring_names: - self.multiworld.itempool.append(self.create_item(ring_name)) + for _ in range(quantity): + self.multiworld.itempool.append(self.create_item(item_name)) def get_pre_fill_items(self): return self.pre_fill_items @@ -460,6 +563,24 @@ def pre_fill(self) -> None: self.pre_fill_seeds() self.pre_fill_dungeon_items() + def filter_confined_dungeon_items_from_pool(self): + my_items = [item for item in self.multiworld.itempool if item.player == self.player] + confined_dungeon_items = [] + # Put Small Keys / Master Keys unless keysanity is enabled for those + if not self.options.keysanity_small_keys: + small_keys_name = "Small Key" + if self.options.master_keys != OracleOfSeasonsMasterKeys.option_disabled: + small_keys_name = "Master Key" + confined_dungeon_items.extend([item for item in my_items if item.name.startswith(small_keys_name)]) + # Put Boss Keys unless keysanity is enabled for those + if not self.options.keysanity_boss_keys: + confined_dungeon_items.extend([item for item in my_items if item.name.startswith("Boss Key")]) + # Put Maps & Compasses unless keysanity is enabled for those + if not self.options.keysanity_maps_compasses: + confined_dungeon_items.extend([item for item in my_items if item.name.startswith("Dungeon Map") + or item.name.startswith("Compass")]) + return confined_dungeon_items + def pre_fill_dungeon_items(self): # If keysanity is off, dungeon items can only be put inside local dungeon locations, and there are not so many # of those which makes them pretty crowded. @@ -467,7 +588,8 @@ def pre_fill_dungeon_items(self): # To circumvent this, we perform a restricted pre-fill here, placing only those dungeon items # before anything else. collection_state = self.multiworld.get_all_state(False) - + # Build a list of all dungeon items that will need to be placed in their own dungeon. + all_confined_dungeon_items = self.filter_confined_dungeon_items_from_pool() for i in range(0, 9): # Build a list of locations in this dungeon dungeon_location_names = [name for name, loc in LOCATIONS_DATA.items() @@ -475,12 +597,14 @@ def pre_fill_dungeon_items(self): dungeon_locations = [loc for loc in self.multiworld.get_locations(self.player) if loc.name in dungeon_location_names] - # Build a list of dungeon items that are "confined" (i.e. must be placed inside this dungeon) - # See `create_items` to see how `self.dungeon_items` is populated depending on current options. - confined_dungeon_items = [item for item in self.dungeon_items if item.name.endswith(f"({DUNGEON_NAMES[i]})")] + # From the list of all dungeon items that needs to be placed restrictively, only filter the ones for the + # dungeon we are currently processing. + confined_dungeon_items = [item for item in all_confined_dungeon_items + if item.name.endswith(f"({DUNGEON_NAMES[i]})")] if len(confined_dungeon_items) == 0: continue # This list might be empty with some keysanity options for item in confined_dungeon_items: + self.multiworld.itempool.remove(item) collection_state.remove(item) # Perform a prefill to place confined items inside locations of this dungeon @@ -501,7 +625,7 @@ def pre_fill_seeds(self) -> None: # - it needs to place a random seed on the "duplicate tree" (can be Horon's tree) # - it needs to place one of each seed on the 5 remaining trees # This has a few implications: - # - if Horon is the duplicate tree, this is the simplest case: we just place a random seed in Horon's tree + # - if Horon is the duplicate tree, this is the simplest case: we just place a starting seed in Horon's tree # and scatter the 5 seed types on the 5 other trees # - if Horon is NOT the duplicate tree, we need to remove Horon's seed from the pool of 5 seeds to scatter # and put a random seed inside the duplicate tree. Then, we place the 4 remaining seeds on the 4 remaining @@ -541,22 +665,36 @@ def place_seed(seed_name: str, location_name: str): def get_filler_item_name(self) -> str: FILLER_ITEM_NAMES = [ - "Rupees (1)", "Rupees (5)", "Rupees (10)", "Rupees (20)", "Rupees (30)", "Rupees (50)", - "Ore Chunks (50)", + "Rupees (1)", "Rupees (5)", "Rupees (5)", "Rupees (10)", "Rupees (10)", + "Rupees (20)", "Rupees (30)", + "Ore Chunks (50)", "Ore Chunks (25)", "Ore Chunks (10)", "Ore Chunks (10)", + "Random Ring", "Random Ring", "Gasha Seed", "Gasha Seed", "Gasha Seed", "Potion" ] - return self.random.choice(FILLER_ITEM_NAMES) + + item_name = self.random.choice(FILLER_ITEM_NAMES) + if item_name == "Random Ring": + return self.get_random_ring_name() + return item_name + + def get_random_ring_name(self): + if len(self.random_rings_pool) > 0: + return self.random_rings_pool.pop() + return "Rupees (1)" def generate_output(self, output_directory: str): - write_patcherdata_file(self, output_directory) + patch = oos_create_ap_procedure_patch(self) + rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" + f"{patch.patch_file_ending}") + patch.write(rom_path) def write_spoiler(self, spoiler_handle): spoiler_handle.write(f"\n\nDefault Seasons ({self.multiworld.player_name[self.player]}):\n") for region_name, season in self.default_seasons.items(): - spoiler_handle.write(f"\t- {REGIONS_CONVERSION_TABLE[region_name]} --> {season}\n") + spoiler_handle.write(f"\t- {region_name} --> {season}\n") - if self.options.shuffle_dungeons != "vanilla": + if self.options.shuffle_dungeons: spoiler_handle.write(f"\nDungeon Entrances ({self.multiworld.player_name[self.player]}):\n") for entrance, dungeon in self.dungeon_entrances.items(): spoiler_handle.write(f"\t- {entrance} --> {dungeon.replace('enter ', '')}\n") @@ -564,4 +702,4 @@ def write_spoiler(self, spoiler_handle): if self.options.shuffle_portals != "vanilla": spoiler_handle.write(f"\nSubrosia Portals ({self.multiworld.player_name[self.player]}):\n") for portal_holo, portal_sub in self.portal_connections.items(): - spoiler_handle.write(f"\t- {PORTALS_CONVERSION_TABLE[portal_holo]} --> {PORTALS_CONVERSION_TABLE[portal_sub]}\n") + spoiler_handle.write(f"\t- {portal_holo} --> {portal_sub}\n") diff --git a/worlds/tloz_oos/data/Constants.py b/worlds/tloz_oos/data/Constants.py index 8150d38f4875..c4433f33dbef 100644 --- a/worlds/tloz_oos/data/Constants.py +++ b/worlds/tloz_oos/data/Constants.py @@ -1,30 +1,34 @@ -VERSION = "6.2" +VERSION = "7.0" +ROM_HASH = "f2dc6c4e093e4f8c6cbea80e8dbd62cb" -COMPANIONS = [ - "Ricky", - "Dimitri", - "Moosh" +DIRECTION_UP = 0 +DIRECTION_RIGHT = 1 +DIRECTION_DOWN = 2 +DIRECTION_LEFT = 3 +DIRECTIONS = [ + DIRECTION_UP, + DIRECTION_RIGHT, + DIRECTION_DOWN, + DIRECTION_LEFT ] +SEASON_SPRING = 0x00 +SEASON_SUMMER = 0x01 +SEASON_AUTUMN = 0x02 +SEASON_WINTER = 0x03 +SEASON_CHAOTIC = 0xFF SEASONS = [ - "spring", - "summer", - "autumn", - "winter" -] - -DIRECTIONS = [ - "up", - "right", - "down", - "left" + SEASON_SPRING, + SEASON_SUMMER, + SEASON_AUTUMN, + SEASON_WINTER ] SEASON_ITEMS = { - "winter": "Rod of Seasons (Winter)", - "summer": "Rod of Seasons (Summer)", - "spring": "Rod of Seasons (Spring)", - "autumn": "Rod of Seasons (Autumn)", + SEASON_WINTER: "Rod of Seasons (Winter)", + SEASON_SUMMER: "Rod of Seasons (Summer)", + SEASON_SPRING: "Rod of Seasons (Spring)", + SEASON_AUTUMN: "Rod of Seasons (Autumn)", } SEED_ITEMS = [ @@ -47,38 +51,6 @@ "Sword & Shield Dungeon" ] -REGIONS_CONVERSION_TABLE = { - "EYEGLASS_LAKE": "north horon", - "NORTH_HORON": "holodrum plain", - "EASTERN_SUBURBS": "eastern suburbs", - "WOODS_OF_WINTER": "woods of winter", - "SUNKEN_CITY": "sunken city", - "WESTERN_COAST": "western coast", - "SPOOL_SWAMP": "spool swamp", - "TEMPLE_REMAINS": "temple remains", - "LOST_WOODS": "lost woods", - "TARM_RUINS": "tarm ruins", - "HORON_VILLAGE": "horon village" -} - -PORTALS_CONVERSION_TABLE = { - "eastern suburbs portal": "eastern suburbs", - "eyeglass lake portal": "eyeglass lake", - "horon village portal": "horon village", - "mt. cucco portal": "mt. cucco", - "spool swamp portal": "spool swamp", - "temple remains lower portal": "temple remains lower", - "temple remains upper portal": "temple remains upper", - - "subrosia portal 1": "volcanoes east", - "subrosia portal 2": "subrosia market", - "subrosia portal 3": "strange brothers", - "subrosia portal 4": "house of pirates", - "subrosia portal 5": "great furnace", - "subrosia portal 6": "volcanoes west", - "subrosia portal 7": "d8 entrance", -} - ESSENCES = [ "Fertile Soil", "Gift of Time", @@ -101,29 +73,21 @@ 0, 1, 2, 5, 10, 20, 25, 30, 40, 50, 60, 70, 80, 100, 200, 300, 400, 500, 900, 999 ] -DAMAGE_MODIFIER_VALUES = { - "peaceful": -4, - "easier": -2, - "vanilla": 0, - "harder": 2, - "insane": 4, -} - DEFAULT_SEASONS = { - "EYEGLASS_LAKE": "winter", - "NORTH_HORON": "spring", - "EASTERN_SUBURBS": "autumn", - "WOODS_OF_WINTER": "summer", - "SUNKEN_CITY": "summer", - "WESTERN_COAST": "winter", - "SPOOL_SWAMP": "autumn", - "TEMPLE_REMAINS": "winter", - "LOST_WOODS": "autumn", - "TARM_RUINS": "spring", - "HORON_VILLAGE": "spring" + "EYEGLASS_LAKE": SEASON_WINTER, + "HOLODRUM_PLAIN": SEASON_SPRING, + "EASTERN_SUBURBS": SEASON_AUTUMN, + "WOODS_OF_WINTER": SEASON_SUMMER, + "SUNKEN_CITY": SEASON_SUMMER, + "WESTERN_COAST": SEASON_WINTER, + "SPOOL_SWAMP": SEASON_AUTUMN, + "TEMPLE_REMAINS": SEASON_WINTER, + "LOST_WOODS": SEASON_AUTUMN, + "TARM_RUINS": SEASON_SPRING, + "HORON_VILLAGE": SEASON_CHAOTIC } -DUNGEON_ENTRANCES = { +DUNGEON_CONNECTIONS = { "d0 entrance": "enter d0", "d1 entrance": "enter d1", "d2 entrance": "enter d2", @@ -136,31 +100,39 @@ } PORTAL_CONNECTIONS = { - "eastern suburbs portal": "subrosia portal 1", - "spool swamp portal": "subrosia portal 2", - "mt. cucco portal": "subrosia portal 3", - "horon village portal": "subrosia portal 4", - "eyeglass lake portal": "subrosia portal 5", - "temple remains lower portal": "subrosia portal 6", - "temple remains upper portal": "subrosia portal 7", + "eastern suburbs portal": "volcanoes east portal", + "spool swamp portal": "subrosia market portal", + "mt. cucco portal": "strange brothers portal", + "horon village portal": "house of pirates portal", + "eyeglass lake portal": "great furnace portal", + "temple remains lower portal": "volcanoes west portal", + "temple remains upper portal": "d8 entrance portal", } LOST_WOODS_ITEM_SEQUENCE = [ - "winter", "left", - "autumn", "left", - "spring", "left", - "summer", "left" + [DIRECTION_LEFT, SEASON_WINTER], + [DIRECTION_LEFT, SEASON_AUTUMN], + [DIRECTION_LEFT, SEASON_SPRING], + [DIRECTION_LEFT, SEASON_SUMMER], +] + +LOST_WOODS_MAIN_SEQUENCE = [ + [DIRECTION_LEFT, SEASON_WINTER], + [DIRECTION_DOWN, SEASON_AUTUMN], + [DIRECTION_RIGHT, SEASON_SPRING], + [DIRECTION_UP, SEASON_SUMMER], ] +# The order of keys in this dictionary matters, since it's the same as the one used inside the ROM OLD_MAN_RUPEE_VALUES = { - "old man in horon": 100, - "old man near d1": 100, - "old man near blaino": 200, "old man in goron mountain": 300, + "old man near blaino": 200, + "old man near d1": 100, "old man near western coast house": 300, + "old man in horon": 100, + "old man near d6": -200, "old man near holly's house": -50, - "old man near mrs. ruul": -100, - "old man near d6": -200 + "old man near mrs. ruul": -100 } RUPEE_OLD_MAN_LOCATIONS = [ @@ -177,22 +149,22 @@ SAMASA_GATE_CODE = [2, 2, 1, 0, 0, 3, 3, 3] SHOP_PRICES_DIVIDERS = { - "horon shop 1": 1, - "horon shop 2": 1, - "horon shop 3": 1, - "member shop 1": 1, - "member shop 2": 1, - "member shop 3": 1, - "advance shop 1": 1, - "advance shop 2": 1, - "advance shop 3": 1, - "syrup shop 1": 1, - "syrup shop 2": 1, - "syrup shop 3": 1, - "subrosian market 2": 2, - "subrosian market 3": 2, - "subrosian market 4": 2, - "subrosian market 5": 2, + "horonShop1": 1, + "horonShop2": 1, + "horonShop3": 1, + "memberShop1": 1, + "memberShop2": 1, + "memberShop3": 1, + "advanceShop1": 1, + "advanceShop2": 1, + "advanceShop3": 1, + "syrupShop1": 1, + "syrupShop2": 1, + "syrupShop3": 1, + "subrosianMarket2": 2, + "subrosianMarket3": 2, + "subrosianMarket4": 2, + "subrosianMarket5": 2, } ITEM_GROUPS = { @@ -267,7 +239,8 @@ 'Gnarled Root Dungeon: Chest in Left Stalfos Room', 'Gnarled Root Dungeon: Hidden Chest Revealed by Button', 'Gnarled Root Dungeon: Chest in Goriya Room', - 'Gnarled Root Dungeon: Boss Reward' + 'Gnarled Root Dungeon: Boss Reward', + 'Gnarled Root Dungeon: Essence', ], 'D2': [ "Snake's Remains: Drop in Left Rope Room", @@ -279,7 +252,8 @@ "Snake's Remains: Chest in Moving Blades Room", "Snake's Remains: Chest in Bomb Spiral Maze Room", "Snake's Remains: Chest on Terrace", - "Snake's Remains: Boss Reward" + "Snake's Remains: Boss Reward", + "Snake's Remains: Essence", ], 'D3': [ "Poison Moth's Lair (B1F): Chest in Roller Room", @@ -291,7 +265,8 @@ "Poison Moth's Lair (1F): Chest Above West Trampoline & Owl", "Poison Moth's Lair (1F): Chest in Room Behind Hidden Cracked Wall", "Poison Moth's Lair (B1F): Chest in Moving Blade Room", - "Poison Moth's Lair (1F): Boss Reward" + "Poison Moth's Lair (1F): Boss Reward", + "Poison Moth's Lair: Essence", ], 'D4': [ 'Dancing Dragon Dungeon (2F): Pots on Buttons Puzzle Drop', @@ -304,7 +279,8 @@ 'Dancing Dragon Dungeon (1F): Chest Revealed by Minecart Torches', 'Dancing Dragon Dungeon (1F): Crumbling Room Chest', 'Dancing Dragon Dungeon (1F): Eye Diving Spot Item', - 'Dancing Dragon Dungeon (B1F): Boss Reward' + 'Dancing Dragon Dungeon (B1F): Boss Reward', + 'Dancing Dragon Dungeon: Essence', ], 'D5': [ "Unicorn's Cave: Right Cart Chest", @@ -317,7 +293,8 @@ "Unicorn's Cave: Magnet Spinner Chest", "Unicorn's Cave: Chest in Right Half of Minecart Bay Room", "Unicorn's Cave: Treadmills Basement Item", - "Unicorn's Cave: Boss Reward" + "Unicorn's Cave: Boss Reward", + "Unicorn's Cave: Essence", ], 'D6': [ 'Ancient Ruins (1F): Magnet Ball Puzzle Drop', @@ -331,7 +308,8 @@ 'Ancient Ruins (1F): Chest on Terrace Left of Entrance', 'Ancient Ruins (2F): Chest After Time Trial', 'Ancient Ruins (2F): Chest on Red Terrace Before Vire', - 'Ancient Ruins (5F): Boss Reward' + 'Ancient Ruins (5F): Boss Reward', + 'Ancient Ruins: Essence', ], 'D7': [ "Explorer's Crypt (1F): Chest in Wizzrobe Room", @@ -345,13 +323,14 @@ "Explorer's Crypt (1F): Chest Above Trampoline Near 2nd Poe", "Explorer's Crypt (B2F): Drop in Room North of Stair Maze", "Explorer's Crypt (B1F): Chest in Jumping Stalfos Room", - "Explorer's Crypt (B1F): Boss Reward" + "Explorer's Crypt (B1F): Boss Reward", + "Explorer's Crypt: Essence", ], 'D8': [ 'Sword & Shield Dungeon (1F): Eye Drop Near Entrance', 'Sword & Shield Dungeon (1F): Three Eyes Chest', 'Sword & Shield Dungeon (1F): Drop in Hardhat & Magnet Ball Room', - 'Sword & Shield Dungeon (1F): U-shaped Spiky Freezer Chest', + 'Sword & Shield Dungeon (1F): U-Shaped Spiky Freezer Chest', 'Sword & Shield Dungeon (B1F): Chest Right of Spinner', 'Sword & Shield Dungeon (1F): Top Chest in Lava Bridge Room', 'Sword & Shield Dungeon (1F): Bottom Chest in Lava Bridge Room', @@ -361,7 +340,8 @@ 'Sword & Shield Dungeon (B1F): Southeast Lava Chest', 'Sword & Shield Dungeon (B1F): Southwest Lava Chest', 'Sword & Shield Dungeon (1F): Chest in Sparks & Pots Room', - 'Sword & Shield Dungeon (B1F): Boss Reward' + 'Sword & Shield Dungeon (B1F): Boss Reward', + 'Sword & Shield Dungeon: Essence', ], 'Trade Sequence': [ 'Horon Village: Dr. Left Reward', @@ -382,3 +362,52 @@ 'Sunken City: Syrup Shop #3' ] } + +GASHA_SPOT_REGIONS = [ + "impa gasha spot", + "horon gasha spot", + "suburbs gasha spot", + "holodrum plain gasha spot", + "holodrum plain island gasha spot", + "spool swamp north gasha spot", + "spool swamp south gasha spot", + "sunken city gasha spot", + "mt cucco gasha spot", + "goron mountain left gasha spot", + "goron mountain right gasha spot", + "eyeglass lake gasha spot", + "tarm ruins gasha spot", + "western coast gasha spot", + "samasa desert gasha spot", + "onox gasha spot", +] + +TREASURE_SPAWN_INSTANT = 0x00 +TREASURE_SPAWN_POOF = 0x10 +TREASURE_SPAWN_DROP = 0x20 +TREASURE_SPAWN_CHEST = 0x30 +TREASURE_SPAWN_DIVE = 0x40 +TREASURE_SPAWN_DIG = 0x50 +TREASURE_SPAWN_DELAYED_CHEST = 0x60 + +TREASURE_GRAB_INSTANT = 0x00 +TREASURE_GRAB_ONE_HAND = 0x01 +TREASURE_GRAB_TWO_HANDS = 0x02 +TREASURE_GRAB_SPIN_SLASH = 0x03 + +TREASURE_SET_ITEM_ROOM_FLAG = 0x08 + +COLLECT_TOUCH = TREASURE_SPAWN_INSTANT | TREASURE_GRAB_TWO_HANDS | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_POOF = TREASURE_SPAWN_POOF | TREASURE_GRAB_TWO_HANDS | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_DROP = TREASURE_SPAWN_DROP | TREASURE_GRAB_ONE_HAND | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_CHEST = TREASURE_SPAWN_CHEST | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_DIVE = TREASURE_SPAWN_DIVE | TREASURE_GRAB_ONE_HAND | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_DIG = TREASURE_SPAWN_DIG | TREASURE_GRAB_TWO_HANDS | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_DELAYED_CHEST = TREASURE_SPAWN_DELAYED_CHEST | TREASURE_GRAB_INSTANT | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_SPINSLASH = TREASURE_SPAWN_INSTANT | TREASURE_GRAB_SPIN_SLASH +COLLECT_FAKE_POOF = TREASURE_SPAWN_POOF | TREASURE_GRAB_INSTANT | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_KEYDROP = TREASURE_SPAWN_DROP | TREASURE_GRAB_INSTANT | TREASURE_SET_ITEM_ROOM_FLAG +COLLECT_DIVER_ROOM = 0x80 +COLLECT_POE_SKIP_ROOM = 0x81 +COLLECT_MAKU_TREE = 0x82 +COLLECT_D5_ARMOS_PUZZLE = 0x83 diff --git a/worlds/tloz_oos/data/Items.py b/worlds/tloz_oos/data/Items.py index dc93c16ba4e2..b66298954265 100644 --- a/worlds/tloz_oos/data/Items.py +++ b/worlds/tloz_oos/data/Items.py @@ -132,12 +132,12 @@ 'subid': 0x02 }, "Rupees (20)": { - 'classification': ItemClassification.progression_skip_balancing, + 'classification': ItemClassification.filler, 'id': 0x28, 'subid': 0x03 }, "Rupees (30)": { - 'classification': ItemClassification.progression_skip_balancing, + 'classification': ItemClassification.filler, 'id': 0x28, 'subid': 0x04 }, @@ -156,21 +156,32 @@ 'id': 0x28, 'subid': 0x08 }, + "Ore Chunks (10)": { + 'classification': ItemClassification.filler, + 'id': 0x37, + 'subid': 0x02 + }, + "Ore Chunks (25)": { + 'classification': ItemClassification.progression_skip_balancing, + 'id': 0x37, + 'subid': 0x01 + }, "Ore Chunks (50)": { 'classification': ItemClassification.progression_skip_balancing, - 'id': 0x37 + 'id': 0x37, + 'subid': 0x00 }, "Heart Container": { 'classification': ItemClassification.useful, 'id': 0x2a }, "Piece of Heart": { - 'classification': ItemClassification.useful, + 'classification': ItemClassification.filler, 'id': 0x2b, 'subid': 0x01 }, "Rare Peach Stone": { - 'classification': ItemClassification.useful, + 'classification': ItemClassification.filler, 'id': 0x2b, 'subid': 0x02 }, diff --git a/worlds/tloz_oos/data/Locations.py b/worlds/tloz_oos/data/Locations.py index f4f32d7d2ed4..7e328b8e3119 100644 --- a/worlds/tloz_oos/data/Locations.py +++ b/worlds/tloz_oos/data/Locations.py @@ -1,1545 +1,2178 @@ +from .Constants import * BASE_LOCATION_ID = 27022001000 LOCATIONS_DATA = { "North Horon: Chest Across Bridge": { - "patcher_name": "eyeglass lake, across bridge", "region_id": "eyeglass lake, across bridge", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC7B8 + "flag_byte": 0xc7b8, + "room": 0x00b8, + "collect": COLLECT_CHEST, }, "Horon Village: Maku Tree Gift": { - "patcher_name": "maku tree", "region_id": "maku tree", "vanilla_item": "Gnarled Key", - "flag_byte": [0xC80B, 0xC80C, 0xC82B, 0xC82C, 0xC82D, 0xC85B, 0xC85C, 0xC85D, 0xC87B] # Maku Tree has several rooms depending on the amount of essences owned + "flag_byte": [0xc80b, 0xc80c, 0xc82b, 0xc82c, 0xc82d, 0xc85b, 0xc85c, 0xc85d, 0xc85e, 0xc87b], + "room": [0x020b, 0x020c, 0x022b, 0x022c, 0x022d, 0x025b, 0x025c, 0x025d, 0x025e, 0x027b], + "collect": COLLECT_MAKU_TREE, + "map_tile": 0xc9, + "symbolic_name": "makuTree", }, "Horon Village: Chest Behind Mushrooms": { - "patcher_name": "horon village SW chest", "region_id": "horon village SW chest", "vanilla_item": "Rupees (20)", - "flag_byte": 0xC7F5 + "flag_byte": 0xc7f5, + "room": 0x00f5, + "collect": COLLECT_CHEST, }, "Horon Village: Chest in Dr. Left's Backyard": { - "patcher_name": "horon village SE chest", "region_id": "horon village SE chest", "vanilla_item": "Rupees (20)", - "flag_byte": 0xC7F9 + "flag_byte": 0xc7f9, + "room": 0x00f9, + "collect": COLLECT_CHEST, }, "Woods of Winter: Holly's Gift": { - "patcher_name": "holly's house", "region_id": "holly's house", "vanilla_item": "Shovel", - "flag_byte": 0xC8A3 + "flag_byte": 0xc8a3, + "room": 0x03a3, + "collect": COLLECT_TOUCH, + "map_tile": 0x7f, + "symbolic_name": "hollyGift", }, "Woods of Winter: Chest on D2 Roof": { - "patcher_name": "chest on top of D2", "region_id": "d2 roof", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC78E + "flag_byte": 0xc78e, + "room": 0x008e, + "collect": COLLECT_CHEST, }, "North Horon: Blaino's Gym Prize": { - "patcher_name": "blaino prize", "region_id": "blaino prize", "vanilla_item": "Ricky's Gloves", - "flag_byte": 0xC8B4 + "flag_byte": 0xc8b4, + "room": 0x03b4, + "collect": COLLECT_TOUCH, + "map_tile": 0x78, + "symbolic_name": "blainoPrize", }, "Holodrum Plain: Underwater Item Below Natzu Bridge": { - "patcher_name": "underwater item below natzu bridge", "region_id": "underwater item below natzu bridge", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC766 + "flag_byte": 0xc766, + "room": 0x0066, + "collect": COLLECT_DIVE, + "symbolic_name": "underwaterItemBelowNatzuBridge" }, "Spool Swamp: Digging Spot Near Vasu's Sign": { - "patcher_name": "spool swamp digging spot", "region_id": "spool swamp digging spot", "vanilla_item": "Armor Ring L-1", # Random ring in vanilla, but rings are randomized anyway - "flag_byte": 0xC782 + "flag_byte": 0xc782, + "room": 0x0082, + "collect": COLLECT_DIG, + "symbolic_name": "spoolSwampDiggingSpot", }, "Spool Swamp: Item in Floodgate Keeper's House": { - "patcher_name": "floodgate keeper's house", "region_id": "floodgate keeper's house", "vanilla_item": "Floodgate Key", - "flag_byte": 0xC8B5 + "flag_byte": 0xc8b5, + "room": 0x03b5, + "collect": COLLECT_TOUCH, + "map_tile": 0x62, + "symbolic_name": "floodgateKeeperHouse" }, "Spool Swamp: Chest in Winter Cave": { - "patcher_name": "spool swamp cave", "region_id": "spool swamp cave", "vanilla_item": "Square Jewel", - "flag_byte": 0xC9FA + "flag_byte": 0xc9fa, + "room": 0x04fa, + "collect": COLLECT_CHEST, + "map_tile": 0xc2, }, "Natzu: Chest after Moblin Keep": { - "patcher_name": "moblin keep", "region_id": "moblin keep chest", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC75B + "flag_byte": 0xc75b, + "room": 0x005b, + "collect": COLLECT_CHEST, }, "Sunken City: Master Diver's Challenge Chest": { - "patcher_name": "master diver's challenge", "region_id": "master diver's challenge", "vanilla_item": "Master's Plaque", - "flag_byte": 0xCABC + "flag_byte": 0xcabc, + "room": 0x05bc, + "collect": COLLECT_CHEST, + "map_tile": 0x2e, }, "Sunken City: Master's Plaque Trade": { - "patcher_name": "master diver's reward", "region_id": "master diver's reward", "vanilla_item": "Flippers", - "flag_byte": 0xCABD, - "bit_mask": 0x80 + "flag_byte": 0xcabd, + "bit_mask": 0x80, + "room": 0x05bd, + "collect": COLLECT_DIVER_ROOM, + "map_tile": 0x2e, + "symbolic_name": "masterPlaqueTrade", }, "Sunken City: Chest in Master Diver's Cave": { - "patcher_name": "chest in master diver's cave", "region_id": "chest in master diver's cave", "vanilla_item": "Rupees (50)", - "flag_byte": 0xCABD + "flag_byte": 0xcabd, + "room": 0x05bd, + "collect": COLLECT_DIVER_ROOM, + "map_tile": 0x2e, }, "Mt. Cucco: Spring Banana Tree": { - "patcher_name": "spring banana tree", "region_id": "spring banana tree", "vanilla_item": "Spring Banana", - "flag_byte": 0xC70F + "flag_byte": 0xc70f, + "room": 0x000f, + "collect": COLLECT_TOUCH, + "symbolic_name": "springBananaTree", }, "Goron Mountain: Item Across Pits": { - "patcher_name": "goron mountain, across pits", "region_id": "goron mountain, across pits", "vanilla_item": "Dragon Key", - "flag_byte": 0xC71A + "flag_byte": 0xc71a, + "room": 0x001a, + "collect": COLLECT_TOUCH, + "symbolic_name": "goronMountainPits", }, "Mt. Cucco: Moving Platform Cave": { - "patcher_name": "mt. cucco, platform cave", "region_id": "mt. cucco, platform cave", "vanilla_item": "Green Joy Ring", - "flag_byte": 0xCABB + "flag_byte": 0xcabb, + "room": 0x05bb, + "collect": COLLECT_DROP, + "map_tile": 0x1f, + "symbolic_name": "mtCuccoPlatformCave", }, "Mt. Cucco: Diving Spot Outside D4": { - "patcher_name": "diving spot outside D4", "region_id": "diving spot outside D4", "vanilla_item": "Pyramid Jewel", - "flag_byte": 0xCAE5, + "flag_byte": 0xcae5, + "room": 0x07e5, + "collect": COLLECT_TOUCH, + "map_tile": 0x1d, + "symbolic_name": "divingSpotOutsideD4", }, "Western Coast: Black Beast's Chest": { - "patcher_name": "black beast's chest", "region_id": "black beast's chest", "vanilla_item": "X-Shaped Jewel", - "flag_byte": 0xC7F4 + "flag_byte": 0xc7f4, + "room": 0x00f4, + "collect": COLLECT_CHEST, }, "Holodrum Plain: Old Man in Treehouse": { - "patcher_name": "old man in treehouse", "region_id": "old man in treehouse", "vanilla_item": "Round Jewel", - "flag_byte": 0xC894 + "flag_byte": 0xc894, + "room": 0x0394, + "collect": COLLECT_TOUCH, + "map_tile": 0xb5, + "symbolic_name": "oldManInTreehouse", }, "Lost Woods: Pedestal Item": { - "patcher_name": "lost woods", "region_id": "lost woods", "vanilla_item": "Progressive Sword", - "flag_byte": 0xC7C9, + "flag_byte": 0xc7c9, + "room": 0x00c9, + "collect": COLLECT_TOUCH, + "map_tile": 0x40, + "symbolic_name": "lostWoodsPedestal", }, "Samasa Desert: Item in Quicksand Pit": { - "patcher_name": "samasa desert pit", "region_id": "samasa desert pit", "vanilla_item": "Rusty Bell", - "flag_byte": 0xCAD2 + "flag_byte": 0xcad2, + "room": 0x05d2, + "collect": COLLECT_TOUCH, + "map_tile": 0xbf, + "symbolic_name": "samasaDesertPit", }, "Samasa Desert: Chest on Cliff": { - "patcher_name": "samasa desert chest", "region_id": "samasa desert chest", "vanilla_item": "Rang Ring L-1", - "flag_byte": 0xC7FF + "flag_byte": 0xc7ff, + "room": 0x00ff, + "collect": COLLECT_CHEST }, "Western Coast: Chest on Beach": { - "patcher_name": "western coast, beach chest", "region_id": "western coast after ship", "vanilla_item": "Blast Ring", - "flag_byte": 0xC7E3 + "flag_byte": 0xc7e3, + "room": 0x00e3, + "collect": COLLECT_CHEST, }, "Western Coast: Chest in House": { - "patcher_name": "western coast, in house", "region_id": "western coast after ship", "vanilla_item": "Bombs (10)", - "flag_byte": 0xC888 + "flag_byte": 0xc888, + "room": 0x0388, + "collect": COLLECT_CHEST, + "map_tile": 0xd2, }, "Holodrum Plain: Chest in Flooded Cave South of Mrs. Ruul": { - "patcher_name": "cave south of mrs. ruul", "region_id": "cave south of mrs. ruul", "vanilla_item": "Octo Ring", - "flag_byte": 0xC9E0 + "flag_byte": 0xc9e0, + "room": 0x04e0, + "collect": COLLECT_CHEST, + "map_tile": 0xb3, }, "Holodrum Plain: Chest in Flooded Cave Behind Mushrooms": { - "patcher_name": "cave north of D1", "region_id": "cave north of D1", "vanilla_item": "Quicksand Ring", - "flag_byte": 0xC9E1 + "flag_byte": 0xc9e1, + "room": 0x04e1, + "collect": COLLECT_CHEST, + "map_tile": 0x87, }, "Woods of Winter: Chest in Autumn Cave Near D2": { - "patcher_name": "cave outside D2", "region_id": "cave outside D2", "vanilla_item": "Moblin Ring", - "flag_byte": 0xCAB3 + "flag_byte": 0xcab3, + "room": 0x05b3, + "collect": COLLECT_CHEST, + "map_tile": 0x8e, }, "Woods of Winter: Chest in Cave Behind Rockslide": { - "patcher_name": "woods of winter, 1st cave", "region_id": "woods of winter, 1st cave", "vanilla_item": "Rupees (30)", - "flag_byte": 0xCAB4 + "flag_byte": 0xcab4, + "room": 0x05b4, + "collect": COLLECT_CHEST, + "map_tile": 0x7d, }, "Sunken City: Chest in Summer Cave": { - "patcher_name": "sunken city, summer cave", "region_id": "sunken city, summer cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xCAB5 + "flag_byte": 0xcab5, + "room": 0x05b5, + "collect": COLLECT_CHEST, + "map_tile": 0x4f, }, "Sunken City: Syrup Shop #1": { - "patcher_name": "syrup shop 1", "region_id": "syrup shop", "vanilla_item": "Potion", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x80, - "scouting_byte": 0xC89C, - "scouting_mask": 0x40 + "scouting_byte": 0xc89c, + "scouting_mask": 0x40, + "room": 0x039c, + "collect": COLLECT_TOUCH, + "map_tile": 0x5e, + "symbolic_name": "syrupShop1", }, "Sunken City: Syrup Shop #2": { - "patcher_name": "syrup shop 2", "region_id": "syrup shop", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x20, - "scouting_byte": 0xC89C, - "scouting_mask": 0x40 + "scouting_byte": 0xc89c, + "scouting_mask": 0x40, + "room": 0x039c, + "collect": COLLECT_TOUCH, + "map_tile": 0x5e, + "symbolic_name": "syrupShop2", }, "Sunken City: Syrup Shop #3": { - "patcher_name": "syrup shop 3", "region_id": "syrup shop", "vanilla_item": "Bombs (10)", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x40, - "scouting_byte": 0xC89C, - "scouting_mask": 0x40 + "scouting_byte": 0xc89c, + "scouting_mask": 0x40, + "room": 0x039c, + "collect": COLLECT_TOUCH, + "map_tile": 0x5e, + "symbolic_name": "syrupShop3", }, "Eyeglass Lake: Chest in Dried Lake East Cave": { - "patcher_name": "dry eyeglass lake, east cave", "region_id": "dry eyeglass lake, east cave", "vanilla_item": "Piece of Heart", - "flag_byte": 0xCAC0 + "flag_byte": 0xcac0, + "room": 0x05c0, + "collect": COLLECT_CHEST, + "map_tile": 0xaa, }, "Goron Mountain: Chest Across Lava": { - "patcher_name": "chest in goron mountain", "region_id": "chest in goron mountain", "vanilla_item": "Armor Ring L-2", - "flag_byte": 0xCAC8 + "flag_byte": 0xcac8, + "room": 0x05c8, + "collect": COLLECT_CHEST, + "map_tile": 0x18, }, "Natzu Region: Chest in Northern Cave": { - "patcher_name": "natzu region, across water", "region_id": "natzu region, across water", "vanilla_item": "Rupees (50)", - "flag_byte": 0xCA0E + "flag_byte": 0xca0e, + "room": 0x050e, + "collect": COLLECT_CHEST, + "map_tile": 0x49, }, "Mt. Cucco: Chest Behind Talon": { - "patcher_name": "mt. cucco, talon's cave", "region_id": "mt. cucco, talon's cave", "vanilla_item": "Subrosian Ring", - "flag_byte": 0xCAB6, - "bit_mask": 0x60 # 0x60 is needed here to ensure we're not sending Talon's wakeup item as a false positive + "flag_byte": 0xcab6, + "bit_mask": 0x60, # 0x60 is needed here to ensure we're not sending Talon's wakeup item as a false positive + "room": 0x05b6, + "collect": COLLECT_CHEST, + "map_tile": 0x1b, }, "Tarm Ruins: Chest in Rabbit Hole Under Tree": { - "patcher_name": "tarm ruins, under tree", "region_id": "tarm ruins, under tree", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC89B + "flag_byte": 0xc89b, + "room": 0x039b, + "collect": COLLECT_CHEST, + "map_tile": 0x10, }, "Eastern Suburbs: Chest in Spring Cave": { - "patcher_name": "eastern suburbs spring cave", "region_id": "eastern suburbs spring cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC9F7 + "flag_byte": 0xc9f7, + "room": 0x04f7, + "collect": COLLECT_CHEST, + "map_tile": 0xcc, }, "Eyeglass Lake: Chest in Dried Lake West Cave": { - "patcher_name": "dry eyeglass lake, west cave", "region_id": "dry eyeglass lake, west cave", "vanilla_item": "Rupees (100)", - "flag_byte": 0xC9FB + "flag_byte": 0xc9fb, + "room": 0x04fb, + "collect": COLLECT_CHEST, + "map_tile": 0xa7, }, "Woods of Winter: Chest in Waterfall Cave": { - "patcher_name": "woods of winter, 2nd cave", "region_id": "woods of winter, 2nd cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xCA12 + "flag_byte": 0xca12, + "room": 0x0512, + "collect": COLLECT_CHEST, + "map_tile": 0x7e, }, "Horon Village: Shop #1": { - "patcher_name": "shop, 20 rupees", "region_id": "horon shop", "vanilla_item": "Bombs (10)", - "flag_byte": 0xC640, + "flag_byte": 0xc640, "bit_mask": 0x20, - "scouting_byte": 0xC8A6, + "scouting_byte": 0xc8a6, + "room": 0x03a6, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "horonShop1", }, "Horon Village: Shop #2": { - "patcher_name": "shop, 30 rupees", "region_id": "horon shop", "vanilla_item": "Progressive Shield", - "flag_byte": 0xC640, + "flag_byte": 0xc640, "bit_mask": 0x40, - "scouting_byte": 0xC8A6, + "scouting_byte": 0xc8a6, + "room": 0x03a6, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "horonShop2", }, "Horon Village: Shop #3": { - "patcher_name": "shop, 150 rupees", "region_id": "horon shop", "vanilla_item": "Flute", - "flag_byte": 0xC640, + "flag_byte": 0xc640, "bit_mask": 0x80, - "scouting_byte": 0xC8A6, + "conditional": True, + "scouting_byte": 0xc8a6, + "room": 0x03a6, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "horonShop3", }, "Horon Village: Member's Shop #1": { - "patcher_name": "member's shop 1", "region_id": "member's shop", "vanilla_item": "Seed Satchel", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x01, - "scouting_byte": 0xC8B0, + "scouting_byte": 0xc8b0, + "room": 0x03b0, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "memberShop1", }, "Horon Village: Member's Shop #2": { - "patcher_name": "member's shop 2", "region_id": "member's shop", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x02, - "scouting_byte": 0xC8B0, + "scouting_byte": 0xc8b0, + "room": 0x03b0, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "memberShop2", }, "Horon Village: Member's Shop #3": { - "patcher_name": "member's shop 3", "region_id": "member's shop", "vanilla_item": "Treasure Map", - "flag_byte": 0xC63F, + "flag_byte": 0xc63f, "bit_mask": 0x08, - "scouting_byte": 0xC8B0, + "scouting_byte": 0xc8b0, + "room": 0x03b0, + "collect": COLLECT_TOUCH, + "map_tile": 0xe6, + "symbolic_name": "memberShop3", }, "Horon Village: Advance Shop #1": { - "patcher_name": "advance shop 1", "region_id": "advance shop", - "vanilla_item": "Rupees (100)", - "flag_byte": 0xC640, + "vanilla_item": "Gasha Seed", + "flag_byte": 0xc640, "bit_mask": 0x01, "conditional": True, - "scouting_byte": 0xC8AF, + "scouting_byte": 0xc8af, + "room": 0x03af, + "collect": COLLECT_TOUCH, + "map_tile": 0xc5, + "symbolic_name": "advanceShop1", }, "Horon Village: Advance Shop #2": { - "patcher_name": "advance shop 2", "region_id": "advance shop", - "vanilla_item": "Rupees (100)", - "flag_byte": 0xC640, + "vanilla_item": "Random Ring", + "flag_byte": 0xc640, "bit_mask": 0x02, "conditional": True, - "scouting_byte": 0xC8AF, + "scouting_byte": 0xc8af, + "room": 0x03af, + "collect": COLLECT_TOUCH, + "map_tile": 0xc5, + "symbolic_name": "advanceShop2", }, "Horon Village: Advance Shop #3": { - "patcher_name": "advance shop 3", "region_id": "advance shop", - "vanilla_item": "Rupees (100)", - "flag_byte": 0xC640, + "vanilla_item": "Random Ring", + "flag_byte": 0xc640, "bit_mask": 0x04, "conditional": True, - "scouting_byte": 0xC8AF, + "scouting_byte": 0xc8af, + "room": 0x03af, + "collect": COLLECT_TOUCH, + "map_tile": 0xc5, + "symbolic_name": "advanceShop3", }, "Subrosia: Tower of Winter": { - "patcher_name": "tower of winter", "region_id": "tower of winter", "vanilla_item": "Rod of Seasons (Winter)", - "flag_byte": 0xCAF2 + "flag_byte": 0xcaf2, + "room": 0x05f2, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "towerOfWinter", }, "Subrosia: Tower of Summer": { - "patcher_name": "tower of summer", "region_id": "tower of summer", "vanilla_item": "Rod of Seasons (Summer)", - "flag_byte": 0xCAF8 + "flag_byte": 0xcaf8, + "room": 0x05f8, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "towerOfSummer", }, "Subrosia: Tower of Spring": { - "patcher_name": "tower of spring", "region_id": "tower of spring", "vanilla_item": "Rod of Seasons (Spring)", - "flag_byte": 0xCAF5 + "flag_byte": 0xcaf5, + "room": 0x05f5, + "collect": COLLECT_TOUCH, + "map_tile": 0x1e, + "symbolic_name": "towerOfSpring", }, "Subrosia: Tower of Autumn": { - "patcher_name": "tower of autumn", "region_id": "tower of autumn", "vanilla_item": "Rod of Seasons (Autumn)", - "flag_byte": 0xCAFB + "flag_byte": 0xcafb, + "room": 0x05fb, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "towerOfAutumn", }, "Subrosia: Dance Hall Reward": { - "patcher_name": "subrosian dance hall", "region_id": "subrosian dance hall", "vanilla_item": "Progressive Boomerang", - "flag_byte": 0xC895 + "flag_byte": 0xc895, + "room": 0x0395, + "collect": COLLECT_TOUCH, + "map_tile": 0x9a, + "symbolic_name": "subrosianDanceHall", }, "Subrosia: Temple of Seasons": { - "patcher_name": "temple of seasons", "region_id": "temple of seasons", "vanilla_item": "Rod of Seasons", - "flag_byte": 0xC8AC + "flag_byte": 0xc8ac, + "room": 0x03ac, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "templeOfSeasons", }, "Subrosia: Seaside Digging Spot": { - "patcher_name": "subrosia seaside", "region_id": "subrosia seaside", "vanilla_item": "Star Ore", - "flag_byte": [0xC865, 0xC866, 0xC875, 0xC876] + "flag_byte": [0xc865, 0xc866, 0xc875, 0xc876], + "room": [0x0166, 0x0176, 0x0175, 0x0165], + "collect": COLLECT_DIG, + "map_tile": 0xb0, + "symbolic_name": "subrosiaSeaside", }, "Subrosia: Wilds Chest": { - "patcher_name": "subrosian wilds chest", "region_id": "subrosian wilds chest", "vanilla_item": "Blue Ore", - "flag_byte": 0xC841 + "flag_byte": 0xc841, + "room": 0x0141, + "collect": COLLECT_CHEST, + "map_tile": 0x1e, }, "Subrosia: Wilds Digging Spot": { - "patcher_name": "subrosian wilds digging spot", "region_id": "subrosian wilds digging spot", "vanilla_item": "Power Ring L-2", # Random ring in vanilla, but this doesn't exist in rando - "flag_byte": 0xC840 + "flag_byte": 0xc840, + "room": 0x0140, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosianWildsDiggingSpot", }, "Subrosia: Chest Above Magnet Cave": { - "patcher_name": "subrosia village chest", "region_id": "subrosia village chest", "vanilla_item": "Red Ore", - "flag_byte": 0xC858 + "flag_byte": 0xc858, + "room": 0x0158, + "collect": COLLECT_CHEST, + "map_tile": 0xb0, }, "Subrosia: Northwest Open Cave": { - "patcher_name": "subrosia, open cave", "region_id": "subrosia, open cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC9F1 + "flag_byte": 0xc9f1, + "room": 0x04f1, + "collect": COLLECT_CHEST, + "map_tile": 0x25, }, "Subrosia: Northwest Locked Cave": { - "patcher_name": "subrosia, locked cave", "region_id": "subrosia, locked cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xCAC6 + "flag_byte": 0xcac6, + "room": 0x05c6, + "collect": COLLECT_CHEST, + "map_tile": 0x25, }, "Subrosia: Market #1": { - "patcher_name": "subrosia market, 1st item", "region_id": "subrosia market star ore", "vanilla_item": "Ribbon", - "flag_byte": 0xC642, + "flag_byte": 0xc642, "bit_mask": 0x01, - "scouting_byte": 0xC8A0, + "scouting_byte": 0xc8a0, + "room": 0x03a0, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "subrosianMarket1", }, "Subrosia: Market #2": { - "patcher_name": "subrosia market, 2nd item", "region_id": "subrosia market ore chunks", "vanilla_item": "Rare Peach Stone", - "flag_byte": 0xC642, + "flag_byte": 0xc642, "bit_mask": 0x02, - "scouting_byte": 0xC8A0, + "scouting_byte": 0xc8a0, + "room": 0x03a0, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "subrosianMarket2", }, "Subrosia: Market #3": { - "patcher_name": "subrosia market, 3rd item", "region_id": "subrosia market ore chunks", "vanilla_item": "Progressive Shield", - "flag_byte": 0xC642, + "flag_byte": 0xc642, "bit_mask": 0x04, - "scouting_byte": 0xC8A0, + "scouting_byte": 0xc8a0, + "room": 0x03a0, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "subrosianMarket3", }, "Subrosia: Market #4": { - "patcher_name": "subrosia market, 4th item", "region_id": "subrosia market ore chunks", "vanilla_item": "Bombs (10)", - "flag_byte": 0xC642, + "flag_byte": 0xc642, "bit_mask": 0x08, - "scouting_byte": 0xC8A0, + "scouting_byte": 0xc8a0, + "room": 0x03a0, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "subrosianMarket4", }, "Subrosia: Market #5": { - "patcher_name": "subrosia market, 5th item", "region_id": "subrosia market ore chunks", "vanilla_item": "Member's Card", - "flag_byte": 0xC642, + "flag_byte": 0xc642, "bit_mask": 0x10, - "scouting_byte": 0xC8A0, + "scouting_byte": 0xc8a0, + "room": 0x03a0, + "collect": COLLECT_TOUCH, + "map_tile": 0xb0, + "symbolic_name": "subrosianMarket5", }, "Subrosia: Item Smelted in Great Furnace": { - "patcher_name": "great furnace", "region_id": "great furnace", "vanilla_item": "Hard Ore", - "flag_byte": 0xC88E + "flag_byte": 0xc88e, + "room": 0x038e, + "collect": COLLECT_TOUCH, + "map_tile": 0xb9, + "symbolic_name": "greatFurnace", }, "Subrosia: Smithy Hard Ore Reforge": { - "patcher_name": "subrosian smithy ore", "region_id": "subrosian smithy ore", "vanilla_item": "Progressive Shield", - "flag_byte": 0xC897, - "bit_mask": 0x40 + "flag_byte": 0xc897, + "bit_mask": 0x40, + "room": 0x0397, + "collect": COLLECT_TOUCH, + "map_tile": 0x1e, + "symbolic_name": "subrosianSmithyOre", }, "Subrosia: Smithy Rusty Bell Reforge": { - "patcher_name": "subrosian smithy bell", "region_id": "subrosian smithy bell", "vanilla_item": "Pirate's Bell", - "flag_byte": 0xC897, - "bit_mask": 0x80 + "flag_byte": 0xc897, + "bit_mask": 0x80, + "room": 0x0397, + "collect": COLLECT_TOUCH, + "map_tile": 0x1e, + "symbolic_name": "subrosianSmithyBell", }, "Hero's Cave: Topmost Chest": { - "patcher_name": "d0 key chest", "region_id": "d0 key chest", "vanilla_item": "Small Key (Hero's Cave)", "dungeon": 0, - "flag_byte": 0xC903 + "flag_byte": 0xc903, + "room": 0x0403, + "collect": COLLECT_CHEST, + "map_tile": 0xd4, }, "Hero's Cave: Final Chest": { - "patcher_name": "d0 sword chest", "region_id": "d0 sword chest", "vanilla_item": "Progressive Sword", "dungeon": 0, - "flag_byte": 0xC906 + "flag_byte": 0xc906, + "room": 0x0404, + "collect": COLLECT_TOUCH, # not a real chest! + "map_tile": 0xd4, + "symbolic_name": "d0SwordChest", }, "Hero's Cave: Item in Basement Under Keese Room": { - "patcher_name": "d0 hidden 2d section", "region_id": "d0 hidden 2d section", "vanilla_item": "Gasha Seed", "dungeon": 0, - "flag_byte": 0xC901 + "flag_byte": 0xc901, + "room": 0x0601, + "collect": COLLECT_TOUCH, + "map_tile": 0xd4, + "symbolic_name": "d0HiddenBasement" }, "Hero's Cave: Alternative Entrance Chest": { - "patcher_name": "d0 rupee chest", "region_id": "d0 rupee chest", "vanilla_item": "Rupees (30)", "dungeon": 0, - "flag_byte": 0xC905 + "flag_byte": 0xc905, + "room": 0x0405, + "collect": COLLECT_CHEST, + "map_tile": 0xd4, }, "Gnarled Root Dungeon: Drop in Right Stalfos Room": { - "patcher_name": "d1 stalfos drop", "region_id": "d1 stalfos drop", "vanilla_item": "Small Key (Gnarled Root Dungeon)", "dungeon": 1, - "flag_byte": 0xC91B + "flag_byte": 0xc91b, + "room": 0x041b, + "collect": COLLECT_DROP, + "map_tile": 0x96, + "symbolic_name": "d1StalfosDrop", }, "Gnarled Root Dungeon: Item in Basement": { - "patcher_name": "d1 basement", "region_id": "d1 basement", "vanilla_item": "Seed Satchel", "dungeon": 1, - "flag_byte": 0xC909 + "flag_byte": 0xc909, + "room": 0x0609, + "collect": COLLECT_TOUCH, + "map_tile": 0x96, + "symbolic_name": "d1Basement", }, "Gnarled Root Dungeon: Chest in Block-pushing Room": { - "patcher_name": "d1 block-pushing room", "region_id": "d1 block-pushing room", "vanilla_item": "Gasha Seed", "dungeon": 1, - "flag_byte": 0xC90D + "flag_byte": 0xc90d, + "room": 0x040d, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Chest Near Railway": { - "patcher_name": "d1 railway chest", "region_id": "d1 railway chest", "vanilla_item": "Bombs (10)", "dungeon": 1, - "flag_byte": 0xC910 + "flag_byte": 0xc910, + "room": 0x0410, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Chest in Floormaster Room": { - "patcher_name": "d1 floormaster room", "region_id": "d1 floormaster room", "vanilla_item": "Discovery Ring", "dungeon": 1, - "flag_byte": 0xC917 + "flag_byte": 0xc917, + "room": 0x0417, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Chest Near Railway Lever": { - "patcher_name": "d1 lever room", "region_id": "d1 lever room", "vanilla_item": "Compass (Gnarled Root Dungeon)", "dungeon": 1, - "flag_byte": 0xC90F + "flag_byte": 0xc90f, + "room": 0x040f, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Chest in Left Stalfos Room": { - "patcher_name": "d1 stalfos chest", "region_id": "d1 stalfos chest", "vanilla_item": "Dungeon Map (Gnarled Root Dungeon)", "dungeon": 1, - "flag_byte": 0xC919 + "flag_byte": 0xc919, + "room": 0x0419, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Hidden Chest Revealed by Button": { - "patcher_name": "d1 button chest", "region_id": "d1 button chest", "vanilla_item": "Small Key (Gnarled Root Dungeon)", "dungeon": 1, - "flag_byte": 0xC911 + "flag_byte": 0xc911, + "room": 0x0411, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Chest in Goriya Room": { - "patcher_name": "d1 goriya chest", "region_id": "d1 goriya chest", "vanilla_item": "Boss Key (Gnarled Root Dungeon)", "dungeon": 1, - "flag_byte": 0xC914 + "flag_byte": 0xc914, + "room": 0x0414, + "collect": COLLECT_CHEST, + "map_tile": 0x96, }, "Gnarled Root Dungeon: Boss Reward": { - "patcher_name": "d1 boss", "region_id": "d1 boss", "vanilla_item": "Heart Container", "dungeon": 1, - "flag_byte": 0xC912 + "flag_byte": 0xc912, + "room": 0x0412, + "collect": COLLECT_POOF, + "map_tile": 0x96, + "symbolic_name": "d1Boss" }, "Snake's Remains: Drop in Left Rope Room": { - "patcher_name": "d2 rope drop", "region_id": "d2 rope drop", "vanilla_item": "Small Key (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC934 + "flag_byte": 0xc934, + "room": 0x0434, + "collect": COLLECT_DROP, + "map_tile": 0x8d, + "symbolic_name": "d2RopeDrop", }, "Snake's Remains: Chest in Distant Moblins Room": { - "patcher_name": "d2 moblin chest", "region_id": "d2 moblin chest", "vanilla_item": "Power Bracelet", "dungeon": 2, - "flag_byte": 0xC92A + "flag_byte": 0xc92a, + "room": 0x042a, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest in Rollers Section": { - "patcher_name": "d2 roller chest", "region_id": "d2 roller chest", "vanilla_item": "Rupees (10)", "dungeon": 2, - "flag_byte": 0xC91F + "flag_byte": 0xc91f, + "room": 0x041f, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest Left from Entrance": { - "patcher_name": "d2 left from entrance", "region_id": "d2 left from entrance", "vanilla_item": "Rupees (5)", "dungeon": 2, - "flag_byte": 0xC938 + "flag_byte": 0xc938, + "room": 0x0438, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest Behind Pots in Hardhat Room": { - "patcher_name": "d2 pot chest", "region_id": "d2 pot chest", "vanilla_item": "Dungeon Map (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC92B + "flag_byte": 0xc92b, + "room": 0x042b, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest in Right Rope Room": { - "patcher_name": "d2 rope chest", "region_id": "d2 rope chest", "vanilla_item": "Compass (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC936 + "flag_byte": 0xc936, + "room": 0x0436, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest in Moving Blades Room": { - "patcher_name": "d2 blade chest", "region_id": "d2 blade chest", "vanilla_item": "Small Key (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC931 + "flag_byte": 0xc931, + "room": 0x0431, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest in Bomb Spiral Maze Room": { - "patcher_name": "d2 spiral chest", "region_id": "d2 spiral chest", "vanilla_item": "Small Key (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC92D + "flag_byte": 0xc92d, + "room": 0x042d, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Chest on Terrace": { - "patcher_name": "d2 terrace chest", "region_id": "d2 terrace chest", "vanilla_item": "Boss Key (Snake's Remains)", "dungeon": 2, - "flag_byte": 0xC924 + "flag_byte": 0xc924, + "room": 0x0424, + "collect": COLLECT_CHEST, + "map_tile": 0x8d, }, "Snake's Remains: Boss Reward": { - "patcher_name": "d2 boss", "region_id": "d2 boss", "vanilla_item": "Heart Container", "dungeon": 2, - "flag_byte": 0xC929 + "flag_byte": 0xc929, + "room": 0x0429, + "collect": COLLECT_POOF, + "map_tile": 0x8d, + "symbolic_name": "d2Boss" }, "Poison Moth's Lair (B1F): Chest in Roller Room": { - "patcher_name": "d3 roller chest", "region_id": "d3 roller chest", "vanilla_item": "Small Key (Poison Moth's Lair)", "dungeon": 3, - "flag_byte": 0xC94C + "flag_byte": 0xc94c, + "room": 0x044c, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Chest in Mimics Room": { - "patcher_name": "d3 mimic chest", "region_id": "d3 mimic chest", "vanilla_item": "Progressive Feather", "dungeon": 3, - "flag_byte": 0xC950 + "flag_byte": 0xc950, + "room": 0x0450, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Chest Above East Trampoline": { - "patcher_name": "d3 zol chest", "region_id": "d3 zol chest", "vanilla_item": "Small Key (Poison Moth's Lair)", "dungeon": 3, - "flag_byte": 0xC94F + "flag_byte": 0xc94f, + "room": 0x044f, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (B1F): Chest in Watery Room": { - "patcher_name": "d3 water room", "region_id": "d3 water room", "vanilla_item": "Rupees (30)", "dungeon": 3, - "flag_byte": 0xC941 + "flag_byte": 0xc941, + "room": 0x0441, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (B1F): Chest on Quicksand Terrace": { - "patcher_name": "d3 quicksand terrace", "region_id": "d3 quicksand terrace", "vanilla_item": "Gasha Seed", "dungeon": 3, - "flag_byte": 0xC944 + "flag_byte": 0xc944, + "room": 0x0444, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Chest in Moldorm Room": { - "patcher_name": "d3 moldorm chest", "region_id": "d3 moldorm chest", "vanilla_item": "Bombs (10)", "dungeon": 3, - "flag_byte": 0xC954 + "flag_byte": 0xc954, + "room": 0x0454, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Chest Above West Trampoline & Owl": { - "patcher_name": "d3 trampoline chest", "region_id": "d3 trampoline chest", "vanilla_item": "Compass (Poison Moth's Lair)", "dungeon": 3, - "flag_byte": 0xC94D + "flag_byte": 0xc94d, + "room": 0x044d, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Chest in Room Behind Hidden Cracked Wall": { - "patcher_name": "d3 bombed wall chest", "region_id": "d3 bombed wall chest", "vanilla_item": "Dungeon Map (Poison Moth's Lair)", "dungeon": 3, - "flag_byte": 0xC951 + "flag_byte": 0xc951, + "room": 0x0451, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (B1F): Chest in Moving Blade Room": { - "patcher_name": "d3 giant blade room", "region_id": "d3 giant blade room", "vanilla_item": "Boss Key (Poison Moth's Lair)", "dungeon": 3, - "flag_byte": 0xC946 + "flag_byte": 0xc946, + "room": 0x0446, + "collect": COLLECT_CHEST, + "map_tile": 0x60, }, "Poison Moth's Lair (1F): Boss Reward": { - "patcher_name": "d3 boss", "region_id": "d3 boss", "vanilla_item": "Heart Container", "dungeon": 3, - "flag_byte": 0xC953 + "flag_byte": 0xc953, + "room": 0x0453, + "collect": COLLECT_POOF, + "map_tile": 0x60, + "symbolic_name": "d3Boss" }, "Dancing Dragon Dungeon (2F): Pots on Buttons Puzzle Drop": { - "patcher_name": "d4 pot puzzle", "region_id": "d4 pot puzzle", "vanilla_item": "Small Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC97B + "flag_byte": 0xc97b, + "room": 0x047b, + "collect": COLLECT_DROP, + "map_tile": 0x1d, + "symbolic_name": "d4PotPuzzle", }, "Dancing Dragon Dungeon (2F): Chest North of Entrance": { - "patcher_name": "d4 north of entrance", "region_id": "d4 north of entrance", "vanilla_item": "Bombs (10)", "dungeon": 4, - "flag_byte": 0xC97F + "flag_byte": 0xc97f, + "room": 0x047f, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (1F): Chest in Southwest Quadrant of Beamos Room": { - "patcher_name": "d4 maze chest", "region_id": "d4 maze chest", "vanilla_item": "Dungeon Map (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC969 + "flag_byte": 0xc969, + "room": 0x0469, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (1F): Dark Room Chest": { - "patcher_name": "d4 dark room", "region_id": "d4 dark room", "vanilla_item": "Small Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC96D + "flag_byte": 0xc96d, + "room": 0x046d, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (2F): Chest in Water Donut Room": { - "patcher_name": "d4 water ring room", "region_id": "d4 water ring room", "vanilla_item": "Compass (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC983 + "flag_byte": 0xc983, + "room": 0x0483, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (2F): Pool Drop": { - "patcher_name": "d4 pool", "region_id": "d4 pool", "vanilla_item": "Small Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC975 + "flag_byte": 0xc975, + "room": 0x0475, + "collect": COLLECT_DROP, + "map_tile": 0x1d, + "symbolic_name": "d4Pool", }, "Dancing Dragon Dungeon (1F): Chest on Small Terrace": { - "patcher_name": "d4 terrace", "region_id": "d4 terrace", "vanilla_item": "Small Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC963 + "flag_byte": 0xc963, + "room": 0x0463, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (1F): Chest Revealed by Minecart Torches": { - "patcher_name": "d4 torch chest", "region_id": "d4 torch chest", "vanilla_item": "Small Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC964 + "flag_byte": 0xc964, + "room": 0x0464, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (1F): Crumbling Room Chest": { - "patcher_name": "d4 cracked floor room", "region_id": "d4 cracked floor room", "vanilla_item": "Progressive Slingshot", "dungeon": 4, - "flag_byte": 0xC973 + "flag_byte": 0xc973, + "room": 0x0473, + "collect": COLLECT_CHEST, + "map_tile": 0x1d, }, "Dancing Dragon Dungeon (1F): Eye Diving Spot Item": { - "patcher_name": "d4 dive spot", "region_id": "d4 dive spot", "vanilla_item": "Boss Key (Dancing Dragon Dungeon)", "dungeon": 4, - "flag_byte": 0xC96C + "flag_byte": 0xc96c, + "room": 0x046c, + "collect": COLLECT_DIVE, + "map_tile": 0x1d, + "symbolic_name": "d4DiveSpot", }, "Dancing Dragon Dungeon (B1F): Boss Reward": { - "patcher_name": "d4 boss", "region_id": "d4 boss", "vanilla_item": "Heart Container", "dungeon": 4, - "flag_byte": 0xC95F + "flag_byte": 0xc95f, + "room": 0x045f, + "collect": COLLECT_POOF, + "map_tile": 0x1d, + "symbolic_name": "d4Boss" }, "Unicorn's Cave: Right Cart Chest": { - "patcher_name": "d5 cart chest", "region_id": "d5 cart chest", "vanilla_item": "Small Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC999 + "flag_byte": 0xc999, + "room": 0x0499, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Chest Left from Entrance": { - "patcher_name": "d5 left chest", "region_id": "d5 left chest", "vanilla_item": "Small Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC9A3 + "flag_byte": 0xc9a3, + "room": 0x04a3, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Magnet Gloves Chest": { - "patcher_name": "d5 magnet ball chest", "region_id": "d5 magnet ball chest", "vanilla_item": "Magnetic Gloves", "dungeon": 5, - "flag_byte": 0xC989 + "flag_byte": 0xc989, + "room": 0x0489, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Terrace Chest": { - "patcher_name": "d5 terrace chest", "region_id": "d5 terrace chest", "vanilla_item": "Rupees (100)", "dungeon": 5, - "flag_byte": 0xC997 + "flag_byte": 0xc997, + "room": 0x0497, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Armos Puzzle Room Chest": { - "patcher_name": "d5 armos chest", "region_id": "d5 armos chest", "vanilla_item": "Small Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC991 + "flag_byte": 0xc991, + "room": 0x0491, + "collect": COLLECT_D5_ARMOS_PUZZLE, + "map_tile": 0x8a, + "symbolic_name": "d5ArmosChest", }, "Unicorn's Cave: Gibdo Room Chest": { - "patcher_name": "d5 gibdo/zol chest", "region_id": "d5 gibdo/zol chest", "vanilla_item": "Dungeon Map (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC98F + "flag_byte": 0xc98f, + "room": 0x048f, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Quicksand Spiral Chest": { - "patcher_name": "d5 spiral chest", "region_id": "d5 spiral chest", "vanilla_item": "Compass (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC99D + "flag_byte": 0xc99d, + "room": 0x049d, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Magnet Spinner Chest": { - "patcher_name": "d5 spinner chest", "region_id": "d5 spinner chest", "vanilla_item": "Small Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC99F + "flag_byte": 0xc99f, + "room": 0x049f, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Chest in Right Half of Minecart Bay Room": { - "patcher_name": "d5 stalfos room", "region_id": "d5 stalfos room", "vanilla_item": "Small Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC9A5 + "flag_byte": 0xc9a5, + "room": 0x04a5, + "collect": COLLECT_CHEST, + "map_tile": 0x8a, }, "Unicorn's Cave: Treadmills Basement Item": { - "patcher_name": "d5 basement", "region_id": "d5 basement", "vanilla_item": "Boss Key (Unicorn's Cave)", "dungeon": 5, - "flag_byte": 0xC98B + "flag_byte": 0xc98b, + "room": 0x068b, + "collect": COLLECT_TOUCH, + "map_tile": 0x8a, + "symbolic_name": "d5Basement", }, "Unicorn's Cave: Boss Reward": { - "patcher_name": "d5 boss", "region_id": "d5 boss", "vanilla_item": "Heart Container", "dungeon": 5, - "flag_byte": 0xC98C + "flag_byte": 0xc98c, + "room": 0x048c, + "collect": COLLECT_POOF, + "map_tile": 0x8a, + "symbolic_name": "d5Boss" }, "Ancient Ruins (1F): Magnet Ball Puzzle Drop": { - "patcher_name": "d6 magnet ball drop", "region_id": "d6 magnet ball drop", "vanilla_item": "Small Key (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9AB + "flag_byte": 0xc9ab, + "room": 0x04ab, + "collect": COLLECT_DROP, + "map_tile": 0x00, + "symbolic_name": "d6MagnetBallDrop", }, "Ancient Ruins (2F): Chest North of Main Spinner": { - "patcher_name": "d6 spinner north", "region_id": "d6 spinner north", "vanilla_item": "Small Key (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9C2 + "flag_byte": 0xc9c2, + "room": 0x04c2, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (3F): Armos Hall Chest": { - "patcher_name": "d6 armos hall", "region_id": "d6 armos hall", "vanilla_item": "Progressive Boomerang", "dungeon": 6, - "flag_byte": 0xC9D0 + "flag_byte": 0xc9d0, + "room": 0x04d0, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (1F): Crystal Maze Room Chest": { - "patcher_name": "d6 crystal trap room", "region_id": "d6 crystal trap room", "vanilla_item": "Rupees (10)", "dungeon": 6, - "flag_byte": 0xC9AF + "flag_byte": 0xc9af, + "room": 0x04af, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (1F): Crumbling Ground Room Chest": { - "patcher_name": "d6 1F east", "region_id": "d6 1F east", "vanilla_item": "Rupees (5)", "dungeon": 6, - "flag_byte": 0xC9B3 + "flag_byte": 0xc9b3, + "room": 0x04b3, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (2F): Chest in Gibdo Room": { - "patcher_name": "d6 2F gibdo chest", "region_id": "d6 2F gibdo chest", "vanilla_item": "Bombs (10)", "dungeon": 6, - "flag_byte": 0xC9BF + "flag_byte": 0xc9bf, + "room": 0x04bf, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (2F): Chest Between 4 Armos": { - "patcher_name": "d6 2F armos chest", "region_id": "d6 2F armos chest", "vanilla_item": "Rupees (5)", "dungeon": 6, - "flag_byte": 0xC9C3 + "flag_byte": 0xc9c3, + "room": 0x04c3, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (1F): Chest in Beamos Room": { - "patcher_name": "d6 beamos room", "region_id": "d6 beamos room", "vanilla_item": "Compass (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9AD + "flag_byte": 0xc9ad, + "room": 0x04ad, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (1F): Chest on Terrace Left of Entrance": { - "patcher_name": "d6 1F terrace", "region_id": "d6 1F terrace", "vanilla_item": "Dungeon Map (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9B0 + "flag_byte": 0xc9b0, + "room": 0x04b0, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (2F): Chest After Time Trial": { - "patcher_name": "d6 escape room", "region_id": "d6 escape room", "vanilla_item": "Boss Key (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9C4 + "flag_byte": 0xc9c4, + "room": 0x04c4, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (2F): Chest on Red Terrace Before Vire": { - "patcher_name": "d6 vire chest", "region_id": "d6 vire chest", "vanilla_item": "Small Key (Ancient Ruins)", "dungeon": 6, - "flag_byte": 0xC9C1 + "flag_byte": 0xc9c1, + "room": 0x04c1, + "collect": COLLECT_CHEST, + "map_tile": 0x00, }, "Ancient Ruins (5F): Boss Reward": { - "patcher_name": "d6 boss", "region_id": "d6 boss", "vanilla_item": "Heart Container", "dungeon": 6, - "flag_byte": 0xC9D5 + "flag_byte": 0xc9d5, + "room": 0x04d5, + "collect": COLLECT_POOF, + "map_tile": 0x00, + "symbolic_name": "d6Boss" }, "Explorer's Crypt (1F): Chest in Wizzrobe Room": { - "patcher_name": "d7 wizzrobe chest", "region_id": "d7 wizzrobe chest", "vanilla_item": "Small Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA54 + "flag_byte": 0xca54, + "room": 0x0554, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (B1F): Chest in Fast Moving Platform Room": { - "patcher_name": "d7 spike chest", "region_id": "d7 spike chest", "vanilla_item": "Progressive Feather", "dungeon": 7, - "flag_byte": 0xCA44 + "flag_byte": 0xca44, + "room": 0x0544, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (B2F): Stair Maze Chest": { - "patcher_name": "d7 maze chest", "region_id": "d7 maze chest", "vanilla_item": "Rupees (1)", "dungeon": 7, - "flag_byte": 0xCA43 + "flag_byte": 0xca43, + "room": 0x0543, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (1F): Chest Right of Entrance": { - "patcher_name": "d7 right of entrance", "region_id": "d7 right of entrance", "vanilla_item": "Power Ring L-1", "dungeon": 7, - "flag_byte": 0xCA5A + "flag_byte": 0xca5a, + "room": 0x055a, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (1F): Chest Behind Cracked Wall": { - "patcher_name": "d7 bombed wall chest", "region_id": "d7 bombed wall chest", "vanilla_item": "Compass (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA52 + "flag_byte": 0xca52, + "room": 0x0552, + "collect": COLLECT_POE_SKIP_ROOM, + "map_tile": 0xd0, }, "Explorer's Crypt (B1F): Zol Button Drop": { - "patcher_name": "d7 zol button", "region_id": "d7 zol button", "vanilla_item": "Small Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA45 + "flag_byte": 0xca45, + "room": 0x0545, + "collect": COLLECT_DROP, + "map_tile": 0xd0, + "symbolic_name": "d7ZolButton", }, "Explorer's Crypt (B2F): Armos Puzzle Drop": { - "patcher_name": "d7 armos puzzle", "region_id": "d7 armos puzzle", "vanilla_item": "Small Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA35 + "flag_byte": 0xca35, + "room": 0x0535, + "collect": COLLECT_DROP, + "map_tile": 0xd0, + "symbolic_name": "d7ArmosPuzzle", }, "Explorer's Crypt (B1F): Chest Connected to Magnet Ball Button": { - "patcher_name": "d7 magunesu chest", "region_id": "d7 magunesu chest", "vanilla_item": "Small Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA47 + "flag_byte": 0xca47, + "room": 0x0547, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (1F): Chest Above Trampoline Near 2nd Poe": { - "patcher_name": "d7 quicksand chest", "region_id": "d7 quicksand chest", "vanilla_item": "Dungeon Map (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA58 + "flag_byte": 0xca58, + "room": 0x0558, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (B2F): Drop in Room North of Stair Maze": { - "patcher_name": "d7 B2F drop", "region_id": "d7 B2F drop", "vanilla_item": "Small Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA3D + "flag_byte": 0xca3d, + "room": 0x053d, + "collect": COLLECT_DROP, + "map_tile": 0xd0, + "symbolic_name": "d7DropNorthOfStairMaze", }, "Explorer's Crypt (B1F): Chest in Jumping Stalfos Room": { - "patcher_name": "d7 stalfos chest", "region_id": "d7 stalfos chest", "vanilla_item": "Boss Key (Explorer's Crypt)", "dungeon": 7, - "flag_byte": 0xCA48 + "flag_byte": 0xca48, + "room": 0x0548, + "collect": COLLECT_CHEST, + "map_tile": 0xd0, }, "Explorer's Crypt (B1F): Boss Reward": { - "patcher_name": "d7 boss", "region_id": "d7 boss", "vanilla_item": "Heart Container", "dungeon": 7, - "flag_byte": 0xCA50 + "flag_byte": 0xca50, + "room": 0x0550, + "collect": COLLECT_POOF, + "map_tile": 0xd0, + "symbolic_name": "d7Boss" }, "Sword & Shield Dungeon (1F): Eye Drop Near Entrance": { - "patcher_name": "d8 eye drop", "region_id": "d8 eye drop", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA82 + "flag_byte": 0xca82, + "room": 0x0582, + "collect": COLLECT_DROP, + "map_tile": 0x04, + "symbolic_name": "d8EyeDrop", }, "Sword & Shield Dungeon (1F): Three Eyes Chest": { - "patcher_name": "d8 three eyes chest", "region_id": "d8 three eyes chest", "vanilla_item": "Steadfast Ring", "dungeon": 8, - "flag_byte": 0xCA7D + "flag_byte": 0xca7d, + "room": 0x057d, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Drop in Hardhat & Magnet Ball Room": { - "patcher_name": "d8 hardhat drop", "region_id": "d8 hardhat drop", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA75 + "flag_byte": 0xca75, + "room": 0x0575, + "collect": COLLECT_DROP, + "map_tile": 0x04, + "symbolic_name": "d8HardhatDrop", }, "Sword & Shield Dungeon (1F): U-Shaped Spiky Freezer Chest": { - "patcher_name": "d8 spike room", "region_id": "d8 spike room", "vanilla_item": "Compass (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA8B + "flag_byte": 0xca8b, + "room": 0x058b, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (B1F): Chest Right of Spinner": { - "patcher_name": "d8 spinner chest", "region_id": "d8 spinner chest", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA70 + "flag_byte": 0xca70, + "room": 0x0570, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Top Chest in Lava Bridge Room": { - "patcher_name": "d8 armos chest", "region_id": "d8 armos chest", "vanilla_item": "Progressive Slingshot", "dungeon": 8, - "flag_byte": 0xCA8D + "flag_byte": 0xca8d, + "room": 0x058d, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Bottom Chest in Lava Bridge Room": { - "patcher_name": "d8 magnet ball room", "region_id": "d8 magnet ball room", "vanilla_item": "Dungeon Map (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA8E + "flag_byte": 0xca8e, + "room": 0x058e, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Chest in Bombable Blocks Room": { - "patcher_name": "d8 darknut chest", "region_id": "d8 darknut chest", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA8C + "flag_byte": 0xca8c, + "room": 0x058c, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Chest on Terrace After Pols Voice Room": { - "patcher_name": "d8 pols voice chest", "region_id": "d8 pols voice chest", "vanilla_item": "Boss Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA80 + "flag_byte": 0xca80, + "room": 0x0580, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Ghost Armos Puzzle Drop": { - "patcher_name": "d8 ghost armos drop", "region_id": "d8 ghost armos drop", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA7F + "flag_byte": 0xca7f, + "room": 0x057f, + "collect": COLLECT_DROP, + "map_tile": 0x04, + "symbolic_name": "d8GhostArmosDrop", }, "Sword & Shield Dungeon (B1F): Southeast Lava Chest": { - "patcher_name": "d8 SE lava chest", "region_id": "d8 SE lava chest", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA6B + "flag_byte": 0xca6b, + "room": 0x056b, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (B1F): Southwest Lava Chest": { - "patcher_name": "d8 SW lava chest", "region_id": "d8 SW lava chest", "vanilla_item": "Bombs (10)", "dungeon": 8, - "flag_byte": 0xCA6A + "flag_byte": 0xca6a, + "room": 0x056a, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (1F): Chest in Sparks & Pots Room": { - "patcher_name": "d8 spark chest", "region_id": "d8 spark chest", "vanilla_item": "Small Key (Sword & Shield Dungeon)", "dungeon": 8, - "flag_byte": 0xCA8A + "flag_byte": 0xca8a, + "room": 0x058a, + "collect": COLLECT_CHEST, + "map_tile": 0x04, }, "Sword & Shield Dungeon (B1F): Boss Reward": { - "patcher_name": "d8 boss", "region_id": "d8 boss", "vanilla_item": "Heart Container", "dungeon": 8, - "flag_byte": 0xCA64 + "flag_byte": 0xca64, + "room": 0x0564, + "collect": COLLECT_POOF, + "map_tile": 0x04, + "symbolic_name": "d8Boss" }, "Horon Village: Item Behind Small Tree": { - "patcher_name": "horon heart piece", "region_id": "horon heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC7D8 + "flag_byte": 0xc7d8, + "room": 0x00d8, + "collect": COLLECT_TOUCH, + "symbolic_name": "horonHeartPiece", }, "Woods of Winter: Item Below Lake": { - "patcher_name": "woods of winter heart piece", "region_id": "woods of winter heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC7AF + "flag_byte": 0xc7af, + "room": 0x00af, + "collect": COLLECT_TOUCH, + "symbolic_name": "woodsOfWinterHeartPiece", }, "Mt. Cucco: Item on Ledge": { - "patcher_name": "mt. cucco heart piece", "region_id": "mt. cucco heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC72D + "flag_byte": 0xc72d, + "room": 0x002d, + "collect": COLLECT_TOUCH, + "symbolic_name": "mtCuccoHeartPiece", }, "Eastern Suburbs: Item in Windmill Cave": { - "patcher_name": "windmill heart piece", "region_id": "windmill heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xCAB2 + "flag_byte": 0xcab2, + "room": 0x05b2, + "collect": COLLECT_TOUCH, + "map_tile": 0xea, + "symbolic_name": "windmillHeartPiece", }, "Western Coast: Item in Graveyard": { - "patcher_name": "graveyard heart piece", "region_id": "graveyard heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC7D1 + "flag_byte": 0xc7d1, + "room": 0x00d1, + "collect": COLLECT_TOUCH, + "symbolic_name": "graveyardHeartPiece", }, "Spool Swamp: Item Amidst Currents in Spring": { - "patcher_name": "spool swamp heart piece", "region_id": "spool swamp heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xC7B1 + "flag_byte": 0xc7b1, + "room": 0x00b1, + "collect": COLLECT_TOUCH, + "symbolic_name": "spoolSwampHeartPiece", }, "Temple Remains: Item in Cave Behind Rockslide": { - "patcher_name": "temple remains heart piece", "region_id": "temple remains heart piece", "vanilla_item": "Piece of Heart", - "flag_byte": 0xCAC7 + "flag_byte": 0xcac7, + "room": 0x05c7, + "collect": COLLECT_TOUCH, + "map_tile": 0x15, + "symbolic_name": "templeRemainsHeartPiece", }, "Horon Village: Item Behind Cracked Wall in Mayor's House": { - "patcher_name": "mayor's house secret room", "region_id": "mayor's house secret room", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC887 + "flag_byte": 0xc887, + "room": 0x0387, + "collect": COLLECT_TOUCH, + "map_tile": 0xc8, + "symbolic_name": "mayorsHouseSecretRoom", }, "Subrosia: Item in House Above Strange Brothers Portal": { - "patcher_name": "subrosian house", "region_id": "subrosian house", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC8A1 + "flag_byte": 0xc8a1, + "room": 0x03a1, + "collect": COLLECT_TOUCH, + "map_tile": 0x1e, + "symbolic_name": "subrosianHouse", }, "Subrosia: Item in Basement to Tower of Spring": { - "patcher_name": "subrosian 2d cave", "region_id": "subrosian 2d cave", "vanilla_item": "Gasha Seed", - "flag_byte": 0xCAE3 + "flag_byte": 0xcae3, + "room": 0x07e3, + "collect": COLLECT_TOUCH, + "map_tile": 0x1e, + "symbolic_name": "subrosian2dCave", }, "Horon Village: Mayor's Gift": { - "patcher_name": "mayor's gift", "region_id": "mayor's gift", "vanilla_item": "Gasha Seed", - "flag_byte": 0xC886 + "flag_byte": 0xc886, + "room": 0x0386, + "collect": COLLECT_TOUCH, + "map_tile": 0xc8, + "symbolic_name": "mayorGift", }, "Horon Village: Vasu's Gift": { - "patcher_name": "vasu's gift", "region_id": "vasu's gift", "vanilla_item": "Friendship Ring", - "flag_byte": 0xC891 + "flag_byte": 0xc891, + "room": 0x0391, + "collect": COLLECT_TOUCH, + "map_tile": 0xe8, + "symbolic_name": "vasuGift" }, "Goron Mountain: Lonely Goron's Gift": { - "patcher_name": "goron's gift", "region_id": "goron's gift", "vanilla_item": "Biggoron's Sword", # Ring Box doesn't really exist anymore - "flag_byte": 0xCAC5 + "flag_byte": 0xcac5, + "room": 0x05c5, + "collect": COLLECT_TOUCH, + "map_tile": 0x19, + "symbolic_name": "goronGift", }, "Horon Village: Dr. Left Reward": { - "patcher_name": "dr. left reward", "region_id": "dr. left reward", "vanilla_item": "Cuccodex", - "flag_byte": 0xC8A4 + "flag_byte": 0xc8a4, + "room": 0x03a4, + "collect": COLLECT_TOUCH, + "map_tile": 0xf9, + "symbolic_name": "drLeftReward", }, "North Horon: Malon Trade": { - "patcher_name": "malon trade", "region_id": "malon trade", "vanilla_item": "Lon Lon Egg", - "flag_byte": 0xC880 + "flag_byte": 0xc880, + "room": 0x0380, + "collect": COLLECT_TOUCH, + "map_tile": 0x88, + "symbolic_name": "malonTrade", }, "Maple Trade": { - "patcher_name": "maple trade", "region_id": "maple trade", "vanilla_item": "Ghastly Doll", - "flag_byte": 0xC640, - "bit_mask": 0x08 + "flag_byte": 0xc640, + "bit_mask": 0x08, + "room": [], + "symbolic_name": "mapleTrade", }, "Holodrum Plain: Mrs. Ruul Trade": { - "patcher_name": "mrs. ruul trade", "region_id": "mrs. ruul trade", "vanilla_item": "Iron Pot", - "flag_byte": 0xC8B3 + "flag_byte": 0xc8b3, + "room": 0x03b3, + "collect": COLLECT_TOUCH, + "map_tile": 0xa3, + "symbolic_name": "mrsRuulTrade", }, "Subrosia: Subrosian Chef Trade": { - "patcher_name": "subrosian chef trade", "region_id": "subrosian chef trade", "vanilla_item": "Lava Soup", - "flag_byte": 0xC88F + "flag_byte": 0xc88f, + "room": 0x038f, + "collect": COLLECT_TOUCH, + "map_tile": 0x9a, + "symbolic_name": "subrosianChefTrade", }, "Goron Mountain: Biggoron Trade": { - "patcher_name": "biggoron trade", "region_id": "biggoron trade", "vanilla_item": "Goron Vase", - "flag_byte": 0xC708 + "flag_byte": 0xc708, + "room": 0x0008, + "collect": COLLECT_TOUCH, + "symbolic_name": "biggoronTrade", }, "Sunken City: Ingo Trade": { - "patcher_name": "ingo trade", "region_id": "ingo trade", "vanilla_item": "Fish", - "flag_byte": 0xC899 + "flag_byte": 0xc899, + "room": 0x0399, + "collect": COLLECT_TOUCH, + "map_tile": 0x4d, + "symbolic_name": "ingoTrade", }, "North Horon: Yelling Old Man Trade": { - "patcher_name": "old man trade", "region_id": "old man trade", "vanilla_item": "Megaphone", - "flag_byte": 0xC7B7 + "flag_byte": 0xc7b7, + "room": 0x00b7, + "collect": COLLECT_TOUCH, + "symbolic_name": "yellingOldManTrade", }, "Mt. Cucco: Talon Trade": { - "patcher_name": "talon trade", "region_id": "talon trade", "vanilla_item": "Mushroom", - "flag_byte": 0xCAB6, - "bit_mask": 0x40 + "flag_byte": 0xcab6, + "bit_mask": 0x40, + "room": 0x05b6, + "map_tile": 0x1b, + "symbolic_name": "talonTrade", }, "Sunken City: Syrup Trade": { - "patcher_name": "syrup trade", "region_id": "syrup trade", "vanilla_item": "Wooden Bird", - "flag_byte": 0xC89C + "flag_byte": 0xc89c, + "room": 0x039c, + "collect": COLLECT_TOUCH, + "map_tile": 0x5e, + "symbolic_name": "syrupTrade", }, "Horon Village: Tick Tock Trade": { - "patcher_name": "tick tock trade", "region_id": "tick tock trade", "vanilla_item": "Engine Grease", - "flag_byte": 0xC883 + "flag_byte": 0xc883, + "room": 0x0383, + "collect": COLLECT_TOUCH, + "map_tile": 0xd7, + "symbolic_name": "tickTockTrade", }, "Eastern Suburbs: Guru-Guru Trade": { - "patcher_name": "guru-guru trade", "region_id": "guru-guru trade", "vanilla_item": "Phonograph", - "flag_byte": 0xC7DA + "flag_byte": 0xc7da, + "room": 0x00da, + "collect": COLLECT_TOUCH, + "symbolic_name": "guruguruTrade", }, "Subrosia: Buried Bomb Flower": { - "patcher_name": "bomb flower", "region_id": "subrosian buried bomb flower", "vanilla_item": "Bomb Flower", - "flag_byte": 0xC869 + "flag_byte": 0xc869, + "room": 0x0169, + "collect": COLLECT_TOUCH, + "map_tile": 0xb9, + "symbolic_name": "bombFlower", }, "Subrosia: Sign-Loving Guy Reward": { - "patcher_name": "subrosian sign loving guy", "region_id": "subrosian sign guy", "vanilla_item": "Sign Ring", - "flag_byte": 0xC8A9 + "flag_byte": 0xc8a9, + "room": 0x03a9, + "collect": COLLECT_TOUCH, + "map_tile": 0xb9, + "symbolic_name": "subrosianSignLovingGuy", }, # Maku seed is 0xC85D "Horon Village: Old Man": { - "patcher_name": "old man, horon village", "region_id": "old man in horon", - "flag_byte": 0xCA05, + "flag_byte": 0xca05, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0505, + "collect": COLLECT_TOUCH, + "map_tile": 0xf9, + "symbolic_name": "oldManHoronVillage", }, "North Horon: Old Man Near D1": { - "patcher_name": "old man, near d1", "region_id": "old man near d1", - "flag_byte": 0xCA03, + "flag_byte": 0xca03, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0503, + "collect": COLLECT_TOUCH, + "map_tile": 0x97, + "symbolic_name": "oldManNearD1", }, "Holodrum Plain: Old Man Near Blaino's Gym": { - "patcher_name": "old man, near blaino", "region_id": "old man near blaino", - "flag_byte": 0xCA02, + "flag_byte": 0xca02, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0502, + "collect": COLLECT_TOUCH, + "map_tile": 0x76, + "symbolic_name": "oldManNearBlaino", }, "Goron Mountain: Old Man": { - "patcher_name": "old man, goron mountain", "region_id": "old man in goron mountain", - "flag_byte": 0xCA01, + "flag_byte": 0xca01, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0501, + "collect": COLLECT_TOUCH, + "map_tile": 0x28, + "symbolic_name": "oldManGoronMountain", }, "Western Coast: Old Man": { - "patcher_name": "old man, western coast", "region_id": "old man near western coast house", - "flag_byte": 0xCA04, + "flag_byte": 0xca04, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0504, + "collect": COLLECT_TOUCH, + "map_tile": 0xd2, + "symbolic_name": "oldManWesternCoast", }, "Woods of Winter: Old Man": { - "patcher_name": "old man, woods of winter", "region_id": "old man near holly's house", - "flag_byte": 0xCA07, + "flag_byte": 0xca07, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0507, + "collect": COLLECT_TOUCH, + "map_tile": 0x8f, + "symbolic_name": "oldManWoodsOfWinter", }, "Holodrum Plain: Old Man Near Mrs. Ruul's House": { - "patcher_name": "old man, ghastly stump", "region_id": "old man near mrs. ruul", - "flag_byte": 0xCA08, + "flag_byte": 0xca08, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0508, + "collect": COLLECT_TOUCH, + "map_tile": 0xa4, + "symbolic_name": "oldManGhastlyStump", }, "Tarm Ruins: Old Man Near D6": { - "patcher_name": "old man, tarm ruins", "region_id": "old man near d6", - "flag_byte": 0xCA06, + "flag_byte": 0xca06, "bit_mask": 0x40, - "vanilla_item": "Rupees (100)", - "conditional": True + "vanilla_item": "Filler Item", + "conditional": True, + "room": 0x0506, + "collect": COLLECT_TOUCH, + "map_tile": 0x02, + "symbolic_name": "oldManTarmRuins", }, "North Horon: Golden Beasts Old Man": { - "patcher_name": "golden beasts old man", "region_id": "golden beasts old man", "vanilla_item": "Red Ring", - "flag_byte": 0xCA11, + "flag_byte": 0xca11, + "room": 0x0511, + "collect": COLLECT_TOUCH, + "map_tile": 0xa6, + "symbolic_name": "goldenBeastsOldMan", }, "Horon Village: Seed Tree": { - "patcher_name": "horon village tree", "region_id": "horon village tree", "local": True, - "flag_byte": 0xC7F8, + "flag_byte": 0xc7f8, + "room": 0x00f8, + "collect": COLLECT_TOUCH, + "symbolic_name": "horonVillageSeedTree", }, "Woods of Winter: Seed Tree": { - "patcher_name": "woods of winter tree", "region_id": "woods of winter tree", "local": True, - "flag_byte": 0xC79E, + "flag_byte": 0xc79e, + "room": 0x009e, + "collect": COLLECT_TOUCH, + "symbolic_name": "woodsOfWinterSeedTree", }, "Holodrum Plain: Seed Tree": { - "patcher_name": "north horon tree", "region_id": "north horon tree", "local": True, - "flag_byte": 0xC767, + "flag_byte": 0xc767, + "room": 0x0067, + "collect": COLLECT_TOUCH, + "symbolic_name": "northHoronSeedTree", }, "Spool Swamp: Seed Tree": { - "patcher_name": "spool swamp tree", "region_id": "spool swamp tree", "local": True, - "flag_byte": 0xC772, + "flag_byte": 0xc772, + "room": 0x0072, + "collect": COLLECT_TOUCH, + "symbolic_name": "spoolSwampSeedTree", }, "Sunken City: Seed Tree": { - "patcher_name": "sunken city tree", "region_id": "sunken city tree", "local": True, - "flag_byte": 0xC75F, + "flag_byte": 0xc75f, + "room": 0x005f, + "collect": COLLECT_TOUCH, + "symbolic_name": "sunkenCitySeedTree", }, "Tarm Ruins: Seed Tree": { - "patcher_name": "tarm ruins tree", "region_id": "tarm ruins tree", "local": True, - "flag_byte": 0xC710, + "flag_byte": 0xc710, + "room": 0x0010, + "collect": COLLECT_TOUCH, + "symbolic_name": "tarmRuinsSeedTree", }, "Gnarled Root Dungeon: Essence": { "region_id": "d1 boss", - "flag_byte": 0xC913, + "flag_byte": 0xc913, "vanilla_item": "Fertile Soil", - "randomized": False + "essence": True, + "symbolic_name": "essenceD1", }, "Snake's Remains: Essence": { "region_id": "d2 boss", - "flag_byte": 0xC92C, + "flag_byte": 0xc92c, "vanilla_item": "Gift of Time", - "randomized": False + "essence": True, + "symbolic_name": "essenceD2", }, "Poison Moth's Lair: Essence": { "region_id": "d3 boss", - "flag_byte": 0xC940, + "flag_byte": 0xc940, "vanilla_item": "Bright Sun", - "randomized": False + "essence": True, + "symbolic_name": "essenceD3", }, "Dancing Dragon Dungeon: Essence": { "region_id": "d4 boss", - "flag_byte": 0xC960, + "flag_byte": 0xc960, "vanilla_item": "Soothing Rain", - "randomized": False + "essence": True, + "symbolic_name": "essenceD4", }, "Unicorn's Cave: Essence": { "region_id": "d5 boss", - "flag_byte": 0xC988, + "flag_byte": 0xc988, "vanilla_item": "Nurturing Warmth", - "randomized": False + "essence": True, + "symbolic_name": "essenceD5", }, "Ancient Ruins: Essence": { "region_id": "d6 boss", - "flag_byte": 0xC898, + "flag_byte": 0xc898, "vanilla_item": "Blowing Wind", - "randomized": False + "essence": True, + "symbolic_name": "essenceD6", }, "Explorer's Crypt: Essence": { "region_id": "d7 boss", - "flag_byte": 0xCA4F, + "flag_byte": 0xca4f, "vanilla_item": "Seed of Life", - "randomized": False + "essence": True, + "symbolic_name": "essenceD7", }, "Sword & Shield Dungeon: Essence": { "region_id": "d8 boss", - "flag_byte": 0xCA5F, + "flag_byte": 0xca5f, "vanilla_item": "Changing Seasons", - "randomized": False + "essence": True, + "symbolic_name": "essenceD8", }, "Horon Village: Item Inside Maku Tree (3+ Essences)": { - "patcher_name": "maku tree, 3 essences", "region_id": "maku tree, 3 essences", - "flag_byte": 0xC9E9, - "vanilla_item": "Gasha Seed" + "flag_byte": 0xc9e9, + "vanilla_item": "Gasha Seed", + "room": 0x04e9, + "collect": COLLECT_TOUCH, + "map_tile": 0xc9, + "symbolic_name": "makuTree3Essences", }, "Horon Village: Item Inside Maku Tree (5+ Essences)": { - "patcher_name": "maku tree, 5 essences", "region_id": "maku tree, 5 essences", - "flag_byte": 0xC9EA, - "vanilla_item": "Gasha Seed" + "flag_byte": 0xc9ea, + "vanilla_item": "Gasha Seed", + "room": 0x04ea, + "collect": COLLECT_TOUCH, + "map_tile": 0xc9, + "symbolic_name": "makuTree5Essences", }, "Horon Village: Item Inside Maku Tree (7+ Essences)": { - "patcher_name": "maku tree, 7 essences", "region_id": "maku tree, 7 essences", - "flag_byte": 0xC9EE, - "vanilla_item": "Gasha Seed" + "flag_byte": 0xc9ee, + "vanilla_item": "Gasha Seed", + "room": 0x04ee, + "collect": COLLECT_TOUCH, + "map_tile": 0xc9, + "symbolic_name": "makuTree7Essences", }, "Subrosia: Strange Brothers' Backyard Treasure": { - "patcher_name": "subrosia hide and seek", "region_id": "subrosia hide and seek", "vanilla_item": "Ore Chunks (50)", - "flag_byte": 0xC860, + "flag_byte": 0xc860, + "room": 0x0160, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaHideAndSeek", }, "Subrosia: Hot Bath Digging Spot": { - "patcher_name": "subrosia bath ore digging spot", "region_id": "subrosia bath digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC806, + "flag_byte": 0xc806, + "room": 0x0106, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaBathOreDiggingSpot", }, "Subrosia: Market Portal Digging Spot": { - "patcher_name": "subrosia market portal ore digging spot", "region_id": "subrosia market digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC857, + "flag_byte": 0xc857, + "room": 0x0157, + "collect": COLLECT_DIG, + "map_tile": 0xb0, + "symbolic_name": "subrosiaMarketPortalOreDiggingSpot", }, "Subrosia: Hard-Working Subrosian Digging Spot": { - "patcher_name": "subrosia hard-working ore digging spot", "region_id": "subrosia market digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC847, + "flag_byte": 0xc847, + "room": 0x0147, + "collect": COLLECT_DIG, + "map_tile": 0xb0, + "symbolic_name": "subrosiaWorkerOreDiggingSpot", }, "Subrosia: Temple of Seasons Digging Spot": { - "patcher_name": "subrosia temple ore digging spot", "region_id": "subrosia temple digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC83A, + "flag_byte": 0xc83a, + "room": 0x013a, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaTempleOreDiggingSpot", }, "Subrosia: Northern Volcanoes Digging Spot": { - "patcher_name": "subrosia northern volcanoes ore digging spot", "region_id": "subrosia temple digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC807, + "flag_byte": 0xc807, + "room": 0x0107, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaNorthernVolcanoesOreDiggingSpot", }, "Subrosia: D8 Portal Digging Spot": { - "patcher_name": "subrosia d8 portal ore digging spot", "region_id": "subrosia bridge digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC820, + "flag_byte": 0xc820, + "room": 0x0120, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaD8PortalOreDiggingSpot", }, "Subrosia: Western Volcanoes Digging Spot": { - "patcher_name": "subrosia western volcanoes ore digging spot", "region_id": "subrosia bridge digging spot", "vanilla_item": "Ore Chunks (50)", "conditional": True, - "flag_byte": 0xC842, + "flag_byte": 0xc842, + "room": 0x0142, + "collect": COLLECT_DIG, + "map_tile": 0x1e, + "symbolic_name": "subrosiaWesternVolcanoesOreDiggingSpot", + }, + "Gasha Nut #1": { + "region_id": "gasha tree 1", + "vanilla_item": "Piece of Heart", + "conditional": True, + "room": [], + }, + "Gasha Nut #2": { + "region_id": "gasha tree 2", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #3": { + "region_id": "gasha tree 3", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #4": { + "region_id": "gasha tree 4", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #5": { + "region_id": "gasha tree 5", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #6": { + "region_id": "gasha tree 6", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #7": { + "region_id": "gasha tree 7", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #8": { + "region_id": "gasha tree 8", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #9": { + "region_id": "gasha tree 9", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #10": { + "region_id": "gasha tree 10", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #11": { + "region_id": "gasha tree 11", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #12": { + "region_id": "gasha tree 12", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #13": { + "region_id": "gasha tree 13", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #14": { + "region_id": "gasha tree 14", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #15": { + "region_id": "gasha tree 15", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], + }, + "Gasha Nut #16": { + "region_id": "gasha tree 16", + "vanilla_item": "Filler Item", + "conditional": True, + "room": [], }, } diff --git a/worlds/tloz_oos/data/Regions.py b/worlds/tloz_oos/data/Regions.py index 8303f69a93ef..74ecd9613fb2 100644 --- a/worlds/tloz_oos/data/Regions.py +++ b/worlds/tloz_oos/data/Regions.py @@ -63,7 +63,7 @@ "spool swamp south (summer)", "spool swamp south (spring)", "spool swamp south (autumn)", - "spool swamp south gasha spot", + "spool swamp south near gasha spot", "spool swamp portal", "spool swamp cave", "spool swamp heart piece", @@ -81,6 +81,7 @@ "cave north of D1", "spool swamp tree", "floodgate keeper's house", + "floodgate keyhole", "spool stump", "dry swamp", "d3 entrance", @@ -142,26 +143,27 @@ "temple remains lower stump", "temple remains upper stump", "temple remains lower portal", + "temple remains lower portal access", "temple remains upper portal", "temple remains heart piece", "maku seed", "d9 entrance", "onox beaten", "ganon beaten", - "subrosia portal 1", + "volcanoes east portal", "subrosia temple sector", - "subrosia portal 2", + "subrosia market portal", "subrosia market sector", - "subrosia portal 3", + "strange brothers portal", "subrosia hide and seek sector", - "subrosia portal 4", + "house of pirates portal", "subrosia pirates sector", - "subrosia portal 5", + "great furnace portal", "subrosia furnace sector", - "subrosia portal 6", + "volcanoes west portal", "subrosia volcano sector", "bomb temple remains", - "subrosia portal 7", + "d8 entrance portal", "d8 entrance", "subrosia east junction", "subrosia bridge sector", @@ -363,4 +365,38 @@ "subrosia market digging spot", "subrosia temple digging spot", "subrosia bridge digging spot", + + "impa gasha spot", + "horon gasha spot", + "suburbs gasha spot", + "holodrum plain gasha spot", + "holodrum plain island gasha spot", + "spool swamp north gasha spot", + "spool swamp south gasha spot", + "sunken city gasha spot", + "mt cucco gasha spot", + "goron mountain left gasha spot", + "goron mountain right gasha spot", + "eyeglass lake gasha spot", + "tarm ruins gasha spot", + "western coast gasha spot", + "samasa desert gasha spot", + "onox gasha spot", + + "gasha tree 1", + "gasha tree 2", + "gasha tree 3", + "gasha tree 4", + "gasha tree 5", + "gasha tree 6", + "gasha tree 7", + "gasha tree 8", + "gasha tree 9", + "gasha tree 10", + "gasha tree 11", + "gasha tree 12", + "gasha tree 13", + "gasha tree 14", + "gasha tree 15", + "gasha tree 16", ] diff --git a/worlds/tloz_oos/data/__init__.py b/worlds/tloz_oos/data/__init__.py index 0b7069fcb5f9..e0ae85f451d0 100644 --- a/worlds/tloz_oos/data/__init__.py +++ b/worlds/tloz_oos/data/__init__.py @@ -1,2 +1,2 @@ from .Items import BASE_ITEM_ID, ITEMS_DATA -from .Locations import BASE_LOCATION_ID, LOCATIONS_DATA \ No newline at end of file +from .Locations import BASE_LOCATION_ID, LOCATIONS_DATA diff --git a/worlds/tloz_oos/data/logic/DungeonsLogic.py b/worlds/tloz_oos/data/logic/DungeonsLogic.py index 91903446f5ea..2a5ff4e41d4f 100644 --- a/worlds/tloz_oos/data/logic/DungeonsLogic.py +++ b/worlds/tloz_oos/data/logic/DungeonsLogic.py @@ -62,7 +62,7 @@ def make_d1_logic(player: int): oos_can_kill_stalfos(state, player) ])], - ["d1 stalfos chest", "d1 goriya chest", False, lambda state: any([ + ["d1 stalfos chest", "d1 goriya chest", False, lambda state: all([ oos_can_use_ember_seeds(state, player, True), oos_can_kill_normal_enemy(state, player, True) ])], @@ -107,11 +107,14 @@ def make_d2_logic(player: int): ["d2 torch room", "d2 rope drop", False, lambda state: oos_can_kill_normal_enemy(state, player)], ["d2 torch room", "d2 arrow room", False, lambda state: oos_can_use_ember_seeds(state, player, True)], - ["d2 arrow room", "d2 torch room", False, lambda state: any([ - # Backwards path is one-way if we don't have ember seeds, so ensure we have a way to warp out in case - # something goes wrong - oos_can_use_ember_seeds(state, player, True), - oos_can_warp(state, player) + ["d2 arrow room", "d2 torch room", False, lambda state: all([ + oos_can_kill_normal_enemy(state, player), + any([ + # Backwards path is one-way if we don't have ember seeds, so ensure we have a way to warp out in case + # something goes wrong + oos_can_use_ember_seeds(state, player, True), + oos_can_warp(state, player) + ]) ])], ["d2 arrow room", "d2 rupee room", False, lambda state: oos_has_bombs(state, player)], ["d2 arrow room", "d2 rope chest", False, lambda state: oos_can_kill_normal_enemy(state, player)], diff --git a/worlds/tloz_oos/data/logic/LogicPredicates.py b/worlds/tloz_oos/data/logic/LogicPredicates.py index b28fdba90d11..d69ce5248333 100644 --- a/worlds/tloz_oos/data/logic/LogicPredicates.py +++ b/worlds/tloz_oos/data/logic/LogicPredicates.py @@ -1,6 +1,6 @@ from BaseClasses import CollectionState from Options import Accessibility -from worlds.tloz_oos.data.Constants import DUNGEON_NAMES, SEASON_ITEMS, ESSENCES, JEWELS +from ..Constants import * # Items predicates ############################################################ @@ -69,19 +69,19 @@ def oos_has_season(state: CollectionState, player: int, season: str): def oos_has_summer(state: CollectionState, player: int): - return state.has(SEASON_ITEMS["summer"], player) + return state.has(SEASON_ITEMS[SEASON_SUMMER], player) def oos_has_spring(state: CollectionState, player: int): - return state.has(SEASON_ITEMS["spring"], player) + return state.has(SEASON_ITEMS[SEASON_SPRING], player) def oos_has_winter(state: CollectionState, player: int): - return state.has(SEASON_ITEMS["winter"], player) + return state.has(SEASON_ITEMS[SEASON_WINTER], player) def oos_has_autumn(state: CollectionState, player: int): - return state.has(SEASON_ITEMS["autumn"], player) + return state.has(SEASON_ITEMS[SEASON_AUTUMN], player) def oos_has_magnet_gloves(state: CollectionState, player: int): @@ -142,7 +142,7 @@ def oos_option_hard_logic(state: CollectionState, player: int): def oos_option_shuffled_dungeons(state: CollectionState, player: int): - return state.multiworld.worlds[player].options.shuffle_dungeons != "vanilla" + return state.multiworld.worlds[player].options.shuffle_dungeons def oos_option_allow_warp_to_start(state: CollectionState, player: int): @@ -201,17 +201,24 @@ def oos_has_required_jewels(state: CollectionState, player: int): def oos_can_reach_lost_woods_pedestal(state: CollectionState, player: int): world = state.multiworld.worlds[player] return all([ - any([ - world.options.lost_woods_item_sequence == "vanilla", - all([ - oos_can_use_ember_seeds(state, player, False), - state.has("Phonograph", player) - ]) - ]), - "winter" not in world.lost_woods_item_sequence or oos_has_winter(state, player), - "spring" not in world.lost_woods_item_sequence or oos_has_spring(state, player), - "summer" not in world.lost_woods_item_sequence or oos_has_summer(state, player), - "autumn" not in world.lost_woods_item_sequence or oos_has_autumn(state, player) + oos_can_use_ember_seeds(state, player, False), + state.has("Phonograph", player), + SEASON_WINTER not in world.lost_woods_item_sequence or oos_has_winter(state, player), + SEASON_SPRING not in world.lost_woods_item_sequence or oos_has_spring(state, player), + SEASON_SUMMER not in world.lost_woods_item_sequence or oos_has_summer(state, player), + SEASON_AUTUMN not in world.lost_woods_item_sequence or oos_has_autumn(state, player) + ]) + + +def oos_can_complete_lost_woods_main_sequence(state: CollectionState, player: int): + world = state.multiworld.worlds[player] + return all([ + oos_can_break_mushroom(state, player, False), + oos_has_shield(state, player), + SEASON_WINTER not in world.lost_woods_main_sequence or oos_has_winter(state, player), + SEASON_SPRING not in world.lost_woods_main_sequence or oos_has_spring(state, player), + SEASON_SUMMER not in world.lost_woods_main_sequence or oos_has_summer(state, player), + SEASON_AUTUMN not in world.lost_woods_main_sequence or oos_has_autumn(state, player) ]) @@ -261,10 +268,16 @@ def oos_can_farm_rupees(state: CollectionState, player: int): def oos_has_ore_chunks(state: CollectionState, player: int, amount: int): + world = state.multiworld.worlds[player] + if not world.options.shuffle_golden_ore_spots: + return oos_can_farm_ore_chunks(state, player) + if not oos_can_farm_ore_chunks(state, player): return False ore_chunks = 0 + ore_chunks += state.count("Ore Chunks (10)", player) * 10 + ore_chunks += state.count("Ore Chunks (25)", player) * 25 ore_chunks += state.count("Ore Chunks (50)", player) * 50 return ore_chunks >= amount @@ -274,8 +287,11 @@ def oos_can_farm_ore_chunks(state: CollectionState, player: int): oos_has_shovel(state, player), all([ oos_option_medium_logic(state, player), - oos_has_magic_boomerang(state, player), - oos_has_sword(state, player) + any([ + oos_has_magic_boomerang(state, player), + oos_has_sword(state, player), + oos_has_bracelet(state, player) + ]) ]), all([ oos_option_hard_logic(state, player), @@ -409,6 +425,7 @@ def oos_can_jump_5_wide_liquid(state: CollectionState, player: int): def oos_can_jump_6_wide_liquid(state: CollectionState, player: int): return all([ + oos_option_medium_logic(state, player), oos_has_cape(state, player), oos_can_use_pegasus_seeds(state, player), ]) @@ -659,6 +676,16 @@ def oos_can_harvest_tree(state: CollectionState, player: int, can_use_companion: ]) +def oos_can_harvest_gasha(state: CollectionState, player: int, count: int): + reachable_soils = [state.has(f"_reached_{region_name}", player) for region_name in GASHA_SPOT_REGIONS] + return all([ + reachable_soils.count(True) >= count, # Enough soils are reachable + state.has("Gasha Seed", player, count), # Enough seeds to plant + oos_can_kill_normal_enemy(state, player), # Can increase kill count to make the tree grow + oos_has_sword(state, player) or oos_has_fools_ore(state, player) # Can actually harvest the nut + ]) + + def oos_can_push_enemy(state: CollectionState, player: int): return any([ oos_has_rod(state, player), @@ -909,8 +936,8 @@ def oos_season_in_temple_remains(state: CollectionState, player: int, season: st return oos_has_season(state, player, season) and state.has("_reached_remains_stump", player) -def oos_season_in_north_horon(state: CollectionState, player: int, season: str): - if oos_get_default_season(state, player, "NORTH_HORON") == season: +def oos_season_in_holodrum_plain(state: CollectionState, player: int, season: str): + if oos_get_default_season(state, player, "HOLODRUM_PLAIN") == season: return True return oos_has_season(state, player, season) and state.has("_reached_ghastly_stump", player) @@ -962,7 +989,7 @@ def oos_season_in_tarm_ruins(state: CollectionState, player: int, season: str): def oos_season_in_horon_village(state: CollectionState, player: int, season: str): # With vanilla behavior, you can randomly have any season inside Horon, making any season virtually accessible - if state.multiworld.worlds[player].options.horon_village_season == "vanilla": + if not state.multiworld.worlds[player].options.normalize_horon_village_season: return True if oos_get_default_season(state, player, "HORON_VILLAGE") == season: return True diff --git a/worlds/tloz_oos/data/logic/OverworldLogic.py b/worlds/tloz_oos/data/logic/OverworldLogic.py index 782f958c5545..402f51ce2733 100644 --- a/worlds/tloz_oos/data/logic/OverworldLogic.py +++ b/worlds/tloz_oos/data/logic/OverworldLogic.py @@ -1,4 +1,4 @@ -from worlds.tloz_oos.data.logic.LogicPredicates import * +from .LogicPredicates import * def make_holodrum_logic(player: int): @@ -24,12 +24,12 @@ def make_holodrum_logic(player: int): oos_has_bombs(state, player), any([ oos_can_swim(state, player, False), - oos_season_in_horon_village(state, player, "winter"), + oos_season_in_horon_village(state, player, SEASON_WINTER), oos_can_jump_2_wide_liquid(state, player) ]) ])], ["horon village", "horon village SW chest", False, lambda state: all([ - oos_season_in_horon_village(state, player, "autumn"), + oos_season_in_horon_village(state, player, SEASON_AUTUMN), oos_can_break_mushroom(state, player, True) ])], @@ -52,11 +52,11 @@ def make_holodrum_logic(player: int): ["horon village", "horon village tree", False, lambda state: oos_can_harvest_tree(state, player, True)], - ["horon village", "horon shop", False, lambda state: oos_has_rupees(state, player, 200)], - ["horon village", "advance shop", False, lambda state: oos_has_rupees(state, player, 400)], + ["horon village", "horon shop", False, lambda state: oos_has_rupees(state, player, 150)], + ["horon village", "advance shop", False, lambda state: oos_has_rupees(state, player, 300)], ["horon village", "member's shop", False, lambda state: all([ state.has("Member's Card", player), - oos_has_rupees(state, player, 600) + oos_has_rupees(state, player, 450) ])], # WESTERN COAST ############################################################################################## @@ -85,17 +85,17 @@ def make_holodrum_logic(player: int): ["western coast after ship", "graveyard (winter)", False, lambda state: all([ oos_can_jump_3_wide_pit(state, player), - oos_season_in_western_coast(state, player, "winter") + oos_season_in_western_coast(state, player, SEASON_WINTER) ])], ["western coast after ship", "graveyard (autumn)", False, lambda state: all([ oos_can_jump_3_wide_pit(state, player), - oos_season_in_western_coast(state, player, "autumn") + oos_season_in_western_coast(state, player, SEASON_AUTUMN) ])], ["western coast after ship", "graveyard (summer or spring)", False, lambda state: any([ oos_can_jump_3_wide_pit(state, player), - oos_season_in_western_coast(state, player, "summer") + oos_season_in_western_coast(state, player, SEASON_SUMMER) ])], ["graveyard (winter)", "d7 entrance", False, lambda state: oos_can_remove_snow(state, player, False)], @@ -103,11 +103,11 @@ def make_holodrum_logic(player: int): ["graveyard (summer or spring)", "d7 entrance", False, None], ["d7 entrance", "graveyard (winter)", False, lambda state: \ - oos_get_default_season(state, player, "WESTERN_COAST") == "winter"], + oos_get_default_season(state, player, "WESTERN_COAST") == SEASON_WINTER], ["d7 entrance", "graveyard (autumn)", False, lambda state: \ - oos_get_default_season(state, player, "WESTERN_COAST") == "autumn"], + oos_get_default_season(state, player, "WESTERN_COAST") == SEASON_AUTUMN], ["d7 entrance", "graveyard (summer or spring)", False, lambda state: \ - oos_get_default_season(state, player, "WESTERN_COAST") in ["summer", "spring"]], + oos_get_default_season(state, player, "WESTERN_COAST") in [SEASON_SUMMER, SEASON_SPRING]], ["graveyard (autumn)", "graveyard heart piece", False, lambda state: oos_can_break_mushroom(state, player, False)], @@ -115,7 +115,7 @@ def make_holodrum_logic(player: int): ["horon village", "suburbs", True, lambda state: oos_can_use_ember_seeds(state, player, False)], - ["suburbs", "windmill heart piece", False, lambda state: oos_season_in_eastern_suburbs(state, player, "winter")], + ["suburbs", "windmill heart piece", False, lambda state: oos_season_in_eastern_suburbs(state, player, SEASON_WINTER)], ["suburbs", "guru-guru trade", False, lambda state: any([ state.has("Engine Grease", player), oos_self_locking_item(state, player, "guru-guru trade", "Engine Grease") @@ -123,7 +123,7 @@ def make_holodrum_logic(player: int): ["suburbs", "eastern suburbs spring cave", False, lambda state: all([ oos_has_bracelet(state, player), - oos_season_in_eastern_suburbs(state, player, "spring"), + oos_season_in_eastern_suburbs(state, player, SEASON_SPRING), any([ oos_has_magnet_gloves(state, player), oos_can_jump_3_wide_pit(state, player) @@ -138,17 +138,17 @@ def make_holodrum_logic(player: int): oos_can_jump_1_wide_liquid(state, player, True) ])], ["suburbs", "suburbs fairy fountain (winter)", True, lambda state: any([ - oos_season_in_eastern_suburbs(state, player, "winter") + oos_season_in_eastern_suburbs(state, player, SEASON_WINTER) ])], ["suburbs fairy fountain (winter)", "suburbs fairy fountain", False, lambda state: \ - oos_can_remove_season(state, player, "winter")], + oos_can_remove_season(state, player, SEASON_WINTER)], ["suburbs fairy fountain", "suburbs fairy fountain (winter)", False, lambda state: \ oos_has_winter(state, player)], ["suburbs fairy fountain", "sunken city", False, lambda state: \ - oos_season_in_eastern_suburbs(state, player, "spring")], + oos_season_in_eastern_suburbs(state, player, SEASON_SPRING)], ["sunken city", "suburbs fairy fountain", False, lambda state: any([ - oos_season_in_eastern_suburbs(state, player, "spring"), + oos_season_in_eastern_suburbs(state, player, SEASON_SPRING), oos_can_warp(state, player) ])], @@ -156,19 +156,19 @@ def make_holodrum_logic(player: int): ["suburbs fairy fountain (winter)", "moblin road", False, lambda state: None], ["moblin road", "suburbs fairy fountain (winter)", False, lambda state: \ - oos_season_in_eastern_suburbs(state, player, "winter")], + oos_season_in_eastern_suburbs(state, player, SEASON_WINTER)], ["sunken city", "moblin road", False, lambda state: all([ oos_has_flippers(state, player), any([ - oos_get_default_season(state, player, "SUNKEN_CITY") != "winter", - oos_can_remove_season(state, player, "winter") + oos_get_default_season(state, player, "SUNKEN_CITY") != SEASON_WINTER, + oos_can_remove_season(state, player, SEASON_WINTER) ]), any([ oos_can_warp(state, player), all([ # We need both seasons to be able to climb back up - oos_season_in_eastern_suburbs(state, player, "winter"), + oos_season_in_eastern_suburbs(state, player, SEASON_WINTER), oos_has_spring(state, player) ]) ]) @@ -178,8 +178,8 @@ def make_holodrum_logic(player: int): oos_can_remove_rockslide(state, player, True), oos_can_break_bush(state, player, False), any([ - oos_get_default_season(state, player, "WOODS_OF_WINTER") != "winter", - oos_can_remove_season(state, player, "winter") + oos_get_default_season(state, player, "WOODS_OF_WINTER") != SEASON_WINTER, + oos_can_remove_season(state, player, SEASON_WINTER) ]) ])], @@ -189,7 +189,7 @@ def make_holodrum_logic(player: int): ])], ["moblin road", "holly's house", False, lambda state: \ - oos_season_in_woods_of_winter(state, player, "winter")], + oos_season_in_woods_of_winter(state, player, SEASON_WINTER)], ["moblin road", "old man near holly's house", False, lambda state: oos_can_use_ember_seeds(state, player, False)], @@ -208,7 +208,7 @@ def make_holodrum_logic(player: int): ["central woods of winter", "woods of winter tree", False, lambda state: oos_can_harvest_tree(state, player, True)], ["central woods of winter", "d2 entrance", True, lambda state: oos_can_break_bush(state, player, True)], ["central woods of winter", "cave outside D2", False, lambda state: all([ - oos_season_in_central_woods_of_winter(state, player, "autumn"), + oos_season_in_central_woods_of_winter(state, player, SEASON_AUTUMN), oos_can_break_mushroom(state, player, True), any([ oos_can_jump_4_wide_pit(state, player), @@ -226,7 +226,7 @@ def make_holodrum_logic(player: int): ["horon village", "eyeglass lake, across bridge", False, lambda state: any([ oos_can_jump_4_wide_pit(state, player), all([ - oos_season_in_eyeglass_lake(state, player, "autumn"), + oos_season_in_eyeglass_lake(state, player, SEASON_AUTUMN), oos_has_feather(state, player) ]) ])], @@ -242,14 +242,14 @@ def make_holodrum_logic(player: int): ["d1 island", "d1 entrance", True, lambda state: state.has("Gnarled Key", player)], ["d1 island", "golden beasts old man", False, lambda state: all([ - oos_season_in_eyeglass_lake(state, player, "summer"), + oos_season_in_eyeglass_lake(state, player, SEASON_SUMMER), oos_can_beat_required_golden_beasts(state, player) ])], ["d1 stump", "eyeglass lake (default)", True, lambda state: all([ any([ - oos_season_in_eyeglass_lake(state, player, "spring"), - oos_season_in_eyeglass_lake(state, player, "autumn"), + oos_season_in_eyeglass_lake(state, player, SEASON_SPRING), + oos_season_in_eyeglass_lake(state, player, SEASON_AUTUMN), ]), oos_can_jump_1_wide_pit(state, player, True), any([ @@ -263,35 +263,35 @@ def make_holodrum_logic(player: int): ]) ])], ["d1 stump", "eyeglass lake (dry)", True, lambda state: all([ - oos_season_in_eyeglass_lake(state, player, "summer"), + oos_season_in_eyeglass_lake(state, player, SEASON_SUMMER), oos_can_jump_1_wide_pit(state, player, True) ])], ["d1 stump", "eyeglass lake (frozen)", True, lambda state: all([ - oos_season_in_eyeglass_lake(state, player, "winter"), + oos_season_in_eyeglass_lake(state, player, SEASON_WINTER), oos_can_jump_1_wide_pit(state, player, True) ])], ["d5 stump", "eyeglass lake (default)", True, lambda state: all([ any([ - oos_season_in_eyeglass_lake(state, player, "spring"), - oos_season_in_eyeglass_lake(state, player, "autumn"), + oos_season_in_eyeglass_lake(state, player, SEASON_SPRING), + oos_season_in_eyeglass_lake(state, player, SEASON_AUTUMN), ]), oos_can_swim(state, player, True) ])], ["d5 stump", "eyeglass lake (dry)", False, lambda state: all([ - oos_season_in_eyeglass_lake(state, player, "summer"), + oos_season_in_eyeglass_lake(state, player, SEASON_SUMMER), oos_can_swim(state, player, False) ])], ["d5 stump", "eyeglass lake (frozen)", True, - lambda state: oos_season_in_eyeglass_lake(state, player, "winter")], + lambda state: oos_season_in_eyeglass_lake(state, player, SEASON_WINTER)], ["eyeglass lake portal", "eyeglass lake (default)", False, lambda state: all([ - oos_get_default_season(state, player, "EYEGLASS_LAKE") in ["autumn", "spring"], + oos_get_default_season(state, player, "EYEGLASS_LAKE") in [SEASON_AUTUMN, SEASON_SPRING], oos_can_swim(state, player, False) ])], ["eyeglass lake (default)", "eyeglass lake portal", False, None], ["eyeglass lake portal", "eyeglass lake (frozen)", False, lambda state: all([ - oos_get_default_season(state, player, "EYEGLASS_LAKE") == "winter", + oos_get_default_season(state, player, "EYEGLASS_LAKE") == SEASON_WINTER, any([ oos_can_swim(state, player, False), oos_can_jump_5_wide_liquid(state, player) @@ -302,7 +302,7 @@ def make_holodrum_logic(player: int): oos_can_jump_5_wide_liquid(state, player) ])], ["eyeglass lake portal", "eyeglass lake (dry)", False, lambda state: \ - oos_get_default_season(state, player, "EYEGLASS_LAKE") == "summer"], + oos_get_default_season(state, player, "EYEGLASS_LAKE") == SEASON_SUMMER], ["eyeglass lake (dry)", "dry eyeglass lake, west cave", False, lambda state: all([ oos_can_remove_rockslide(state, player, True), @@ -315,7 +315,7 @@ def make_holodrum_logic(player: int): # If we don't have autumn, we need to ensure we were able to reach that node with autumn as default # season without changing to another season which we wouldn't be able to revert back all([ - oos_get_default_season(state, player, "EYEGLASS_LAKE") == "autumn", + oos_get_default_season(state, player, "EYEGLASS_LAKE") == SEASON_AUTUMN, oos_can_swim(state, player, False) ]) ]), @@ -338,7 +338,7 @@ def make_holodrum_logic(player: int): ])], ["d5 entrance", "dry eyeglass lake, east cave", False, lambda state: all([ - oos_get_default_season(state, player, "EYEGLASS_LAKE") == "summer", + oos_get_default_season(state, player, "EYEGLASS_LAKE") == SEASON_SUMMER, oos_has_bracelet(state, player), ])], @@ -347,13 +347,13 @@ def make_holodrum_logic(player: int): ["north horon", "north horon tree", False, lambda state: oos_can_harvest_tree(state, player, True)], ["north horon", "blaino prize", False, lambda state: oos_can_farm_rupees(state, player)], ["north horon", "cave north of D1", False, lambda state: all([ - oos_season_in_north_horon(state, player, "autumn"), + oos_season_in_holodrum_plain(state, player, SEASON_AUTUMN), oos_can_break_mushroom(state, player, True), oos_has_flippers(state, player) ])], ["north horon", "old man near blaino", False, lambda state: all([ any([ - oos_season_in_north_horon(state, player, "summer"), + oos_season_in_holodrum_plain(state, player, SEASON_SUMMER), oos_can_summon_ricky(state, player) ]), oos_can_use_ember_seeds(state, player, False) @@ -370,13 +370,13 @@ def make_holodrum_logic(player: int): ["north horon", "ghastly stump", True, lambda state: any([ oos_can_jump_1_wide_pit(state, player, True), - oos_season_in_north_horon(state, player, "winter") + oos_season_in_holodrum_plain(state, player, SEASON_WINTER) ])], ["spool swamp north", "ghastly stump", False, None], ["ghastly stump", "spool swamp north", False, lambda state: all([ any([ - oos_season_in_north_horon(state, player, "summer"), + oos_season_in_holodrum_plain(state, player, SEASON_SUMMER), oos_can_jump_4_wide_pit(state, player), oos_can_summon_ricky(state, player), oos_can_summon_moosh(state, player) @@ -417,25 +417,25 @@ def make_holodrum_logic(player: int): ])], ["spool swamp north", "spool swamp digging spot", False, lambda state: all([ - oos_season_in_spool_swamp(state, player, "summer"), + oos_season_in_spool_swamp(state, player, SEASON_SUMMER), oos_has_shovel(state, player) ])], - ["floodgate keeper's house", "spool stump", False, lambda state: all([ + ["floodgate keeper's house", "floodgate keyhole", False, lambda state: all([ any([ oos_can_use_pegasus_seeds(state, player), oos_has_flippers(state, player), oos_has_feather(state, player) ]), - oos_has_bracelet(state, player), - state.has("Floodgate Key", player) + oos_has_bracelet(state, player) ])], + ["floodgate keyhole", "spool stump", False, lambda state: state.has("Floodgate Key", player)], - ["spool stump", "d3 entrance", False, lambda state: oos_season_in_spool_swamp(state, player, "summer")], + ["spool stump", "d3 entrance", False, lambda state: oos_season_in_spool_swamp(state, player, SEASON_SUMMER)], ["d3 entrance", "spool stump", False, lambda state: any([ # Jumping down D3 entrance without having a way to put summer is a risky situation, so expect player # to have a way to warp out - oos_season_in_spool_swamp(state, player, "summer"), + oos_season_in_spool_swamp(state, player, SEASON_SUMMER), oos_can_warp(state, player) ])], @@ -446,13 +446,13 @@ def make_holodrum_logic(player: int): oos_can_summon_dimitri(state, player) ])], - ["spool swamp middle", "spool swamp south gasha spot", False, lambda state: oos_can_summon_ricky(state, player)], - ["spool swamp south gasha spot", "spool swamp middle", False, lambda state: any([ + ["spool swamp middle", "spool swamp south near gasha spot", False, lambda state: oos_can_summon_ricky(state, player)], + ["spool swamp south near gasha spot", "spool swamp middle", False, lambda state: any([ oos_has_feather(state, player), oos_can_break_bush(state, player, True) ])], - ["spool swamp south gasha spot", "spool swamp portal", True, lambda state: oos_has_bracelet(state, player)], + ["spool swamp south near gasha spot", "spool swamp portal", True, lambda state: oos_has_bracelet(state, player)], ["spool swamp middle", "spool swamp south", True, lambda state: any([ oos_can_jump_2_wide_pit(state, player), @@ -462,39 +462,39 @@ def make_holodrum_logic(player: int): ])], ["spool swamp south", "spool swamp south (winter)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "winter")], + oos_season_in_spool_swamp(state, player, SEASON_WINTER)], ["spool swamp south", "spool swamp south (spring)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "spring")], + oos_season_in_spool_swamp(state, player, SEASON_SPRING)], ["spool swamp south", "spool swamp south (summer)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "summer")], + oos_season_in_spool_swamp(state, player, SEASON_SUMMER)], ["spool swamp south", "spool swamp south (autumn)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "autumn")], + oos_season_in_spool_swamp(state, player, SEASON_AUTUMN)], ["spool swamp south (winter)", "spool swamp south", False, None], ["spool swamp south (spring)", "spool swamp south", False, None], ["spool swamp south (summer)", "spool swamp south", False, None], ["spool swamp south (autumn)", "spool swamp south", False, None], - ["spool swamp south (spring)", "spool swamp south gasha spot", False, lambda state: \ + ["spool swamp south (spring)", "spool swamp south near gasha spot", False, lambda state: \ oos_can_break_flowers(state, player, True) ], - ["spool swamp south (winter)", "spool swamp south gasha spot", False, lambda state: \ + ["spool swamp south (winter)", "spool swamp south near gasha spot", False, lambda state: \ oos_can_remove_snow(state, player, True) ], - ["spool swamp south (summer)", "spool swamp south gasha spot", False, None], - ["spool swamp south (autumn)", "spool swamp south gasha spot", False, None], + ["spool swamp south (summer)", "spool swamp south near gasha spot", False, None], + ["spool swamp south (autumn)", "spool swamp south near gasha spot", False, None], - ["spool swamp south gasha spot", "spool swamp south (spring)", False, lambda state: all([ - oos_season_in_spool_swamp(state, player, "spring"), + ["spool swamp south near gasha spot", "spool swamp south (spring)", False, lambda state: all([ + oos_season_in_spool_swamp(state, player, SEASON_SPRING), oos_can_break_flowers(state, player, True) ])], - ["spool swamp south gasha spot", "spool swamp south (winter)", False, lambda state: all([ - oos_season_in_spool_swamp(state, player, "winter"), + ["spool swamp south near gasha spot", "spool swamp south (winter)", False, lambda state: all([ + oos_season_in_spool_swamp(state, player, SEASON_WINTER), oos_can_remove_snow(state, player, True) ])], - ["spool swamp south gasha spot", "spool swamp south (summer)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "summer")], - ["spool swamp south gasha spot", "spool swamp south (autumn)", False, lambda state: \ - oos_season_in_spool_swamp(state, player, "autumn")], + ["spool swamp south near gasha spot", "spool swamp south (summer)", False, lambda state: \ + oos_season_in_spool_swamp(state, player, SEASON_SUMMER)], + ["spool swamp south near gasha spot", "spool swamp south (autumn)", False, lambda state: \ + oos_season_in_spool_swamp(state, player, SEASON_AUTUMN)], ["spool swamp south (winter)", "spool swamp cave", False, lambda state: all([ oos_can_remove_snow(state, player, True), @@ -577,7 +577,7 @@ def make_holodrum_logic(player: int): oos_has_feather(state, player), oos_has_flippers(state, player), oos_can_summon_dimitri(state, player), - oos_get_default_season(state, player, "SUNKEN_CITY") == "winter" + oos_get_default_season(state, player, "SUNKEN_CITY") == SEASON_WINTER ]), oos_can_harvest_tree(state, player, True) ])], @@ -589,7 +589,7 @@ def make_holodrum_logic(player: int): any([ oos_has_feather(state, player), oos_has_flippers(state, player), - oos_get_default_season(state, player, "SUNKEN_CITY") == "winter" + oos_get_default_season(state, player, "SUNKEN_CITY") == SEASON_WINTER ]) ]) ])], @@ -600,7 +600,7 @@ def make_holodrum_logic(player: int): ])], ["sunken city", "syrup trade", False, lambda state: all([ any([ - oos_get_default_season(state, player, "SUNKEN_CITY") == "winter", + oos_get_default_season(state, player, "SUNKEN_CITY") == SEASON_WINTER, all([ oos_has_winter(state, player), any([ @@ -611,7 +611,7 @@ def make_holodrum_logic(player: int): ]), state.has("Mushroom", player) ])], - ["syrup trade", "syrup shop", False, lambda state: oos_has_rupees(state, player, 800)], + ["syrup trade", "syrup shop", False, lambda state: oos_has_rupees(state, player, 600)], # Use Dimitri to get the tree seeds, using dimitri to get seeds being medium difficulty ["sunken city dimitri", "sunken city tree", False,lambda state: all([ @@ -634,10 +634,7 @@ def make_holodrum_logic(player: int): ["sunken city dimitri", "chest in master diver's cave", False, None], ["sunken city", "sunken city, summer cave", False, lambda state: all([ - any([ - oos_get_default_season(state, player, "SUNKEN_CITY") == "summer", - oos_has_summer(state, player) - ]), + oos_season_in_sunken_city(state, player, SEASON_SUMMER), oos_has_flippers(state, player), oos_can_break_bush(state, player, False) ])], @@ -645,7 +642,7 @@ def make_holodrum_logic(player: int): ["mount cucco", "sunken city", False, lambda state: oos_has_flippers(state, player)], ["sunken city", "mount cucco", False, lambda state: all([ oos_has_flippers(state, player), - oos_season_in_sunken_city(state, player, "summer") + oos_season_in_sunken_city(state, player, SEASON_SUMMER) ])], # MT. CUCCO / GORON MOUNTAINS ############################################################################## @@ -655,7 +652,7 @@ def make_holodrum_logic(player: int): ["mount cucco", "rightmost rooster ledge", False, lambda state: all([ any([ # to reach the rooster all([ - oos_season_in_mt_cucco(state, player, "spring"), + oos_season_in_mt_cucco(state, player, SEASON_SPRING), any([ oos_can_break_flowers(state, player, False), # Moosh can break flowers one way, but it won't be of any help when coming back so we need @@ -671,7 +668,7 @@ def make_holodrum_logic(player: int): ["rightmost rooster ledge", "mt. cucco, platform cave", False, None], ["rightmost rooster ledge", "spring banana tree", False, lambda state: all([ oos_has_feather(state, player), - oos_season_in_mt_cucco(state, player, "spring"), + oos_season_in_mt_cucco(state, player, SEASON_SPRING), any([ # can harvest tree oos_has_sword(state, player), oos_has_fools_ore(state, player) @@ -679,7 +676,7 @@ def make_holodrum_logic(player: int): ])], ["mount cucco", "mt. cucco, talon's cave entrance", False, lambda state: \ - oos_season_in_mt_cucco(state, player, "spring")], + oos_season_in_mt_cucco(state, player, SEASON_SPRING)], ["mt. cucco, talon's cave entrance", "talon trade", False, lambda state: state.has("Megaphone", player)], ["talon trade", "mt. cucco, talon's cave", False, None], @@ -751,31 +748,33 @@ def make_holodrum_logic(player: int): ])], ["lost woods stump", "lost woods", False, lambda state: oos_can_reach_lost_woods_pedestal(state, player)], - - ["lost woods stump", "d6 sector", False, lambda state: all([ - oos_has_winter(state, player), - oos_has_autumn(state, player), - oos_has_spring(state, player), - oos_has_summer(state, player), - ])], + ["lost woods stump", "d6 sector", False, lambda state: oos_can_complete_lost_woods_main_sequence(state, player)], ["d6 sector", "tarm ruins tree", False, lambda state: oos_can_harvest_tree(state, player, False)], ["d6 sector", "tarm ruins, under tree", False, lambda state: all([ - oos_season_in_tarm_ruins(state, player, "autumn"), + oos_season_in_tarm_ruins(state, player, SEASON_AUTUMN), oos_can_break_mushroom(state, player, False), oos_can_use_ember_seeds(state, player, False) ])], ["d6 sector", "d6 entrance", False, lambda state: all([ - oos_season_in_tarm_ruins(state, player, "winter"), + oos_season_in_tarm_ruins(state, player, SEASON_WINTER), any([ oos_has_shovel(state, player), oos_can_use_ember_seeds(state, player, False) ]), - oos_season_in_tarm_ruins(state, player, "spring"), + oos_season_in_tarm_ruins(state, player, SEASON_SPRING), oos_can_break_flowers(state, player, False) ])], - ["d6 entrance", "old man near d6", False, lambda state: oos_can_use_ember_seeds(state, player, False)], + ["d6 sector", "old man near d6", False, lambda state: all([ + oos_season_in_tarm_ruins(state, player, SEASON_WINTER), + oos_season_in_tarm_ruins(state, player, SEASON_SPRING), + oos_can_break_flowers(state, player, False), + oos_can_use_ember_seeds(state, player, False) + ])], + # When coming from D6 entrance, the pillar needs to be broken during spring to be able to go backwards + ["d6 entrance", "d6 sector", False, lambda state: + oos_get_default_season(state, player, "TARM_RUINS") == SEASON_SPRING], # SAMASA DESERT ###################################################################################### @@ -787,56 +786,56 @@ def make_holodrum_logic(player: int): ["temple remains lower stump", "temple remains upper stump", False, lambda state: any([ all([ # Winter rule - oos_season_in_temple_remains(state, player, "winter"), + oos_season_in_temple_remains(state, player, SEASON_WINTER), oos_can_remove_snow(state, player, False), oos_can_break_bush(state, player, False), oos_can_jump_6_wide_pit(state, player) ]), all([ # Summer rule - oos_season_in_temple_remains(state, player, "summer"), + oos_season_in_temple_remains(state, player, SEASON_SUMMER), oos_can_break_bush(state, player, False), oos_can_jump_6_wide_pit(state, player) ]), all([ # Spring rule - oos_season_in_temple_remains(state, player, "spring"), + oos_season_in_temple_remains(state, player, SEASON_SPRING), oos_can_break_flowers(state, player, False), oos_can_break_bush(state, player, False), oos_can_jump_6_wide_pit(state, player) ]), all([ # Autumn rule - oos_season_in_temple_remains(state, player, "autumn"), + oos_season_in_temple_remains(state, player, SEASON_AUTUMN), oos_can_break_bush(state, player) ]) ])], ["temple remains upper stump", "temple remains lower stump", False, lambda state: any([ # Winter rule - oos_season_in_temple_remains(state, player, "winter"), + oos_season_in_temple_remains(state, player, SEASON_WINTER), all([ # Summer rule - oos_season_in_temple_remains(state, player, "summer"), + oos_season_in_temple_remains(state, player, SEASON_SUMMER), oos_can_break_bush(state, player, False), oos_can_jump_6_wide_pit(state, player) ]), all([ # Spring rule - oos_season_in_temple_remains(state, player, "spring"), + oos_season_in_temple_remains(state, player, SEASON_SPRING), oos_can_break_flowers(state, player, False), oos_can_break_bush(state, player, False), oos_can_jump_6_wide_pit(state, player) ]), all([ # Autumn rule - oos_season_in_temple_remains(state, player, "autumn"), + oos_season_in_temple_remains(state, player, SEASON_AUTUMN), oos_can_break_bush(state, player) ]) ])], - ["temple remains upper stump", "temple remains lower portal", False, lambda state: all([ - oos_season_in_temple_remains(state, player, "winter"), + ["temple remains upper stump", "temple remains lower portal access", False, lambda state: all([ + oos_season_in_temple_remains(state, player, SEASON_WINTER), oos_can_jump_1_wide_pit(state, player, False) ])], - ["temple remains lower portal", "temple remains upper stump", False, lambda state: any([ + ["temple remains lower portal access", "temple remains upper stump", False, lambda state: any([ # Portal can be escaped only if default season is winter or if volcano erupted all([ - oos_get_default_season(state, player, "TEMPLE_REMAINS") == "winter", + oos_get_default_season(state, player, "TEMPLE_REMAINS") == SEASON_WINTER, oos_can_jump_1_wide_pit(state, player, False) ]), all([ @@ -844,6 +843,9 @@ def make_holodrum_logic(player: int): oos_can_jump_2_wide_liquid(state, player) ]), ])], + + ["temple remains lower portal access", "temple remains lower portal", True, None], + ["temple remains lower portal", "temple remains lower stump", False, lambda state: \ # There is an added ledge in rando that enables jumping from the portal down to the stump, whatever # the season is, but it is a risky action so we ask for the player to be able to warp back @@ -857,7 +859,7 @@ def make_holodrum_logic(player: int): ["temple remains lower stump", "temple remains upper portal", False, lambda state: all([ state.has("_triggered_volcano", player), - oos_season_in_temple_remains(state, player, "summer"), + oos_season_in_temple_remains(state, player, SEASON_SUMMER), oos_can_jump_2_wide_liquid(state, player), any([ oos_has_magnet_gloves(state, player), @@ -872,8 +874,8 @@ def make_holodrum_logic(player: int): ["temple remains upper portal", "temple remains upper stump", False, lambda state: \ oos_can_jump_1_wide_pit(state, player, False)], - ["temple remains upper portal", "temple remains lower portal", False, lambda state: \ - oos_get_default_season(state, player, "TEMPLE_REMAINS") == "winter"], + ["temple remains upper portal", "temple remains lower portal access", False, lambda state: \ + oos_get_default_season(state, player, "TEMPLE_REMAINS") == SEASON_WINTER], # ONOX CASTLE ############################################################################################# @@ -904,22 +906,22 @@ def make_holodrum_logic(player: int): # GOLDEN BEASTS ############################################################################################# ["d0 entrance", "golden darknut", False, lambda state: all([ - oos_season_in_western_coast(state, player, "spring"), + oos_season_in_western_coast(state, player, SEASON_SPRING), any([ oos_has_sword(state, player), oos_has_fools_ore(state, player) ]) ])], ["tarm ruins", "golden lynel", False, lambda state: all([ - oos_season_in_lost_woods(state, player, "summer"), - oos_season_in_lost_woods(state, player, "winter"), + oos_season_in_lost_woods(state, player, SEASON_SUMMER), + oos_season_in_lost_woods(state, player, SEASON_WINTER), any([ oos_has_sword(state, player), oos_has_fools_ore(state, player) ]) ])], ["d2 entrance", "golden moblin", False, lambda state: all([ - oos_season_in_central_woods_of_winter(state, player, "autumn"), + oos_season_in_central_woods_of_winter(state, player, SEASON_AUTUMN), any([ oos_has_sword(state, player), oos_has_fools_ore(state, player), @@ -934,4 +936,60 @@ def make_holodrum_logic(player: int): oos_has_sword(state, player), oos_has_fools_ore(state, player) ])], + + # GASHA TREES ############################################################################################# + + ["horon village", "horon gasha spot", False, None], + ["horon village", "impa gasha spot", False, lambda state: oos_can_break_bush(state, player, True)], + ["suburbs", "suburbs gasha spot", False, lambda state: oos_can_break_bush(state, player, True)], + ["ghastly stump", "holodrum plain gasha spot", False, lambda state: all([ + oos_can_break_bush(state, player, True), + oos_has_shovel(state, player), + ])], + ["d1 island", "holodrum plain island gasha spot", False, lambda state: all([ + oos_can_swim(state, player, True), + any([ + oos_can_break_bush(state, player, False), + oos_can_summon_dimitri(state, player), # Only Dimitri can be brought here + ]), + ])], + ["floodgate keyhole", "spool swamp north gasha spot", False, lambda state: oos_has_bracelet(state, player)], + ["spool swamp south near gasha spot", "spool swamp south gasha spot", False, lambda state: oos_has_bracelet(state, player)], + ["sunken city", "sunken city gasha spot", False, lambda state: all([ + oos_season_in_sunken_city(state, player, SEASON_SUMMER), + oos_can_swim(state, player, False), + oos_can_break_bush(state, player, False), + ])], + ["sunken city dimitri", "sunken city gasha spot", False, None], + ["goron mountain entrance", "goron mountain left gasha spot", False, lambda state: oos_has_shovel(state, player)], + ["goron mountain entrance", "goron mountain right gasha spot", False, lambda state: oos_has_bracelet(state, player)], + ["d5 stump", "eyeglass lake gasha spot", False, lambda state: all([ + oos_has_shovel(state, player), + oos_can_break_bush(state, player), + ])], + ["mount cucco", "mt cucco gasha spot", False, lambda state: all([ + oos_season_in_mt_cucco(state, player, SEASON_AUTUMN), + oos_can_break_mushroom(state, player, False), + ])], + ["d6 sector", "tarm ruins gasha spot", False, lambda state: oos_has_shovel(state, player)], + ["samasa desert", "samasa desert gasha spot", False, None], + ["western coast after ship", "western coast gasha spot", False, None], + ["north horon", "onox gasha spot", False, lambda state: oos_has_shovel(state, player)], + + ["Menu", "gasha tree 1", False, lambda state: oos_can_harvest_gasha(state, player, 1)], + ["Menu", "gasha tree 2", False, lambda state: oos_can_harvest_gasha(state, player, 2)], + ["Menu", "gasha tree 3", False, lambda state: oos_can_harvest_gasha(state, player, 3)], + ["Menu", "gasha tree 4", False, lambda state: oos_can_harvest_gasha(state, player, 4)], + ["Menu", "gasha tree 5", False, lambda state: oos_can_harvest_gasha(state, player, 5)], + ["Menu", "gasha tree 6", False, lambda state: oos_can_harvest_gasha(state, player, 6)], + ["Menu", "gasha tree 7", False, lambda state: oos_can_harvest_gasha(state, player, 7)], + ["Menu", "gasha tree 8", False, lambda state: oos_can_harvest_gasha(state, player, 8)], + ["Menu", "gasha tree 9", False, lambda state: oos_can_harvest_gasha(state, player, 9)], + ["Menu", "gasha tree 10", False, lambda state: oos_can_harvest_gasha(state, player, 10)], + ["Menu", "gasha tree 11", False, lambda state: oos_can_harvest_gasha(state, player, 11)], + ["Menu", "gasha tree 12", False, lambda state: oos_can_harvest_gasha(state, player, 12)], + ["Menu", "gasha tree 13", False, lambda state: oos_can_harvest_gasha(state, player, 13)], + ["Menu", "gasha tree 14", False, lambda state: oos_can_harvest_gasha(state, player, 14)], + ["Menu", "gasha tree 15", False, lambda state: oos_can_harvest_gasha(state, player, 15)], + ["Menu", "gasha tree 16", False, lambda state: oos_can_harvest_gasha(state, player, 16)], ] diff --git a/worlds/tloz_oos/data/logic/SubrosiaLogic.py b/worlds/tloz_oos/data/logic/SubrosiaLogic.py index 0c15ad255dd9..0d2b895b9f74 100644 --- a/worlds/tloz_oos/data/logic/SubrosiaLogic.py +++ b/worlds/tloz_oos/data/logic/SubrosiaLogic.py @@ -5,13 +5,13 @@ def make_subrosia_logic(player: int): return [ # Portals ############################################################### - ["subrosia portal 1", "subrosia temple sector", True, None], - ["subrosia portal 2", "subrosia market sector", True, None], - ["subrosia portal 3", "subrosia hide and seek sector", True, lambda state: oos_has_feather(state, player)], - ["subrosia portal 4", "subrosia pirates sector", True, None], - ["subrosia portal 5", "subrosia furnace sector", True, None], - ["subrosia portal 6", "subrosia volcano sector", True, None], - ["subrosia portal 7", "d8 entrance", True, None], + ["volcanoes east portal", "subrosia temple sector", True, None], + ["subrosia market portal", "subrosia market sector", True, None], + ["strange brothers portal", "subrosia hide and seek sector", True, lambda state: oos_has_feather(state, player)], + ["house of pirates portal", "subrosia pirates sector", True, None], + ["great furnace portal", "subrosia furnace sector", True, None], + ["volcanoes west portal", "subrosia volcano sector", True, None], + ["d8 entrance portal", "d8 entrance", True, None], ["subrosia pirates sector", "western coast after ship", False, lambda state: state.has("Pirate's Bell", player)], diff --git a/worlds/tloz_oos/docs/en_The Legend of Zelda - Oracle of Seasons.md b/worlds/tloz_oos/docs/en_The Legend of Zelda - Oracle of Seasons.md index ef9fa403f61c..760b62020b95 100644 --- a/worlds/tloz_oos/docs/en_The Legend of Zelda - Oracle of Seasons.md +++ b/worlds/tloz_oos/docs/en_The Legend of Zelda - Oracle of Seasons.md @@ -9,21 +9,35 @@ configure and export a config file. Most acquirable pickups are shuffled among each other, following some logic to ensure the game ends up being completable. -There are a few checks still **not** being randomized: -- Maple Ghastly Doll drop (having Lon Lon Egg will always give Ghastly Doll) -- Subrosian sign-loving guy -- golden Old Man asking you to fight golden beasts -- rare Maple drop (Piece of Heart in vanilla) -- any Gasha Seed contents - -Linked games (e.g. starting a game with a code coming from Oracle of Ages) aren't supported and will most likely never be, so linked-only checks are not handled either. +Rare Maple drop is **not** randomized because it would not make much sense. ## What does another world's item look like in Oracle of Seasons? -Items belonging to other worlds are currently being represented as a "galaxy blue" colored Star Ore sprite. -When collecting it, the game will inform you that you just sent an item to another player, but you need to check -on the client to see which item was actually sent. +Items belonging to other worlds use an easily recognizable Archipelago sprite, which is blue if the item is unimportant, and red if it is a progression item. +When collecting it, the game will inform you that you just sent an item to another player, and you can check on the client window to see which item was actually sent. + +## Frequently Asked Questions + +**What does medium / hard logic expects from me?** + +> - **Casual logic** only expects what you are forced to know when doing a casual playthrough of the game +> - **Medium logic** expects you to have a complete knowledge of the game, but nothing execution-heavy will be required (no tricks or glitches) +> - **Hard logic** expects you to use glitches and perform tricks such as bomb jumps +> +> A [document](https://docs.google.com/document/d/1IVYvvZS6NuTDoeWJlbFA5AW2Lj-nIaweRJkKuq7ncqc/) was built by Ishigh and the community to explain the breakdown between those logic levels more in detail. + +**Is there a tracker for this game?** + +> Yes, Seto has built a Poptracker pack which can be downloaded [here](https://github.com/seto10987/Oracle-of-Seasons-AP-Poptracker-Pack/releases). + +**How can I warp to start?** + +> You need to press A+B while the screen is fading to white into any menu (e.g. after pressing START or SELECT) + +**How can I switch directly to a specific season instead of cycling all seasons everytime?** + +> You can hold a diagonal on your directional pad right after having used the rod. This will switch directly to the season whose icon is placed in that direction next to the Rod of Seasons icon at the top of your screen. Timing is tight, but with some practice you will be able to save precious seconds. -## Changes from the vanilla game +**Can I do linked games using passwords from Oracle of Ages?** -- If enabled, you can warp to start by holding Start while exiting the map menu +> No, linked features are completely disabled in this randomizer since it changes the world events and locations too heavily to be reasonable to maintain. diff --git a/worlds/tloz_oos/docs/fr_The Legend of Zelda - Oracle of Seasons.md b/worlds/tloz_oos/docs/fr_The Legend of Zelda - Oracle of Seasons.md index 0159cf26d1d6..2d8b3b4dde9c 100644 --- a/worlds/tloz_oos/docs/fr_The Legend of Zelda - Oracle of Seasons.md +++ b/worlds/tloz_oos/docs/fr_The Legend of Zelda - Oracle of Seasons.md @@ -8,17 +8,38 @@ La [page des paramètres du joueur pour ce jeu](../player-settings) contient la La plupart des objets à acquérir sont mélangés les uns aux autres, en suivant une certaine logique pour s'assurer que le jeu peut être complété. -Il y a quelques contrôles qui ne sont toujours pas randomisés : -- rare Maple drop (Piece of Heart dans le jeux de base) -- any Gasha Seed contents - -Les "linked games" (par exemple, commencer une partie avec un code provenant d'Oracle of Ages) ne sont pas supportés et ne le seront probablement jamais, donc les "linked-only checks" ne sont pas gérés non plus. +Le drop rare de Maple (un quart de coeur dans le jeu de base) n'est **pas** randomisé car cela ne serait pas très intéressant. ## A quoi ressemble un objet d'un autre monde dans Oracle of Seasons ? -Les objets appartenant à d'autres mondes sont actuellement représentés par un sprite Star Ore de couleur "bleu galaxie". -Lorsque vous le récupérez, le jeu vous informe que vous venez d'envoyer un objet à un autre joueur, mais vous devez vérifier sur le client quel objet a été envoyé. +Les items appartenant à d'autres mondes utilisent un sprite Archipelago facilement reconnaissable. +Celui-ci sera bleu si l'objet n'est pas forcément utile, et rouge s'il s'agit d'un objet de progression. + +Lorsque vous le collectez, le jeu vous informe que vous venez d'envoyer un item à un autre monde, et vous pouvez regarder sur la fenêtre du client de quel item il s'agit. + +## Foire aux questions + +**Qu'est-ce que la logique medium / hard attend de moi?** + +> - La **logique Casual** attend du joueur ce qui est obligé d'être su en faisant une partie normale complète sur le jeu +> - La **logique Medium** attend du joueur d'avoir une connaissance complète du jeu et des façons alternatives de faire les choses, mais ne requiert rien qui demande un bon niveau d'exécution (pas de tricks / glitches) +> - La **logique Hard** attend du joueur de connaître et d'utiliser des glitches et tricks tels que les bomb jumps +> +> Un [document](https://docs.google.com/document/d/1IVYvvZS6NuTDoeWJlbFA5AW2Lj-nIaweRJkKuq7ncqc/) a été fait par Ishigh et la communauté pour décrire les différences entre les niveaux de logique plus en détail. + +**Y a-t-il un tracker pour ce jeu?** + +> Oui, Seto a mis au point un pack Poptracker qui peut être téléchargé [ici](https://github.com/seto10987/Oracle-of-Seasons-AP-Poptracker-Pack/releases). + +**Comment est-ce que je peux me téléporter au point de départ?** + +> Vous devez appuyer sur A et B en même temps pendant que l'écran est en train de fondre vers le blanc lors de la transition vers un menu (par exemple, après avoir appuyé sur START ou SELECT) + +**Comment est-ce que je peux directement changer la saison pour une saison particulière plutôt que de devoir faire le cycle complet à chaque fois?** + +> Vous pouvez maintenir une diagonale sur votre pavé directionnel just après avoir utilisé le sceptre des saisons. Cela transitionnera vers la saison dont l'icône est placée dans cette direction à côté de l'icône du sceptre des saisons, en haut de votre écran. +> Le timing est serré, mais avec un peu d'entrainement, vous y arriverez systématiquement et cela vous sauvera de précieuses secondes. -## Changements par rapport au jeu vanille +**Est-ce que je peux faire un jeu lié avec un code provenant d'Oracle of Ages?** -- Si cette option est activée, vous pouvez vous rendre au point de départ en maintenant la touche Start enfoncée tout en quittant le menu de la carte. +> Non, l'ensemble des fonctionnalités de jeu lié sont entièrement désactivées dans ce randomizer. diff --git a/worlds/tloz_oos/docs/oos_setup_en.md b/worlds/tloz_oos/docs/oos_setup_en.md index a0ef42461508..bccd7c6a3adf 100644 --- a/worlds/tloz_oos/docs/oos_setup_en.md +++ b/worlds/tloz_oos/docs/oos_setup_en.md @@ -2,34 +2,39 @@ ## Required Software -- [Oracle of Seasons .apworld](https://github.com/Dinopony/Archipelago/releases/latest) -- [Oracles Archipelago Patcher](https://github.com/Dinopony/oracles-archipelago-patcher/releases/latest) +- [Oracle of Seasons .apworld](https://github.com/Dinopony/ArchipelagoOoS/releases/latest) - [Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) - Your legally obtained Oracle of Seasons US ROM file ## Installation Instructions -1. Download the **Oracle of Seasons .apworld file** and put it inside the "lib/worlds/" subdirectory of your Archipelago install directory -2. Generate a seed using your .yaml settings file (see template below) -3. When generating, the server built for you a .patcherdata file that can be fed to the **Oracles Archipelago Patcher** software, download it -4. Download the **Oracles Archipelago Patcher** software and unzip it in its own directory -5. Put your **Oracle of Seasons US ROM** inside this folder (name doesn't matter as long as it has the .gbc file extension) -6. Get your own .patcherdata file that was generated by Archipelago Server while generating -7. Right-click the .patcherdata file, select "Open With..." and point to the **oracles-archipelago-patcher.exe** inside the same directory where you previously put the vanilla ROM -8. If everything went fine, the patched ROM should have appeared next to the .patcherdata file -9. Open the patched ROM inside Bizhawk -10. Inside Bizhawk, go into "Tools > Lua Console", then "Script > Open Script" and pick the "connector_bizhawk_generic.lua" file inside the "data/lua/" subfolder of your Archipelago install -11. Launch Bizhawk Generic Client, it should automatically connect to the emulator -12. Connect the Client to the AP Server of your choice, and you can start playing! +1. Put your **Oracle of Seasons US ROM** inside your Archipelago install folder (named "Legend of Zelda, The - Oracle of Seasons (USA).gbc") +2. Download the **Oracle of Seasons .apworld file** and double-click it to install it the "custom_worlds/" subdirectory of your Archipelago install directory +3. Generate a seed using your .yaml settings file (see below if you don't know how to get the template) +4. Download the .apoos patch file that was built by the server while generating, this will be used to generate your modified ROM +5. Open this patch file using the Archipelago Launcher +6. If everything went fine, the patched ROM was built in the same directory as the .apoos file, and both Bizhawk and the client launched +7. Connect the Client to the AP Server of your choice, and you can start playing! ## Create a Config (.yaml) File To get the template YAML file: -1. install the .apworld file as instructed above -2. if Archipelago Launcher was running on your computer, close it -3. run the Archipelago launcher -4. click on "Generate Template Settings" -5. it should open a directory in file explorer, pick the file named `The Legend of Zelda - Oracle of Seasons.yaml` - -From there, you can edit it and place it directly inside the "Players" subdirectory of your Archipelago install. -Once you have files in there, you can run ArchipelagoGenerate and play your generated multiworld! \ No newline at end of file +1. Install the .apworld file as instructed above +2. If Archipelago Launcher was running on your computer, close it +3. Run the Archipelago launcher +4. Click on "Generate Template Settings" +5. It should open a directory in file explorer, pick the file named `The Legend of Zelda - Oracle of Seasons.yaml` + +## Setting up cosmetic options (sprite, palette...) + +Inside the "host.yaml" configuration file that can be found in the Archipelago install folder, you can configure a few cosmetic options for the game. +Under the "tloz_oos_options" item, you can find the following options: +- "**character_sprite**", used to change the sprite for your character +- "**character_palette**", used to change the color of your character +- "**heart_beep_interval**", used to alter the speed of the beeping sound when low on health + +Most of those settings are pretty self-explanatory, but sprites need some extra information. +Sprites are files with the ".bin" extension which needs to be placed inside the "data/sprites/oos_ooa/" subfolder inside your Archipelago install. +You need to download the sprites you want to use from [that repository](https://github.com/Dinopony/oracles-sprites/), and place them inside the folder mentioned above. +This means if you placed a file called "goron.bin" in that folder, you then just have to set "goron" as "character_sprite" inside your host.yaml file before patching. +Once this is done, all subsequent patched ROMs will use those cosmetic settings, but you can easily change them by just editing that file again. diff --git a/worlds/tloz_oos/docs/oos_setup_fr.md b/worlds/tloz_oos/docs/oos_setup_fr.md index 521de65c1d0c..72a7782e12c7 100644 --- a/worlds/tloz_oos/docs/oos_setup_fr.md +++ b/worlds/tloz_oos/docs/oos_setup_fr.md @@ -1,35 +1,41 @@ # Guide d'installation de The Legend of Zelda : Oracle of Seasons -## Logiciel requis +## Logiciels requis -- [Oracle of Seasons .apworld](https://github.com/Dinopony/Archipelago/releases/latest) -- [Oracles Archipelago Patcher](https://github.com/Dinopony/oracles-archipelago-patcher/releases/latest) +- [Oracle of Seasons .apworld](https://github.com/Dinopony/ArchipelagoOoS/releases/latest) - Bizhawk 2.9.1 (x64)](https://tasvideos.org/BizHawk/ReleaseHistory) -- Votre ROM Oracle of Seasons US obtenu légalement +- Votre ROM Oracle of Seasons US obtenue légalement ## Instructions d'installation -1. Téléchargez le fichier **Oracle of Seasons .apworld** et placez-le dans le sous-répertoire "lib/worlds/" de votre répertoire d'installation d'Archipelago. -2. Générez en utilisant votre fichier de configuration .yaml (voir le modèle ci-dessous). -3. Lors de la génération, le serveur a généré pour vous un fichier .patcherdata qui pourra être transmis au logiciel **Oracles Archipelago Patcher**, téléchargez-le. -4. Téléchargez le logiciel **Oracles Archipelago Patcher** et décompressez-le dans son propre répertoire. -5. Placez votre **Rom de Oracles of Seasons US** dans ce dossier (le nom n'a pas d'importance tant qu'il a l'extension .gbc). -6. Récupérez votre propre fichier .patcherdata qui a été généré par Archipelago lors de la génération du fichier .patcherdata. -7. Faites un clic droit sur le fichier .patcherdata, sélectionnez "Open With..." et pointez sur **oracles-archipelago-patcher.exe** dans le même répertoire que celui où vous avez placé la ROM vanille. -8. Si tout s'est bien passé, la ROM patchée devrait apparaître à côté du fichier .patcherdata. -9. Ouvrez la ROM patchée dans Bizhawk -10. Dans Bizhawk, allez dans "Tools > Lua Console", puis "Script > Open Script" et choisissez le fichier "connector_bizhawk_generic.lua" dans le sous-dossier "data/lua/" de votre installation Archipelago. -11. Lancez Bizhawk Generic Client, il devrait se connecter automatiquement à l'émulateur. -12. Connectez le client au serveur AP de votre choix, et vous pouvez commencer à jouer ! - -## Créer un fichier de configuration (.yaml) - -Pour obtenir le fichier modèle: -1. Installer le fichier .apworld en suivant les instructions ci-dessus -2. Si le Launcher Archipelago était en cours de fonctionnement, fermez-le -3. Démarrez le Launcher Archipelago +1. Mettez votre **ROM US d'Oracle of Seasons** dans le dossier où Archipelago est installé (nommée "Legend of Zelda, The - Oracle of Seasons (USA).gbc") +2. Téléchargez le **fichier .apworld pour Oracle of Seasons** et double-cliquez dessus afin de l'installer dans le répertoire "custom_worlds/" de votre installation Archipelago +3. Générez une seed en utilisant vos fichiers d'options au format .yaml (voir ci-dessous si vous ne savez pas comment obtenir le fichier modèle) +4. Téléchargez le fichier de patch au format .apoos qui a été généré en même temps que la seed par le serveur. Celui-ci vous permettra de générer votre ROM modifiée. +5. Ouvrez ce fichier avec l'Archipelago Launcher +6. Si tout s'est bien passé, la ROM patchée a été générée dans le même répertoire que le fichier .apoos, et Bizhawk ainsi que le client se sont automatiquement lancés +7. Connectez-vous au serveur Archipelago de votre choix, et vous pouvez commencer à jouer! + +## Créer un fichier d'options (.yaml) + +Pour obtenir le fichier YAML modèle: +1. Installez le fichier .apworld comme indiqué ci-dessus +2. Si l'Archipelago Launcher était déjà lancé, fermez-le +3. Lancez l'Archipelago launcher 4. Cliquez sur "Generate Template Settings" -5. Cela devrait ouvrir un explorateur de fichiers dans un dossier précis, prenez le fichier `The Legend of Zelda - Oracle of Seasons.yaml` +5. Cela devrait ouvrir un répertoire de fichier avec les fichiers modèles, prenez le fichier `The Legend of Zelda - Oracle of Seasons.yaml` + +## Gérer les options cosmétiques (sprite, palette...) + +Dans le fichier de configuration "host.yaml" qui se trouve dans votre répertoire d'installation d'Archipelago, +vous pouvez régler des options cométiques pour le jeu. +Sous la catégorie "tloz_oos_options", vous trouverez les options suivantes: +- "**character_sprite**", qui sert à changer le sprite de votre personnage +- "**character_palette**", qui sert à changer la couleur de votre personnage +- "**heart_beep_interval**", qui sert à changer l'intervalle de bip du son lorsque vous êtes bas en coeurs -A partir de là, vous pouvez éditer ce fichier à votre convenance et le placer directement dans le répertoire "Players" de votre installation Archipelago. -Dès que vous avez un ou plusieurs fichiers dedans, vous pouvez lancer "ArchipelagoGenerate" et profiter de votre multiworld fraîchement créé ! \ No newline at end of file +La plupart des ces réglages parlent d'eux-même, sauf les sprites qui méritent quelques informations supplémentaires. +Les sprites sont des fichiers avec l'extension ".bin" extension qui doivent être placés dans le sous-répertoire "data/sprites/oos_ooa/" de votre installation Archipelago. +Vous devez télécharger les fichiers de sprites que vous souhaitez utiliser depuis [ce repository](https://github.com/Dinopony/oracles-sprites/), et les placer ensuite dans le dossier mentionné ci-dessus. +Cela signifie que si vous placez un fichier "goron.bin" dans ce dossier, vous pouvez ensuite mettre "goron" comme valeur pour l'option "character_sprite" dans le fichier host.yaml avant de générer votre ROM. +Une fois cela réglé, toutes les ROMs seront patchées en utilisant ces paramètres cosmétiques. diff --git a/worlds/tloz_oos/patching/Constants.py b/worlds/tloz_oos/patching/Constants.py new file mode 100644 index 000000000000..0b3751ef0dc5 --- /dev/null +++ b/worlds/tloz_oos/patching/Constants.py @@ -0,0 +1,592 @@ +from ..data.Constants import * + +EOB_ADDR = [ + 0x3ec8, # 00 + 0x3e89, # 01 + 0x35bb, # 02 + 0x3dd7, # 03 + 0x3e12, # 04 + 0x3e2d, # 05 + 0x3864, # 06 - 128 bytes reserved for sprite expansion w/ web patcher + 0x3900, # 07 + 0x3fc0, # 08 + 0x3f4e, # 09 + 0x3bf9, # 0a + 0x3f6d, # 0b + 0x3ea1, # 0c + 0x3b82, # 0d + 0x3ef3, # 0e + 0x3f9d, # 0f + 0x3bee, # 10 + 0x3eb0, # 11 + 0x3c8f, # 12 + 0x3bd2, # 13 + 0x2fc9, # 14 - ton of free space here + 0x392d, # 15 + 0x3a07, # 16 + 0x3f3a, # 17 + 0x3e6d, # 18 + 0x36e1, # 19 + 0x30f0, # 1a - here too + 0x3c40, # 1b + 0x4000, # 1c + 0x4000, # 1d + 0x4000, # 1e + 0x4000, # 1f + 0x4000, # 20 + 0x4000, # 21 + 0x4000, # 22 + 0x4000, # 23 + 0x4000, # 24 + 0x4000, # 25 + 0x4000, # 26 + 0x4000, # 27 + 0x4000, # 28 + 0x4000, # 29 + 0x4000, # 2a + 0x4000, # 2b + 0x4000, # 2c + 0x4000, # 2d + 0x4000, # 2e + 0x4000, # 2f + 0x4000, # 30 + 0x4000, # 31 + 0x4000, # 32 + 0x4000, # 33 + 0x4000, # 34 + 0x4000, # 35 + 0x4000, # 36 + 0x4000, # 37 + 0x3df0, # 38 + 0x4000, # 39 + 0x4000, # 3a + 0x4000, # 3b + 0x4000, # 3c + 0x4000, # 3d + 0x4000, # 3e + 0x314b # 3f - also here +] + +DEFINES = { + # WRAM addresses + "wSubscreen1CurrentSlotIndex": "$c085", + "wOriginalMinimapGroup": "$c09d", # Custom address + "wOriginalDungeonIndex": "$c09e", # Custom address + "wMinimapCycleToNextMode": "$c09f", # Custom address + "wKeysPressed": "$c481", + "wKeysJustPressed": "$c482", + "wPaletteThread_mode": "$c4ab", + "wCustomBuffer": "$c4bf", # Custom address + "wAnimalRegion": "$c610", + "wRingsObtained": "$c616", + "wTotalSignsDestroyed": "$c626", + "wDeathRespawnBuffer": "$c62b", + "wMinimapGroup": "$c63a", + "wBoughtShopItems2": "$c640", + "wBoughtSubrosianItems": "$c642", + "wDimitriState": "$c644", + "wAnimalTutorialFlags": "$c646", + "wGashaSpotFlags": "$c649", + "wDungeonCompasses": "$c67c", + "wDungeonMaps": "$c67e", + "wObtainedTreasureFlags": "$c692", + "wNetCountInL": "$c6a0", + "wNetCountInH": "$c6a1", + "wLinkMaxHealth": "$c6a3", + "wCurrentBombs": "$c6aa", + "wMaxBombs": "$c6ab", + "wSeedSatchelLevel": "$c6ae", + "wFluteIcon": "$c6af", + "wFeatherLevel": "$c6b4", + "wNumEmberSeeds": "$c6b5", + "wEssencesObtained": "$c6bb", + "wSatchelSelectedSeeds": "$c6be", + "wActiveRing": "$c6c5", + "wRingBoxLevel": "$c6c6", + "wInsertedJewels": "$c6e1", + "wTextIndexL": "$cba2", + "wTextIndexH": "$cba3", + "wTextNumberSubstitution": "$cba8", + "wDungeonMapScroll": "$cbb4", + "wMapMenuMode": "$cbb3", + "wMapMenuCursorIndex": "$cbb6", + "wMenuLoadState": "$cbcc", + "wMenuActiveState": "$cbcd", + "wDungeonMapScrollState": "$cbce", + "wInventorySubmenu1CursorPos": "$cbd1", + "wRingMenu_mode": "$cbd3", + "wStatusBarNeedsRefresh": "$cbea", + "wNetTreasureIn": "$cbfb", # Custom address + "wFrameCounter": "$cc00", + "wIsLinkedGame": "$cc01", + "wMenuDisabled": "$cc02", + "wRememberedCompanionRoom": "$cc42", + "wRememberedCompanionY": "$cc43", + "wLinkObjectIndex": "$cc48", + "wActiveGroup": "$cc49", + "wActiveRoom": "$cc4c", + "wActiveRoomPack": "$cc4d", + "wRoomStateModifier": "$cc4e", + "wLostWoodsTransitionCounter1": "$cc53", + "wLostWoodsTransitionCounter2": "$cc54", + "wDungeonIndex": "$cc55", + "wDungeonFloor": "$cc57", + "wWarpDestGroup": "$cc63", + "wWarpDestRoom": "$cc64", + "wWarpTransition": "$cc65", + "wWarpDestPos": "$cc66", + "wWarpTransition2": "$cc67", + "wLinkGrabState": "$cc75", + "wDisabledObjects": "$cca4", + "wDisableWarpTiles": "$ccaa", + "wScreenTransitionDirection": "$cd02", + "wScreenOffsetY": "$cd08", + + "w1Link.yh": "$d00b", + "w7ActiveBank": "$d0d4", + + # High RAM offsets (FF00 + offset) + "hRomBank": "$97", + + # Bank 0 functions + "addAToDe": "$0068", + "interBankCall": "$008a", + "getNumSetBits": "$0176", + "checkFlag": "$0205", + "setFlag": "$020e", + "decHlRef16WithCap": "$0237", + "disableLcd": "$02c1", + "getRandomNumber": "$041a", + "queueDmaTransfer": "$0566", + "loadUncompressedGfxHeader": "$05b6", + "forceEnableIntroInputs": "$0862", + "saveFile": "$09b4", + "playSound": "$0c74", + "setMusicVolume": "$0c89", + "giveTreasure": "$16eb", + "loseTreasure": "$1702", + "checkTreasureObtained": "$1717", + "refillSeedSatchel": "$17e5", + "showTextNonExitable": "$1847", + "showText": "$184b", + "getThisRoomFlags": "$1956", + "getRoomFlags": "$1963", + "openMenu": "$1a76", + "linkInteractWithAButtonSensitiveObjects": "$1b23", + "lookupKey": "$1dc4", + "lookupCollisionTable": "$1ddd", + "objectSetVisiblec2": "$1e03", + "objectSetInvisible": "$1e39", + "convertShortToLongPosition": "$2089", + "objectCopyPosition": "$21fd", + "objectCopyPosition_rawAddress": "$2202", + "interactionIncState": "$239b", + "interactionSetScript": "$24fe", + "createTreasure": "$271b", + "setLinkIdOverride": "$2a16", + "clearStaticObjects": "$3076", + "checkGlobalFlag": "$30c7", + "setGlobalFlag": "$30cd", + "fastFadeoutToWhite": "$313b", + "loadScreenMusicAndSetRoomPack": "$32dc", + "setTile": "$3a52", + "getFreeInteractionSlot": "$3ac6", + "interactionDelete": "$3ad9", + "getFreePartSlot": "$3ea7", + + # Byte constants + "STARTING_TREE_MAP_INDEX": "$f8", + "INTERACID_TREASURE": "$60", + "BTN_A": "$01", + "BTN_B": "$02", + "BTN_START": "$08", + "BTN_RIGHT": "$10", + "BTN_LEFT": "$20", + "BTN_UP": "$40", + "BTN_DOWN": "$80", + "COLLECT_PICKUP": "$0a", + "COLLECT_PICKUP_NOFLAG": "$02", + "COLLECT_CHEST": "$38", + "COLLECT_CHEST_NOFLAG": "$30", + # "COLLECT_CHEST_MAP_OR_COMPASS": "$68", + "COLLECT_FALL": "$29", + "COLLECT_FALL_KEY": "$28", + + "SND_SOLVEPUZZLE_2": "$5b", + "SND_GETSEED": "$5e", + "SND_TELEPORT": "$8d", + "SND_COMPASS": "$a2", + + "SEASON_SPRING": "$00", + "SEASON_SUMMER": "$01", + "SEASON_AUTUMN": "$02", + "SEASON_WINTER": "$03", + + "TREASURE_SHIELD": "$01", + "TREASURE_PUNCH": "$02", + "TREASURE_BOMBS": "$03", + "TREASURE_SWORD": "$05", + "TREASURE_BOOMERANG": "$06", + "TREASURE_ROD_OF_SEASONS": "$07", + "TREASURE_MAGNET_GLOVES": "$08", + "TREASURE_FLUTE": "$0e", + "TREASURE_SLINGSHOT": "$13", + "TREASURE_BRACELET": "$16", + "TREASURE_FEATHER": "$17", + "TREASURE_SEED_SATCHEL": "$19", + "TREASURE_FOOLS_ORE": "$1e", + "TREASURE_EMBER_SEEDS": "$20", + "TREASURE_SCENT_SEEDS": "$21", + "TREASURE_PEGASUS_SEEDS": "$22", + "TREASURE_GALE_SEEDS": "$23", + "TREASURE_MYSTERY_SEEDS": "$24", + "TREASURE_PIRATES_BELL": "$25", # Rando specific ID + "TREASURE_RUPEES": "$28", + "TREASURE_HEART_REFILL": "$29", + "TREASURE_HEART_CONTAINER": "$2a", + "TREASURE_RING": "$2d", + "TREASURE_FLIPPERS": "$2e", + "TREASURE_POTION": "$2f", + "TREASURE_SMALL_KEY": "$30", + "TREASURE_BOSS_KEY": "$31", + "TREASURE_COMPASS": "$32", + "TREASURE_MAP": "$33", + "TREASURE_GASHA_SEED": "$34", + "TREASURE_MAKU_SEED": "$36", + "TREASURE_ORE_CHUNKS": "$37", + "TREASURE_ESSENCE": "$40", + "TREASURE_GNARLED_KEY": "$42", + "TREASURE_FLOODGATE_KEY": "$43", + "TREASURE_DRAGON_KEY": "$44", + "TREASURE_STAR_ORE": "$45", + "TREASURE_RIBBON": "$46", + "TREASURE_SPRING_BANANA": "$47", + "TREASURE_RICKY_GLOVES": "$48", + "TREASURE_BOMB_FLOWER": "$49", + "TREASURE_RUSTY_BELL": "$4a", + "TREASURE_TREASURE_MAP": "$4b", + "TREASURE_ROUND_JEWEL": "$4c", + "TREASURE_PYRAMID_JEWEL": "$4d", + "TREASURE_SQUARE_JEWEL": "$4e", + "TREASURE_X_SHAPED_JEWEL": "$4f", + "TREASURE_RED_ORE": "$50", + "TREASURE_BLUE_ORE": "$51", + "TREASURE_HARD_ORE": "$52", + "TREASURE_MEMBERS_CARD": "$53", + "TREASURE_MASTERS_PLAQUE": "$54", + "TREASURE_BOMB_FLOWER_LOWER_HALF": "$58", + "TREASURE_CUCCODEX": "$55", # Rando specific ID + "TREASURE_LON_LON_EGG": "$56", # Rando specific ID + "TREASURE_GHASTLY_DOLL": "$57", # Rando specific ID + "TREASURE_IRON_POT": "$35", # Rando specific ID + "TREASURE_LAVA_SOUP": "$38", # Rando specific ID + "TREASURE_GORON_VASE": "$39", # Rando specific ID + "TREASURE_FISH": "$3a", # Rando specific ID + "TREASURE_MEGAPHONE": "$3b", # Rando specific ID + "TREASURE_MUSHROOM": "$3c", # Rando specific ID + "TREASURE_WOODEN_BIRD": "$3d", # Rando specific ID + "TREASURE_ENGINE_GREASE": "$3e", # Rando specific ID + "TREASURE_PHONOGRAPH": "$3f", # Rando specific ID + + # Scripting + "scriptend": "$00", + "loadscript": "$83", + "jumptable_memoryaddress": "$87", + "setcollisionradii": "$8d", + "setanimation": "$8f", + "writememory": "$91", + "ormemory": "$92", + "rungenericnpc": "$97", + "showtext": "$98", + "checkabutton": "$9e", + "checkcfc0_bit0": "$a0", + "jumpifroomflagset": "$b0", + "orroomflag": "$b1", + "jumpifc6xxset": "$b3", + "writec6xx": "$b4", + "setglobalflag": "$b6", + "setdisabledobjectsto00": "$b9", + "setdisabledobjectsto11": "$ba", + "disableinput": "$bd", + "enableinput": "$be", + "callscript": "$c0", + "retscript": "$c1", + "jumpalways": "$c4", + "jumpifmemoryset": "$c7", + "jumpifmemoryeq": "$cb", + "checkcollidedwithlink_onground": "$d0", + "setcounter1": "$d7", + "loseitem": "$dc", + "spawnitem": "$dd", + "giveitem": "$de", + "jumpifitemobtained": "$df", + "asm15": "$e0", + "initcollisions": "$eb", + "movedown": "$ee", + "delay1frame": "$f0", + "delay30frames": "$f6", + "setdisabledobjectsto91": "$b8", + "showtextlowindex": "$98", + "writeobjectbyte": "$8e", + "setspeed": "$8b", + "moveup": "$ec", +} + +ASM_FILES = [ + "asm/animals.yaml", + "asm/boss_items.yaml", + "asm/collect.yaml", + "asm/combat_difficulty.yaml", + "asm/compass_chimes.yaml", + "asm/cutscenes.yaml", + "asm/file_select_custom_string.yaml", + "asm/gasha_loot.yaml", + "asm/get_item_behavior.yaml", + "asm/gfx.yaml", + "asm/impa_refill.yaml", + "asm/item_events.yaml", + "asm/layouts.yaml", + "asm/locations.yaml", + "asm/map_menu.yaml", + "asm/maku_tree.yaml", + "asm/misc.yaml", + "asm/multi.yaml", + "asm/new_game.yaml", + "asm/new_treasures.yaml", + "asm/progressives.yaml", + "asm/remove_items_on_use.yaml", + "asm/rings.yaml", + "asm/samasa_combination.yaml", + "asm/seasons_handling.yaml", + "asm/shops_handling.yaml", + "asm/subscreen_1_improvement.yaml", + "asm/static_items.yaml", + "asm/tarm_gate_requirement.yaml", + "asm/text.yaml", + "asm/triggers.yaml", + "asm/util.yaml", + "asm/vars.yaml", + "asm/warp_to_start.yaml", +] + +RUPEE_VALUES = { + 0: 0x00, + 1: 0x01, + 2: 0x02, + 5: 0x03, + 10: 0x04, + 20: 0x05, + 40: 0x06, + 30: 0x07, + 60: 0x08, + 70: 0x09, + 25: 0x0a, + 50: 0x0b, + 100: 0x0c, + 200: 0x0d, + 400: 0x0e, + 150: 0x0f, + 300: 0x10, + 500: 0x11, + 900: 0x12, + 80: 0x13, + 999: 0x14, +} + +DUNGEON_ENTRANCES = { + "d0": { + "addr": 0x13651, + "map_tile": 0xd4, + "room": 0xd4, + "group": 0x00, + "position": 0x54 + }, + "d1": { + "addr": 0x1346d, + "map_tile": 0x96, + "room": 0x96, + "group": 0x00, + "position": 0x44 + }, + "d2": { + "addr": 0x13659, + "map_tile": 0x8d, + "room": 0x8d, + "group": 0x00, + "position": 0x24 + }, + "d3": { + "addr": 0x13671, + "map_tile": 0x60, + "room": 0x60, + "group": 0x00, + "position": 0x25 + }, + "d4": { + "addr": 0x13479, + "map_tile": 0x1d, + "room": 0x1d, + "group": 0x00, + "position": 0x13 + }, + "d5": { + "addr": 0x1347d, + "map_tile": 0x8a, + "room": 0x8a, + "group": 0x00, + "position": 0x25 + }, + "d6": { + "addr": 0x13481, + "map_tile": 0x00, + "room": 0x00, + "group": 0x00, + "position": 0x34 + }, + "d7": { + "addr": 0x13485, + "map_tile": 0xd0, + "room": 0xd0, + "group": 0x00, + "position": 0x34 + }, + "d8": { + "addr": 0x1369d, + "map_tile": 0x04, + "room": 0x00, + "group": 0x01, + "position": 0x23 + }, +} + +DUNGEON_EXITS = { + "d0": 0x13909, + "d1": 0x1390d, + "d2": 0x13911, + "d3": 0x13915, + "d4": 0x13919, + "d5": 0x1391d, + "d6": 0x13921, + "d7": 0x13a89, + "d8": 0x13a8d, +} + +PORTAL_WARPS = { + "eastern suburbs portal": { + "addr": 0x134fd, + "map_tile": 0x9a, + "in_subrosia": False, + "text_index": 0x0, + }, + "spool swamp portal": { + "addr": 0x13501, + "map_tile": 0xb0, + "in_subrosia": False, + "text_index": 0x1, + }, + "mt. cucco portal": { + "addr": 0x13601, + "map_tile": 0x1e, + "in_subrosia": False, + "text_index": 0x2, + }, + "eyeglass lake portal": { + "addr": 0x13509, + "map_tile": 0xb9, + "in_subrosia": False, + "text_index": 0x3, + }, + "horon village portal": { + "addr": 0x13905, + "map_tile": 0xf7, + "in_subrosia": False, + "text_index": 0x4, + }, + "temple remains lower portal": { + "addr": 0x1350d, + "map_tile": 0x25, + "in_subrosia": False, + "text_index": 0x5, + }, + "temple remains upper portal": { + "addr": 0x1388d, + "map_tile": 0x04, + "in_subrosia": False, + "text_index": 0x6, + }, + + "volcanoes east portal": { + "addr": 0x136b5, + "map_tile": 0x05, + "in_subrosia": True, + "text_index": 0x7, + }, + "subrosia market portal": { + "addr": 0x136b9, + "map_tile": 0x3e, + "in_subrosia": True, + "text_index": 0x8, + }, + "strange brothers portal": { + "addr": 0x136bd, + "map_tile": 0x3a, + "in_subrosia": True, + "text_index": 0x9, + }, + "great furnace portal": { + "addr": 0x136c1, + "map_tile": 0x36, + "in_subrosia": True, + "text_index": 0xa, + }, + "house of pirates portal": { + "addr": 0x13729, + "map_tile": 0x4f, + "in_subrosia": True, + "text_index": 0xb, + }, + "volcanoes west portal": { + "addr": 0x136c5, + "map_tile": 0x0e, + "in_subrosia": True, + "text_index": 0xc, + }, + "d8 entrance portal": { + "addr": 0x136c9, + "map_tile": 0x16, + "in_subrosia": True, + "text_index": 0xd, + } +} + +PALETTE_BYTES = { + "green": 0x00, + "blue": 0x01, + "red": 0x02, + "orange": 0x03, +} + +# Scripting constants +DELAY_6 = 0xf6 +CALL_SCRIPT = 0xc0 +MOVE_UP = 0xec +MOVE_DOWN = 0xee +MOVE_LEFT = 0xef +MOVE_RIGHT = 0xed +WRITE_OBJECT_BYTE = 0x8e +SHOW_TEXT_LOW_INDEX = 0x98 +ENABLE_ALL_OBJECTS = 0xb9 + +DIRECTION_STRINGS = { + DIRECTION_UP: [0x15, 0x20], + DIRECTION_DOWN: [0x16, 0x20], + DIRECTION_LEFT: [0x17, 0x20], + DIRECTION_RIGHT: [0x18, 0x20], +} + +SEASON_STRINGS = { + SEASON_SPRING: [0x02, 0xde], + SEASON_SUMMER: ['S'.encode()[0], 0x04, 0xbc], + SEASON_AUTUMN: ['A'.encode()[0], 0x05, 0x25], + SEASON_WINTER: [0x03, 0x7e] +} \ No newline at end of file diff --git a/worlds/tloz_oos/patching/Functions.py b/worlds/tloz_oos/patching/Functions.py new file mode 100644 index 000000000000..1f603d0731f2 --- /dev/null +++ b/worlds/tloz_oos/patching/Functions.py @@ -0,0 +1,703 @@ +import os +import random +from typing import List +import Utils +from settings import get_settings +from . import RomData +from .Util import * +from .z80asm.Assembler import Z80Assembler +from .Constants import * +from ..data.Constants import * +from .. import LOCATIONS_DATA, OracleOfSeasonsOldMenShuffle, OracleOfSeasonsGoal, OracleOfSeasonsAnimalCompanion, \ + OracleOfSeasonsMasterKeys, OracleOfSeasonsFoolsOre, OracleOfSeasonsGoldenOreSpotsShuffle +from pathlib import Path + + +def get_asm_files(patch_data): + asm_files = ASM_FILES.copy() + if patch_data["options"]["quick_flute"]: + asm_files.append("asm/conditional/quick_flute.yaml") + if patch_data["options"]["shuffle_old_men"] == OracleOfSeasonsOldMenShuffle.option_turn_into_locations: + asm_files.append("asm/conditional/old_men_as_locations.yaml") + if patch_data["options"]["remove_d0_alt_entrance"]: + asm_files.append("asm/conditional/remove_d0_alt_entrance.yaml") + if patch_data["options"]["remove_d2_alt_entrance"]: + asm_files.append("asm/conditional/remove_d2_alt_entrance.yaml") + if patch_data["options"]["goal"] == OracleOfSeasonsGoal.option_beat_ganon: + asm_files.append("asm/conditional/ganon_goal.yaml") + if patch_data["options"]["shuffle_essences"]: + asm_files.append("asm/conditional/essence_sanity.yaml") + if get_settings()["tloz_oos_options"]["remove_music"]: + asm_files.append("asm/conditional/mute_music.yaml") + return asm_files + + +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(): + # Some very specific chests don't have the "COLLECT_CHEST" type but still are chests + is_special_chest = location_name in ["Sunken City: Chest in Master Diver's Cave", + "Explorer's Crypt (1F): Chest Behind Cracked Wall"] + if not is_special_chest and ('collect' not in location_data or location_data['collect'] != COLLECT_CHEST): + continue + chest_addr = rom.get_chest_addr(location_data['room']) + item = patch_data["locations"][location_name] + item_id, item_subid = get_item_id_and_subid(item) + rom.write_byte(chest_addr, item_id) + rom.write_byte(chest_addr + 1, item_subid) + + +def define_samasa_combination(assembler: Z80Assembler, patch_data): + 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_sign_guy_requirement_digits(assembler: Z80Assembler, requirement: int): + digits = [] + while requirement > 0: + digits.append(0x30 + (requirement % 10)) + requirement = int(requirement / 10) + # If list is empty, it means requirement was <= 0, so just display "0" + if len(digits) == 0: + digits = [0x30] + assembler.add_floating_chunk("signGuyRequirementDigits", list(reversed(digits))) + + +def define_compass_rooms_table(assembler: Z80Assembler, patch_data): + table = [] + for location_name, item in patch_data["locations"].items(): + item_id, item_subid = get_item_id_and_subid(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): + """ + 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 falling small keys + item_id, _ = get_item_id_and_subid(item) + if mode == COLLECT_DROP and item_id == 0x30: + 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]) + + table.append(0xff) + assembler.add_floating_chunk("collectPropertiesTable", table) + + +def define_additional_tile_replacements(assembler: Z80Assembler, patch_data): + """ + 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 = [] + # Reveal hidden subrosia digging spots if required + if get_settings()["tloz_oos_options"]["reveal_hidden_subrosia_digging_spots"]: + table.extend([ + 0x01, 0x06, 0x00, 0x18, 0x2f, # Bath digging spot + 0x01, 0x57, 0x00, 0x38, 0x2f, # Market portal digging spot + 0x01, 0x47, 0x00, 0x33, 0x2f, # Hard-working Subrosian digging spot + 0x01, 0x3a, 0x00, 0x46, 0x2f, # Temple of Seasons digging spot + 0x01, 0x07, 0x00, 0x13, 0x2f, # Northern volcanoes digging spot + 0x01, 0x20, 0x00, 0x68, 0x2f, # D8 portal digging spot + 0x01, 0x42, 0x00, 0x14, 0x2f # Western volcanoes digging spot + ]) + # If D0 alternate entrance is removed, put stairs inside D0 to make chest reachable without the alternate entrance + if patch_data["options"]["remove_d0_alt_entrance"] > 0: + table.extend([0x04, 0x05, 0x00, 0x5a, 0x53]) + # 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, 0x04, # North Horon: Gasha Spot Above Impa + 0x00, 0xc8, 0x20, 0x67, 0x04, # Horon Village: Gasha Spot Near Mayor's House + 0x00, 0xac, 0x20, 0x27, 0x04, # Eastern Suburbs: Gasha Spot + 0x00, 0x95, 0x20, 0x32, 0x04, # Holodrum Plain: Gasha Spot Near Mrs. Ruul's House + 0x00, 0x75, 0x20, 0x34, 0x04, # Holodrum Plain: Gasha Spot on Island Above D1 + 0x00, 0x80, 0x20, 0x53, 0x04, # Spool Swamp: Gasha Spot Near Floodgate Keyhole + 0x00, 0xc0, 0x20, 0x61, 0x04, # Spool Swamp: Gasha Spot Near Portal + 0x00, 0x3f, 0x20, 0x44, 0x04, # Sunken City: Gasha Spot + 0x00, 0x1f, 0x20, 0x21, 0x12, # Mt. Cucco: Gasha Spot + 0x00, 0x38, 0x20, 0x25, 0x04, # Goron Mountain: Gasha Spot Left of Entrance + 0x00, 0x3b, 0x20, 0x53, 0x12, # Goron Mountain: Gasha Spot Right of Entrance + 0x00, 0x89, 0x20, 0x24, 0x04, # Eyeglass Lake: Gasha Spot Near D5 + 0x00, 0x22, 0x20, 0x45, 0x04, # Tarm Ruins: Gasha Spot + 0x00, 0xf0, 0x20, 0x22, 0x12, # Western Coast: Gasha Spot South of Graveyard + 0x00, 0xef, 0x20, 0x66, 0xaf, # Samasa Desert: Gasha Spot + 0x00, 0x44, 0x20, 0x44, 0x04, # Path to Onox Castle: Gasha Spot + ]) + assembler.add_floating_chunk("additionalTileReplacements", table) + +def define_location_constants(assembler: Z80Assembler, patch_data): + # 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"] = "Potion" + + # 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: + item = {"item": location_data["vanilla_item"]} + + item_id, item_subid = get_item_id_and_subid(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) + deterministic_gasha_table.extend([item_id, item_subid]) + assembler.add_floating_chunk("deterministicGashaLootTable", deterministic_gasha_table) + + +def define_option_constants(assembler: Z80Assembler, patch_data): + 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.startingPos", 0x55) + assembler.define_byte("option.startingSeason", patch_data["default_seasons"]["EYEGLASS_LAKE"]) + assembler.define_byte("option.startingMapsCompasses", patch_data["options"]["starting_maps_compasses"]) + + 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.warpToStart", options["warp_to_start"]) + + 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"]) + define_sign_guy_requirement_digits(assembler, 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) + + +def define_season_constants(assembler: Z80Assembler, patch_data): + 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, patch_data): + 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) + assembler.add_floating_chunk("text.lostWoodsPedestalSequence", 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) + assembler.add_floating_chunk("text.lostWoodsMainSequence", main_text) + + +def process_lost_woods_sequence(sequence): + """ + 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 = [] + text_bytes = [] + for i in range(4): + direction = sequence[i][0] + season = sequence[i][1] + sequence_bytes.extend(sequence[i]) + text_bytes.extend(DIRECTION_STRINGS[direction]) + text_bytes.extend(SEASON_STRINGS[season]) + if i != 3: + text_bytes.extend([0x05, 0x56]) # wait for input + newline + text_bytes.append(0x00) + return sequence_bytes, text_bytes + + +def get_treasure_addr(rom: RomData, item_name: str): + item_id, item_subid = get_item_id_and_subid({"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_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_treasure_types(rom: RomData): + # 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, "Fool's Ore", 0x36, 0x4a) + set_treasure_data(rom, "Rare Peach Stone", None, 0x3f) + set_treasure_data(rom, "Ribbon", 0x41, 0x4c) + set_treasure_data(rom, "Treasure Map", 0x6c, 0x49) + set_treasure_data(rom, "Member's Card", 0x45, 0x48) + set_treasure_data(rom, "Potion", 0x6d, 0x4b) + + # Set data for remote Archipelago items + set_treasure_data(rom, "Archipelago Item", 0x57, 0x53) + set_treasure_data(rom, "Archipelago Progression Item", 0x57, 0x52) + + # 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) + + # Colored Rod of Seasons to make them recognizable + set_treasure_data(rom, "Rod of Seasons (Spring)", None, 0x4f) + set_treasure_data(rom, "Rod of Seasons (Autumn)", None, 0x50) + set_treasure_data(rom, "Rod of Seasons (Winter)", None, 0x51) + + +def set_old_men_rupee_values(rom: RomData, patch_data): + 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): + # 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(0x24f39, 0xa7) + rom.write_byte(0x24f3b, 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(0x20cfa, 0x00) + + if patch_data["options"]["master_keys"] != OracleOfSeasonsMasterKeys.option_disabled: + # Remove small key consumption on keydoor opened + rom.write_byte(0x18357, 0x00) + # Change obtention text + rom.write_bytes(0x7546f, [0x02, 0xe5, 0x20, 0x4b, 0x65, 0x79, 0x05, 0xD8, 0x00]) + if patch_data["options"]["master_keys"] == OracleOfSeasonsMasterKeys.option_all_dungeon_keys: + # Remove boss key consumption on boss keydoor opened + rom.write_word(0x1834f, 0x0000) + + +def set_fixed_subrosia_seaside_location(rom: RomData, patch_data): + """ + 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): + 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"ARCHIPELAGO {VERSION}".ljust(16, " ")] + 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 process_item_name_for_shop_text(item: Dict) -> List[int]: + if "player" in item: + item_name = f"{item['player']}'s {item['item']}" + else: + item_name = item["item"] + + 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:] + + # If name is more than 2 lines long, discard excess lines and put an ellipsis to suggest content was truncated + if len(lines) > 2: + lines = lines[0:2] + lines[1] = lines[1][0:15] + "." + + result = [] + for line in lines: + if len(line) > 16: + line = line[0:15] + "." + if len(result) > 0: + result.append(0x01) # Newline + result.extend(line.encode()) + return result + + +def define_text_constants(assembler: Z80Assembler, patch_data): + # Holodrum shop slots + overworld_shops = [ + "Horon Village: Shop", + "Horon Village: Member's Shop", + "Sunken City: Syrup Shop", + "Horon Village: 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"] + if location_name not in patch_data["locations"]: + continue + item_name_bytes = process_item_name_for_shop_text(patch_data["locations"][location_name]) + + text_bytes = [0x09, 0x01] + item_name_bytes + [0x03, 0xe2] # Item name + text_bytes.extend([0x20, 0x0c, 0x08, 0x02, 0x8f, 0x01]) # Price + text_bytes.extend([0x02, 0x00, 0x00]) # OK / No thanks + assembler.add_floating_chunk(f"text.{symbolic_name}", text_bytes) + + # Subrosian market slots + 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_name_bytes = process_item_name_for_shop_text(patch_data["locations"][location_name]) + text_bytes = [0x09, 0x01] + item_name_bytes + [0x03, 0xe2] # (Item name) + text_bytes.extend([0x02, 0x08]) # "I'll trade for" + if market_slot == 1: + text_bytes.extend([0x02, 0x8e, 0x2e, 0x01]) # "Star-Shaped Ore." + else: + text_bytes.extend([0x09, 0x01, 0x0c, 0x08, 0x20, 0x02, 0x09, 0x2e, 0x01]) # "(number) Ore Chunks." + assembler.add_floating_chunk(f"text.{symbolic_name}", text_bytes) + + +def set_heart_beep_interval_from_settings(rom: RomData): + 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): + 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_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_oos_options"]["character_palette"] + if palette == "random": + colors = list(PALETTE_BYTES.keys()) + palette = colors[random.randint(0, len(colors) - 1)] + 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(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): + 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): + 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)) + + +def set_portal_warps(rom: RomData, patch_data): + 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(assembler: Z80Assembler, patch_data): + for i in range(9): + if i == 0: + # " for\nHero's Cave" + dungeon_precision = [0x02, 0xe2, 0x03, 0x78] + else: + # " for\nDungeon X" + dungeon_precision = [0x02, 0xe2, 0x44, 0x05, 0x8a, 0x20, (0x30 + i)] + + # ###### Small keys ############################################## + # "You found a\n\color(RED)" + small_key_text = [0x05, 0x9d, 0x02, 0x78, 0x61, 0x01, 0x09, 0x01] + if patch_data["options"]["master_keys"]: + # "Master Key" + small_key_text.extend([0x02, 0xe5, 0x20, 0x4b, 0x65, 0x79]) + else: + # "Small Key" + small_key_text.extend([0x53, 0x6d, 0x04, 0x07, 0x4b, 0x65, 0x79]) + if patch_data["options"]["keysanity_small_keys"]: + small_key_text.extend(dungeon_precision) + small_key_text.extend([0x05, 0xd8, 0x00]) # "\color(WHITE)!(end)" + assembler.add_floating_chunk(f"text.smallKeyD{i}", 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" + boss_key_text = [ + 0x05, 0x9d, 0x02, 0x78, 0x04, 0xa7, + 0x09, 0x01, 0x42, 0x6f, 0x73, 0x73, 0x20, 0x4b, 0x65, 0x79 + ] + if patch_data["options"]["keysanity_boss_keys"]: + boss_key_text.extend(dungeon_precision) + boss_key_text.extend([0x05, 0xd8, 0x00]) # "\color(WHITE)!(end)" + assembler.add_floating_chunk(f"text.bossKeyD{i}", boss_key_text) + + # ###### Dungeon maps ############################################## + # "You found the\n\color(RED)" + dungeon_map_text = [0x05, 0x9d, 0x02, 0x78, 0x04, 0xa7, 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([0x05, 0xd8, 0x00]) # "\color(WHITE)!(end)" + assembler.add_floating_chunk(f"text.dungeonMapD{i}", dungeon_map_text) + + # ###### Compasses ############################################## + # "You found the\n\color(RED)Compass" + compasses_text = [ + 0x05, 0x9d, 0x02, 0x78, 0x04, 0xa7, + 0x09, 0x01, 0x43, 0x6f, 0x6d, 0x05, 0x11 + ] + if patch_data["options"]["keysanity_maps_compasses"]: + compasses_text.extend(dungeon_precision) + compasses_text.extend([0x05, 0xd8, 0x00]) # "\color(WHITE)!(end)" + assembler.add_floating_chunk(f"text.compassD{i}", compasses_text) diff --git a/worlds/tloz_oos/patching/ProcedurePatch.py b/worlds/tloz_oos/patching/ProcedurePatch.py new file mode 100644 index 000000000000..0e3e2f963036 --- /dev/null +++ b/worlds/tloz_oos/patching/ProcedurePatch.py @@ -0,0 +1,95 @@ +import hashlib +import pkgutil + +import yaml +from worlds.Files import APProcedurePatch, APTokenMixin, APPatchExtension + +from .Functions import * +from .Constants import * +from .RomData import RomData +from .z80asm.Assembler import Z80Assembler, Z80Block + + +class OoSPatchExtensions(APPatchExtension): + game = "The Legend of Zelda - Oracle of Seasons" + + @staticmethod + def apply_patches(caller: APProcedurePatch, rom: bytes, patch_file: str) -> bytes: + rom_data = RomData(rom) + patch_data = yaml.load(caller.get_file(patch_file).decode("utf-8"), yaml.Loader) + + if patch_data["version"] != VERSION: + raise Exception(f"Invalid version: this seed was generated on v{patch_data['version']}, " + f"you are currently using v{VERSION}") + + assembler = Z80Assembler(EOB_ADDR, DEFINES) + + # Define assembly constants & floating chunks + define_location_constants(assembler, patch_data) + define_option_constants(assembler, patch_data) + define_season_constants(assembler, patch_data) + define_text_constants(assembler, patch_data) + define_compass_rooms_table(assembler, patch_data) + define_collect_properties_table(assembler, patch_data) + define_additional_tile_replacements(assembler, patch_data) + define_samasa_combination(assembler, patch_data) + define_dungeon_items_text_constants(assembler, patch_data) + define_lost_woods_sequences(assembler, patch_data) + set_file_select_text(assembler, caller.player_name) + + # Parse assembler files, compile them and write the result in the ROM + print(f"Compiling ASM files...") + for file_path in get_asm_files(patch_data): + data_loaded = yaml.safe_load(pkgutil.get_data(__name__, file_path)) + for metalabel, contents in data_loaded.items(): + assembler.add_block(Z80Block(metalabel, contents)) + assembler.compile_all() + for block in assembler.blocks: + rom_data.write_bytes(block.addr.address_in_rom(), block.byte_array) + + # Perform direct edits on the ROM + alter_treasure_types(rom_data) + write_chest_contents(rom_data, patch_data) + set_old_men_rupee_values(rom_data, patch_data) + set_dungeon_warps(rom_data, patch_data) + set_portal_warps(rom_data, patch_data) + apply_miscellaneous_options(rom_data, patch_data) + set_fixed_subrosia_seaside_location(rom_data, patch_data) + + # Apply cosmetic settings + set_heart_beep_interval_from_settings(rom_data) + set_character_sprite_from_settings(rom_data) + inject_slot_name(rom_data, caller.player_name) + + rom_data.update_checksum(0x14e) + return rom_data.output() + + +class OoSProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [ROM_HASH] + patch_file_ending: str = ".apoos" + result_file_ending: str = ".gbc" + + game = "The Legend of Zelda - Oracle of Seasons" + procedure = [ + ("apply_patches", ["patch.dat"]) + ] + + @classmethod + def get_source_data(cls) -> bytes: + base_rom_bytes = getattr(cls, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_settings().tloz_oos_options["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + + base_rom_bytes = bytes(open(file_name, "rb").read()) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if ROM_HASH != basemd5.hexdigest(): + raise Exception("Supplied ROM does not match known MD5 for Oracle of Seasons US version." + "Get the correct game and version, then dump it.") + setattr(cls, "base_rom_bytes", base_rom_bytes) + return base_rom_bytes + diff --git a/worlds/tloz_oos/patching/RomData.py b/worlds/tloz_oos/patching/RomData.py new file mode 100644 index 000000000000..948e658e6d33 --- /dev/null +++ b/worlds/tloz_oos/patching/RomData.py @@ -0,0 +1,73 @@ +from collections.abc import Collection +from typing import Optional + +from worlds.tloz_oos.patching.Util import hex_str + + +class RomData: + buffer: bytearray + + def __init__(self, file: bytes, name: Optional[str] = None) -> None: + self.file = bytearray(file) + self.name = name + + def read_bit(self, address: int, bit_number: int) -> bool: + bitflag = (1 << bit_number) + return (self.buffer[address] & bitflag) != 0 + + def read_byte(self, address: int) -> int: + return self.file[address] + + def read_bytes(self, start_address: int, length: int) -> bytearray: + return self.file[start_address:start_address + length] + + def read_word(self, address: int): + word_bytes = self.read_bytes(address, 2) + return (word_bytes[1] * 0x100) + word_bytes[0] + + def write_byte(self, address: int, value: int) -> None: + self.file[address] = value + + def write_bytes(self, start_address: int, values: Collection[int]) -> None: + self.file[start_address:start_address + len(values)] = values + + def write_word(self, address: int, value: int) -> None: + value = value & 0xFFFF + self.write_bytes(address, [value & 0xFF, (value >> 8) & 0xFF]) + + def write_word_be(self, address: int, value: int) -> None: + value = value & 0xFFFF + self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF]) + + def update_checksum(self, address): + """ + Updates the 16-bit checksum for ROM data located in the rom header. + This is calculated by summing the non-global-checksum bytes in the rom. + This must not be confused with the header checksum, which is the byte before. + """ + result = 0 + for b in self.read_bytes(0x0, address): + result += b + for b in self.read_bytes(address + 2, 0xffffff): + result += b + result &= 0xffff + self.write_word_be(address, result & 0xffff) + + def get_chest_addr(self, group_and_room: int): + """ + Return the address where to edit item ID and sub-ID to modify the contents + of the chest contained in given room of given group + """ + base_addr = 0x54f6c + room = group_and_room & 0xFF + group = group_and_room >> 8 + current_addr = 0x50000 + self.read_word(base_addr + (group * 2)) + while self.read_byte(current_addr) != 0xff: + chest_room = self.read_byte(current_addr + 1) + if chest_room == room: + return current_addr + 2 + current_addr += 4 + raise Exception(f"Unknown chest in room {group}|{hex_str(room)}") + + def output(self) -> bytes: + return bytes(self.file) diff --git a/worlds/tloz_oos/patching/Util.py b/worlds/tloz_oos/patching/Util.py new file mode 100644 index 000000000000..e39ac164de98 --- /dev/null +++ b/worlds/tloz_oos/patching/Util.py @@ -0,0 +1,38 @@ +from typing import Dict + +from .Constants import * +from ..data import ITEMS_DATA + + +def camel_case(text): + if len(text) == 0: + return text + s = text.replace("-", " ").replace("_", " ").split() + return s[0] + ''.join(i.capitalize() for i in s[1:]) + + +def get_item_id_and_subid(item: Dict): + # Remote item, use the generic "Archipelago Item" + if item["item"] == "Archipelago Item" or ("player" in item and not item["progression"]): + return 0x41, 0x00 + if item["item"] == "Archipelago Progression Item" or ("player" in item and item["progression"]): + return 0x41, 0x01 + + # Local item, put the real item in there + item_data = ITEMS_DATA[item["item"]] + item_id = item_data["id"] + item_subid = item_data["subid"] if "subid" in item_data else 0x00 + return item_id, item_subid + + +def hex_str(value, size=1, min_length=0): + if value < 0: + if size == 1: + value += 0x100 + elif size == 2: + value += 0x10000 + else: + raise Exception("Invalid size (should be 1 or 2)") + if min_length == 0: + min_length = size * 2 + return hex(value)[2:].rjust(min_length, "0") diff --git a/worlds/tloz_oos/patching/__init__.py b/worlds/tloz_oos/patching/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/worlds/tloz_oos/patching/asm/animals.yaml b/worlds/tloz_oos/patching/asm/animals.yaml new file mode 100644 index 000000000000..4e153280a52b --- /dev/null +++ b/worlds/tloz_oos/patching/asm/animals.yaml @@ -0,0 +1,163 @@ + +# data for checkSetAnimalSavePoint: +# source room, animal room, saved y, saved x. +04//animalSavePointTable: | + db $c2,$c2,$18,$68 ; spool swamp cave + db $29,$2a,$38,$18 ; goron mountain east cave + db $8e,$8e,$58,$88 ; cave outside d2 + db $87,$86,$48,$68 ; cave north of d1 + db $28,$2a,$38,$18 ; goron mountain main + db $0f,$2f,$18,$68 ; spring banana cave + db $1f,$2f,$18,$68 ; cave below spring banana cave + db $9a,$9a,$38,$48 ; rosa portal + db $8d,$8d,$38,$38 ; d2 entrance + db $ff + +# if entering certain warps blocked by snow piles, mushrooms, or bushes, set +# the animal companion to appear right outside the entrance instead of where +# you left them. +04//checkSetAnimalSavePoint: | + push bc + push de + ld a,(wActiveRoom) + ld b,a + ld a,(wRememberedCompanionRoom) + ld c,a + ld e,$02 + ld hl,animalSavePointTable + call searchDoubleKey + jr nc,.done + ld de,wRememberedCompanionY + ldi a,(hl) + ld (de),a + inc de + ld a,(hl) + ld (de),a + + .done: + pop de + pop bc + ld a,(wWarpDestRoom) + ret +04/061e/: call checkSetAnimalSavePoint + +# vanilla game doesn't save non-natzu animal positions when it thinks you +# won't need them anymore. stop that. +05/05c9/: jr $28 + +# do this so that animals don't immediately stop walking onscreen when called +# on a bridge, namely the one to/from d1. +05//animalEntryIgnoreBridges: | + call $44aa ; _specialObjectGetRelativeTileWithDirectionTable + or a + ret z + cp $1a + ret z + cp $1b + ret +05/093b/: | + call animalEntryIgnoreBridges + nop +05/31ea/: | + call animalEntryIgnoreBridges + nop + +# this is called to make moosh unrideable on mt cucco in the case of not +# having flute in a moosh seed. +05//checkFlute: | + ld a,TREASURE_FLUTE + jp checkTreasureObtained +05/376b/: call checkFlute +05/3a65/: call checkFlute + +# animals called by flute normally veto any nonzero collision value for the +# purposes of entering a screen, but this allows double-wide bridges (1a and +# 1b) as well. this specifically fixes the problem of not being able to call +# an animal on the d1 screen, or on the bridge to the screen to the right. +# the vertical collision check isn't modified, since bridges only run +# horizontally. +09//checkFluteCollisions: | + ld b,$01 + ld a,(hl) + cp $1a + jr z,@firstTileMatched + cp $1b + jr z,@firstTileMatched + or a + ret nz + + @firstTileMatched: + ld a,l + add a,b + ld l,a + ld a,(hl) + cp $1a + jr z,@secondTileMatched + cp $1b + jr z,@secondTileMatched + or a + + @secondTileMatched: + ld a,l + ret nz + call convertShortToLongPosition + xor a + ret +09/0d9a/: call checkFluteCollisions +09/0dad/: call checkFluteCollisions + +# some of the ricky code here doesn't matter since ricky's flute isn't +# currently randomized (6cefde1), but it can stay in case things change. + +# check flute icon instead of animal region for dimitri events: +# spawning dimitri + kids in sunken +09/0e4b/: | + ld a,($c6af) + cp $02 +09/2f07/: | + ld a,($c6af) + cp $02 +09/337d/: | + ld a,($c6af) + cp $02 +# trying to leave sunken w/ dimitri +09/2f34/: | + ld a,($c6af) + cp $02 + +# check flute icon instead of animal region for ricky events: +# spawn ricky in pen +09/0e76/: | + ld a,(wFluteIcon) + cp $01 +# say goodbye when reaching spool +09/2ccc/: | + ld a,(wFluteIcon) + cp $01 + +# prevent subrosian dancing from giving dimitri's flute. +09/1e37/: or $20 + +# stop ricky from giving his flute. +09/2e6c/: ret +0b/2b77/: | + db jumpifmemoryeq + dw wAnimalRegion + db $7f + +# prevent holodrum plain from changing the animal region when entered. +09/2f79/: jr $08 + +# remove the moosh and dimitri events in spool swamp. +11/2572/: db $ff +11/28d4/: db $ff + +# add proper treasure entries for each flute, like ages has. +15//fluteTreasureData: | + db $0a,$0b,$38,$3c + db $0a,$0c,$39,$3d + db $0a,$0d,$3a,$3e +15/1161/: | + db $80 + dw fluteTreasureData + db $00 diff --git a/worlds/tloz_oos/patching/asm/boss_items.yaml b/worlds/tloz_oos/patching/asm/boss_items.yaml new file mode 100644 index 000000000000..622b8bfb3294 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/boss_items.yaml @@ -0,0 +1,36 @@ +# entries (ID, subID) indexed by wDungeon Index +15//bossItemTable: | + db $00,$00 ; Hero's Cave doesn't have a boss + dwbe locations.d1Boss + dwbe locations.d2Boss + dwbe locations.d3Boss + dwbe locations.d4Boss + dwbe locations.d5Boss + dwbe locations.d6Boss + dwbe locations.d7Boss + dwbe locations.d8Boss + +# spawn items from bossItemTable in place of boss heart containers. +15//spawnBossItem: | + push hl + ld hl,bossItemTable + ld a,(wDungeonIndex) + cp $0c ; ages d6 past + jr nz,@next + ld a,$06 + @next: + rst 18 + ld b,(hl) + inc hl + ld c,(hl) + call createTreasure + call objectCopyPosition + pop hl + ret + +0b/0b8f/: | + db asm15 + dw spawnBossItem +0b/0bb1/: | + db asm15 + dw spawnBossItem diff --git a/worlds/tloz_oos/patching/asm/collect.yaml b/worlds/tloz_oos/patching/asm/collect.yaml new file mode 100644 index 000000000000..5bae5c801983 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/collect.yaml @@ -0,0 +1,141 @@ +# calls lookupCollectMode_body in another bank. +00//lookupCollectMode: | + push bc + push de + push hl + ld e,$06 + ld hl,lookupCollectMode_body + call interBankCall + ld a,e + pop hl + cp $ff + jr nz,@next + dec hl + ldi a,(hl) + @next: + pop de + pop bc + ret + +# return a spawning item's collection mode in a and e, based on current room. +# the table format is (group, room, mode), and modes 80+ are used to index a +# jump table for special cases. if no match is found, it returns the regular, +# non-overriden mode. does nothing if the item's collect mode is already set. +06//collectPropertiesTable: /include collectPropertiesTable +06//lookupCollectMode_body: | + ld e,$71 + ld a,(de) + ld e,a + and a + ret nz + ld a,(wActiveGroup) + ld b,a + ld a,(wActiveRoom) + ld c,a + ld e,$01 + ld hl,collectPropertiesTable + call searchDoubleKey + ld a,$00 ; Don't use "xor a" here since it would affect C flag! + ld e,$02 + ret nc + + ld a,(hl) + ld e,a + cp $80 + ret c + + ld hl,collectSpecialJumpTable + and $7f + add a,a + rst 10 + ldi a,(hl) + ld h,(hl) + ld l,a + jp (hl) + +# Add a special bypass to not show textboxes for Small Keys dropping from ceiling +09//bypassKeydropsTextbox: | + ; Don't bypass anything if keysanity is on, since we want the textbox to indicate for which dungeon + ; that key was for. + ld a,option.keysanity_small_keys + or a + jr nz,@regularText + + ld e,$71 ; var31, containing spawn mode + ld a,(de) + cp $02 + jr nz,@regularText ; jump if not a drop from ceiling + + ld e,$72 ; var32, containing grab mode + ld a,(de) + or a + jr nz,@regularText ; jump if grab triggers an animation change + + ; It's a drop not triggering an animation change ==> it's a keydrop, so skip its textbox (0xFF) + ld a,$ff + ret + + ; return regular text ID otherwise + @regularText: + ld e,$75 ; var35, containing text id + ld a,(de) + ret +09/02ef/: call bypassKeydropsTextbox + +# collect modes starting at 80 index this jump table to determine the actual +# mode. +06//collectSpecialJumpTable: | + dw collectDiverRoom + dw collectPoeSkipRoom + dw collectMakuTree + dw collectD5Armos + +# master diver's room has chest on the left and reward on the right. +06//collectDiverRoom: | + ld e,$4d + ld a,(de) ; object x position + cp $80 + ld e,COLLECT_CHEST + ret c + ld e,COLLECT_PICKUP_NOFLAG + ret + +# bombed wall chest in d7 has an item drop on the left side. +06//collectPoeSkipRoom: | + ld e,$4d + ld a,(de) ; object x position + cp $80 + ld e,COLLECT_FALL_KEY + ret c + ld e,COLLECT_CHEST + ret + +# maku tree item drops at a specific script pos, otherwise use regular mode. +06//collectMakuTree: | + ld a,($d258) ; script position + cp $8e + ld e,COLLECT_FALL + ret z + ld e,COLLECT_PICKUP + ret + +# when the falling item hits the water, it creates a new item interaction. +# that one should have the mode that requires diving to get the item. +# 06//collectD4Pool: | +# ld e,$54 +# ld a,(de) ; object z position +# sub a,$01 +# ld e,COLLECT_FALL_KEY +# ret c +# ld e,COLLECT_DIVE +# ret + +# the first three chests opened in the d5 armos room shouldn't set the room's +# "item obtained" flag. +06//collectD5Armos: | + ld a,($cfd8) + cp $04 + ld e,COLLECT_CHEST + ret z + ld e,COLLECT_CHEST_NOFLAG + ret diff --git a/worlds/tloz_oos/patching/asm/combat_difficulty.yaml b/worlds/tloz_oos/patching/asm/combat_difficulty.yaml new file mode 100644 index 000000000000..e60832454766 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/combat_difficulty.yaml @@ -0,0 +1,7 @@ +06//alterDamageReceived: | + add a,option.receivedDamageModifier + ld b,a + ld (de),a + ld hl,$468d ; @ringDamageModifierTable + jp $4683 ; @writeDamageToApply +06/064d/: call alterDamageReceived diff --git a/worlds/tloz_oos/patching/asm/compass_chimes.yaml b/worlds/tloz_oos/patching/asm/compass_chimes.yaml new file mode 100644 index 000000000000..20e52bdd8350 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/compass_chimes.yaml @@ -0,0 +1,226 @@ +# The following blocks are the actual compass chime sound data that describe notes +# and modifiers being applied on the sound chip. We inject those in bank 2 since +# it's a pretty unused bank. +# Those were extracted from Stewmath's randomizer, so credit goes to whoever +# composed those (most likely Stewmath). +02//sndCompassD1: | + db F6,02 + db D9,2F,05,D3,2F,05 + db D9,2E,05,D3,2E,05 + db D9,2C,05,D3,2C,05 + db D9,2A,05,D3,2A,05 + db D9,29,05,D3,29,05 + db 60,10 + db D9,2D,05,D3,2D,05 + db D9,2C,05,D3,2C,05 + db D9,2A,05,D3,2A,05 + db D9,29,05,D3,29,05 + db D9,27,05,D3,27,05 + db FF +02//sndCompassD2: | + db F6,02 + db D9,26,06,D3,26,06 + db D9,27,06,D3,27,06 + db D9,2D,06,D3,2D,06 + db D9,32,06,D3,32,06 + db 60,0C + db D9,30,06,D3,30,06 + db FF +02//sndCompassD3: | + db F6,02 + db D9,28,06,D3,28,06 + db D9,2F,06,D3,2F,06 + db D9,28,06,D3,28,06 + db D9,27,06,D3,27,06 + db D9,2E,06,D3,2E,06 + db 60,0C + db D9,35,06,D3,35,06 + db D9,32,06,D3,32,06 + db FF +02//sndCompassD4: | + db F6,02 + db D9,21,06,D3,21,06 + db D9,21,05,D3,21,01 + db D9,24,05,D3,24,01 + db D9,21,05,D3,21,01 + db D9,24,05,D3,24,01 + db D9,2B,05,D3,2B,01 + db D9,2A,06,D3,2A,06 + db FF +02//sndCompassD5: | + db F6,02 + db D9,1E,06,D3,1E,06 + db D9,1E,05,D3,1E,01 + db D9,1E,05,D3,1E,01 + db D9,25,06,D3,25,06 + db 60,0C + db D9,24,06,D3,24,06 + db D9,21,05,D3,21,01 + db D9,1E,05,D3,1E,01 + db D9,28,06,D3,28,06 + db FF +02//sndCompassD6: | + db F6,02 + db D9,2A,06,D3,2A,06 + db D9,31,06,D3,31,06 + db D9,30,06,D3,30,06 + db 60,0C + db D9,2C,05,D3,2C,01 + db D9,2D,05,D3,2D,01 + db D9,2F,05,D3,2F,01 + db D9,2D,05,D3,2D,01 + db D9,2C,06,D3,2C,06 + db FF +02//sndCompassD7: | + db F6,02 + db D9,26,06,D3,26,06 + db D9,29,06,D3,29,06 + db D9,28,06,D3,28,06 + db D9,27,06,D3,27,06 + db 60,0C + db D9,26,06,D3,26,06 + db FF +02//sndCompassD8: | + db F6,02 + db D9,24,06,D3,24,06 + db D9,25,06,D3,25,06 + db D9,24,06,D3,24,06 + db D9,25,06,D3,25,06 + db D9,24,05,D3,24,01 + db D9,24,05,D3,24,01 + db D9,24,05,D3,24,01 + db 60,06 + db D9,24,06,D3,24,06 + db FF + +# The following blocks inject the description (into what would be "soundChannelPointers.s") +# for the new dungeon-specific compass chime sound channels. The first byte describes +# the audio channel and priority, and the word is a pointer to the actuel sound +# data (inside the bank that was explicited in "soundPointers.s"). +# Bank 39 being extremely packed, we are forced to overwrite unused sound & music +# data to describe those new sounds. + +# Injected in place of "sndd6"-"snddd", unused sound descriptors +39/115d/sndCompassD1Ptr: | + db $b2 + dw sndCompassD1 + db $ff +39/1161/sndCompassD2Ptr: | + db $b2 + dw sndCompassD2 + db $ff +39/1165/sndCompassD3Ptr: | + db $b2 + dw sndCompassD3 + db $ff +# Injected in place of "mus41", unused music descriptor +39/145c/sndCompassD4Ptr: | + db $b2 + dw sndCompassD4 + db $ff +39/1460/sndCompassD5Ptr: | + db $b2 + dw sndCompassD5 + db $ff +39/1464/sndCompassD6Ptr: | + db $b2 + dw sndCompassD6 + db $ff +# Injected in place of unused data at the end of "soundPointers.s" +39/1a79/sndCompassD7Ptr: | + db $b2 + dw sndCompassD7 + db $ff +39/1a7d/sndCompassD8Ptr: | + db $b2 + dw sndCompassD8 + db $ff +# There a 5 more unused bytes there, letting some space for another sound injection + +# The following blocks replace the pointers from "soundPointers.s" to make them +# point on our new sounds data instead of dummy values. +# The first byte corresponds to the bank where the actual sound data is (NOT the bank +# where the second word points!), to which 0x39 is subtracted. This leads to weird +# bank numbers, but don't worry, it works. +39/1a4b/: | # sound id D4 + db $c9 ; bank 2 + dw sndCompassD1Ptr +39/1a4e/: | # sound id D5 + db $c9 ; bank 2 + dw sndCompassD2Ptr +39/1a51/: | # sound id D6 + db $c9 ; bank 2 + dw sndCompassD3Ptr +39/1a54/: | # sound id D7 + db $c9 ; bank 2 + dw sndCompassD4Ptr +39/1a57/: | # sound id D8 + db $c9 ; bank 2 + dw sndCompassD5Ptr +39/1a5a/: | # sound id D9 + db $c9 ; bank 2 + dw sndCompassD6Ptr +39/1a5d/: | # sound id DA + db $c9 ; bank 2 + dw sndCompassD7Ptr +39/1a60/: | # sound id DB + db $c9 ; bank 2 + dw sndCompassD8Ptr + +# Edit the function playing the compass sound to: +# - use an alternate table capable of ringing compasses in any room in the game +# - play the appropriate compass sound to indicate which dungeon it belongs to +01//compassRoomsTable: /include compassRoomsTable +01/09e5/: | + call getThisRoomFlags + bit 5,(hl) + ret nz + + ld a,(wActiveGroup) + ld b,a + ld a,(wActiveRoom) + ld c,a + ld e,$01 + ld hl,compassRoomsTable + call searchDoubleKey + ret nc + + ld a,(hl) + ld b,a + + ; Check if player owns the compass for this dungeon + ld hl,wDungeonCompasses + call checkFlag + ret z + + ; If keysanity is not enabled, use vanilla compass chime + ld a,option.customCompassChimes + or a + jr z,@vanillaChime + + ; Else, try using a custom chime if relevant (= not Hero's Cave) + ld a,b + or a + jr z,@vanillaChime ; if dungeon is 0 (Hero's Cave), use vanilla chime + add a,$d3 ; Sounds d4-db are the custom compass chimes + jr @playSound + + @vanillaChime: + ld a,SND_COMPASS + @playSound: + jp playSound +# Up to 49e5-4A1B (included) + +# Address the issue with @skipWeirdCall described in the disasm +39/0bfe/: | + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop + nop \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/conditional/essence_sanity.yaml b/worlds/tloz_oos/patching/asm/conditional/essence_sanity.yaml new file mode 100644 index 000000000000..ba9a2301a0de --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/essence_sanity.yaml @@ -0,0 +1,86 @@ +# Edit interaction7f_subid00 state0 to spawn a treasure interaction instead of giving +# essence graphics to the INTERACID_ESSENCE interaction. +09/0a13/: | + ld hl,@essenceLocationsTable + rst 18 ; rst_addDoubleIndex + ld b,(hl) + inc hl + ld c,(hl) + + call getFreeInteractionSlot + ret nz + ld (hl),INTERACID_TREASURE + inc l + ld (hl),b + inc l + ld (hl),c + ld l,$71 + ld (hl),COLLECT_PICKUP + call objectCopyPosition + ret + + @essenceLocationsTable: + dwbe locations.essenceD1 + dwbe locations.essenceD2 + dwbe locations.essenceD3 + dwbe locations.essenceD4 + dwbe locations.essenceD5 + dwbe locations.essenceD6 + dwbe locations.essenceD7 + dwbe locations.essenceD8 + +# Replace "ld e,Interaction.zh ; ld (de),a" by a custom function applying the same kind of effect, +# but on the remote-controlled treasure instead +09//applyFloatingEffect: | + push hl + ld b,a + call findTreasure + ld l,$4f ; z position + ld (hl),b + ld l,$5a + ld (hl),$c0 ; Highest priority to be drawn in front of the glow effect + pop hl + ret +09/0a56/: call applyFloatingEffect + +# Lower the speed at which the item travels to the player (from SPEED_80 to SPEED_20) +09/0a8c/: db $05 +# Make the position of the treasure follow the position of the INTERACID_ESSENCE invisible object, and skip a few +# states once the treasure is collected +09/0ab6/: | + ld c,$04 + call $1f04 ; objectUpdateSpeedZ_paramC + call findTreasure + jr nc,@treasureCollected + call objectCopyPosition + ret + + @treasureCollected: + ; Make Link face down + ld a,$02 + ld ($d008),a + ; Switch to "swirl" cutscene + call interactionIncState + call interactionIncState + jp interactionIncState + +# Make the glow disappear once treasure has been collected +09//stopFlickerOnTreasureCollection: | + push hl + call findTreasure + pop hl + jr nc,@treasureCollected + ; Repeat instructions overwritten by call + ld a,$80 + xor (hl) + ret + + @treasureCollected: + ; Treasure was collected, hide the flickering glow + xor a + ld l,$40 + ret +09/0bc1/: call stopFlickerOnTreasureCollection + +# Replace the sound when getting an essence from "MUS_GET_ESSENCE" to "SND_GETITEM" +3f/2ced/: db $4c ; SND_GETITEM diff --git a/worlds/tloz_oos/patching/asm/conditional/ganon_goal.yaml b/worlds/tloz_oos/patching/asm/conditional/ganon_goal.yaml new file mode 100644 index 000000000000..63859da5c2c3 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/ganon_goal.yaml @@ -0,0 +1,9 @@ +# Re-cable state 0 of DIN_DESCENDING_CRYSTAL to middle of state27 +# to directly get warped to the Room of Rites +03/1589/: | + ; Fully heal the player + ld hl,wLinkMaxHealth + ldd a,(hl) + ld (hl),a + ; Jump to middle of state27 (warp to Room of Rites) + jp $5a1f diff --git a/worlds/tloz_oos/patching/asm/conditional/mute_music.yaml b/worlds/tloz_oos/patching/asm/conditional/mute_music.yaml new file mode 100644 index 000000000000..31142555115a --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/mute_music.yaml @@ -0,0 +1,20 @@ +# Remove "ld (wMusicVolume),a" from initSound by a call to a custom function +39/001c/: call muteMusic + +# Remove "ld (wc023),a" from initSound +39/002a/: | + nop + nop + nop + +# Edit updateMusicVolume to replace the whole music volume management part by a fixed set +39/0065/: | + pop hl + pop de + pop bc +39/0068/muteMusic: | # Fallthrough for label + xor a + ld ($c022),a + ld a,$01 + ld ($c023),a + ret \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/conditional/old_men_as_locations.yaml b/worlds/tloz_oos/patching/asm/conditional/old_men_as_locations.yaml new file mode 100644 index 000000000000..a40fa97d1a65 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/old_men_as_locations.yaml @@ -0,0 +1,32 @@ +# In "oldManScript_giveRupees", replace "wait 8; checkrupeedisplayupdated" by +# two useless "disableinput" instructions +0b/347e/: db $bd,$bd + +# Replace most of "oldMan_takeRupees" by a table of location contents for +# each Old Man (in the same order as vanilla table) +15/2212/oldManLocationTable: | + dwbe locations.oldManGoronMountain + dwbe locations.oldManNearBlaino + dwbe locations.oldManNearD1 + dwbe locations.oldManWesternCoast + dwbe locations.oldManHoronVillage + dwbe locations.oldManTarmRuins + dwbe locations.oldManWoodsOfWinter + dwbe locations.oldManGhastlyStump + +# Replace "oldMan_giveRupees" by a function performing an item lookup inside +# "oldManLocationTable" declared right above +15/2226/oldMan_giveRupees: | + ld e,$42 + ld a,(de) + ld hl,oldManLocationTable + add a,a + rst 10 + ldi a,(hl) + ld b,a + ld c,(hl) + jp spawnTreasureOnLink + +# Replace "oldManScript_takeRupees" by a single jump to "oldManScript_giveRupees", +# so that the function being called doesn't matter anymore +0b/3488/: db $74,$72 diff --git a/worlds/tloz_oos/patching/asm/conditional/quick_flute.yaml b/worlds/tloz_oos/patching/asm/conditional/quick_flute.yaml new file mode 100644 index 000000000000..81203a173136 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/quick_flute.yaml @@ -0,0 +1,13 @@ +# 2 frames before the end, reactivate enemies so that Pols Voice have time to +# witness the flute playing and die accordingly. This is a hacky fix, but it works. +06/0ccf/: | + cp $02 + jr z,@frameReactivateEnemies + or a + jr z,$2d ; @donePlayingSong + jr $10 ; ++ + + @frameReactivateEnemies: + xor a + ld (wDisabledObjects),a + ret diff --git a/worlds/tloz_oos/patching/asm/conditional/remove_d0_alt_entrance.yaml b/worlds/tloz_oos/patching/asm/conditional/remove_d0_alt_entrance.yaml new file mode 100644 index 000000000000..dfee4d247ccf --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/remove_d0_alt_entrance.yaml @@ -0,0 +1,11 @@ +# Remove the bush above the cliff +22/02a0/: db $11,$11,$11,$11 +22/3fe8/: db $11,$11,$11,$11 +23/3ce7/: db $11,$11,$11,$11 +24/3a03/: db $11,$11,$11,$11 + +# Remove the hole leading to D0 +22/02bf/: db $af +23/0007/: db $af +23/3d06/: db $af +24/3a22/: db $af diff --git a/worlds/tloz_oos/patching/asm/conditional/remove_d2_alt_entrance.yaml b/worlds/tloz_oos/patching/asm/conditional/remove_d2_alt_entrance.yaml new file mode 100644 index 000000000000..dc6907d8fa27 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/conditional/remove_d2_alt_entrance.yaml @@ -0,0 +1,21 @@ +# Remove stairs on main entrance map +21/30e1/: db $04 +22/2d19/: db $04 +23/2b27/: db $04 +24/282d/: db $04 + +# Remove stairs on the map to the right +21/311f/: db $04 +22/2d57/: db $04 +23/2b65/: db $04 +24/286b/: db $04 + +# Make inside left stairs point to inside right stairs +04/3935/: dw $420d +# Make inside right stairs point to inside left stairs +04/3939/: dw $420c + +# Since both stairs leading outside in vanilla are now connected together, +# remove D2 alt-entrance introduction textbox. +15/2a37/: jp interactionDelete + diff --git a/worlds/tloz_oos/patching/asm/cutscenes.yaml b/worlds/tloz_oos/patching/asm/cutscenes.yaml new file mode 100644 index 000000000000..abe83976a3a5 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/cutscenes.yaml @@ -0,0 +1,139 @@ +# skip or abbreviate most potentially mandatory cutscenes. + +# skip the cutscene when throwing a bomb into the volcano. +02//skipVolcanoCutscene: | + call getThisRoomFlags + set 6,(hl) + ld de,$d244 + ld a,$02 + ld (de),a + ld hl,$6314 + call interactionSetScript + ld a,$15 + call setGlobalFlag + ; Unset a room flag which changed the layout to ensure exiting Temple Remains cave didn't lead to a softlock + ld hl,$c716 + res 0,(hl) + ret + +# enable exit from volcano room after skipping cutscene. +08/3cf5/: | + nop + nop + nop + +# set up for and call skipVolcanoCutscene. +08/3d07/: | + ld a,($d244) + cp $01 + ret nz + call interactionDelete + ld hl,skipVolcanoCutscene + jp callBank2 + +# use a non-cutscene screen transition for exiting a dungeon via essence, so that overworld music plays. +09/0b4d/: ld a,$81 + +# end maku seed cutscene as soon as link gets the seed. +0b/31ec/: | + db setglobalflag,$19 + db enableinput + db scriptend + +# end northen peak cutscene as soon as the barrier is broken. +0b/39f1/: | + db setglobalflag,$1d + db enableinput + db scriptend + +# skip linked cutscene when entering d8. cutscene warp will ignore dungeon +# shuffle if not removed. +0b/3a2a/: | + db writememory + dw wDisableWarpTiles + db $00 +0b/3a32/: | + db setglobalflag,$1e + db scriptend + +# skip the great furnace dance. for some reason jumpalways doesn't work here. +14/0b15/: | + db jumpifc6xxset,$92,$ff + dw $c33f + +# set flags that are normally set during the pirate cutscene when skipping +# it. the season value should be set to the western coast default at +# randomization. +15//setPirateCutsceneFlags: | + call setGlobalFlag + ; ??? + ld a,$17 + call setGlobalFlag + ; ??? + ld a,$1b + call setGlobalFlag + ; Remove ship from desert + ld hl,$c7e2 + set 6,(hl) + ; Set default season for Western Coast since the warp doesn't ensure it + ld a,defaultSeason.WESTERN_COAST + ld (wRoomStateModifier),a + ; Take Pirate's Bell from the inventory + ld a,TREASURE_PIRATES_BELL + call loseTreasure + ret +15/1a0e/: call setPirateCutsceneFlags + +# change destination of initial transition in pirate cutscene. +15/1a1c/: db $80,$e2,$00,$66 ; wWarpDestVariables, in order? + +# Remove the pirate captain from western coast map because it triggers an odd cutscene when coming +# from the left in cases where D7 is replaced by D0 or D2 and alt entrances are enabled +11/2b5a/: db $b1,$10,$18,$18,$ff + +# Remove floodgate's keeper behavior where he notices the key when it's owned while inside his +# house (which could trigger right as you hit the lever in a randomized context) +09/2289/: ret + +# skip "you got all four seasons" text from season spirts. +15/17c2/: cp $05 + +# remove D8 falling crystal cutscenes (INTERACID_D8_GRABBABLE_ICE @state3) +09/1894/: | + ; Set a flag for this room + call getThisRoomFlags + set 6,(hl) + + ; Store current active room + ld a,(wActiveRoom) + push af + + ; Change dungeon floor + ld hl,wDungeonFloor + ld a,(hl) + dec a + ld (hl),a + push hl + + ; Set active room to the one of the new dungeon floor + call $2d65 ; getActiveRoomFromDungeonMapPosition + ld (wActiveRoom),a + + ; Set a flag for this room + call getThisRoomFlags + set 6,(hl) + + ; Reset dungeon floor + pop hl + ld a,(hl) + inc a + ld (hl),a + + ; Reset active room + pop af + ld (wActiveRoom),a + + ld a,SND_SOLVEPUZZLE_2 + call playSound + + jp interactionDelete diff --git a/worlds/tloz_oos/patching/asm/file_select_custom_string.yaml b/worlds/tloz_oos/patching/asm/file_select_custom_string.yaml new file mode 100644 index 000000000000..43f3941cc2e9 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/file_select_custom_string.yaml @@ -0,0 +1,76 @@ +# uncompressed 2bpp format: capital letters, then four punctuation characters. +# the characters are one tile each and roughly match the single-tile digits. +# these need to be loaded in two steps due to DMA transfer limitations? +14//dma_CustomFontLetters: | + db 00,ff,3c,ff,66,ff,66,ff,7e,ff,66,ff,66,ff,00,ff + db 00,ff,7c,ff,66,ff,7c,ff,66,ff,66,ff,7c,ff,00,ff + db 00,ff,3c,ff,66,ff,60,ff,60,ff,66,ff,3c,ff,00,ff + db 00,ff,7c,ff,66,ff,66,ff,66,ff,66,ff,7c,ff,00,ff + db 00,ff,7e,ff,60,ff,7c,ff,60,ff,60,ff,7e,ff,00,ff + db 00,ff,7e,ff,60,ff,7c,ff,60,ff,60,ff,60,ff,00,ff + db 00,ff,3c,ff,66,ff,60,ff,6e,ff,66,ff,3c,ff,00,ff + db 00,ff,66,ff,66,ff,7e,ff,66,ff,66,ff,66,ff,00,ff + db 00,ff,3c,ff,18,ff,18,ff,18,ff,18,ff,3c,ff,00,ff + db 00,ff,06,ff,06,ff,66,ff,66,ff,66,ff,3c,ff,00,ff + db 00,ff,66,ff,6c,ff,78,ff,6c,ff,66,ff,66,ff,00,ff + db 00,ff,60,ff,60,ff,60,ff,60,ff,60,ff,7e,ff,00,ff + db 00,ff,7c,ff,7e,ff,6a,ff,6a,ff,6a,ff,6a,ff,00,ff + db 00,ff,62,ff,72,ff,7a,ff,5e,ff,4e,ff,46,ff,00,ff + db 00,ff,3c,ff,66,ff,66,ff,66,ff,66,ff,3c,ff,00,ff + db 00,ff,7c,ff,66,ff,66,ff,7c,ff,60,ff,60,ff,00,ff + db 00,ff,3c,ff,66,ff,66,ff,66,ff,3c,ff,0e,ff,00,ff + db 00,ff,7c,ff,66,ff,66,ff,7c,ff,66,ff,66,ff,00,ff + db 00,ff,3c,ff,66,ff,38,ff,1c,ff,66,ff,3c,ff,00,ff + db 00,ff,7e,ff,18,ff,18,ff,18,ff,18,ff,18,ff,00,ff + db 00,ff,66,ff,66,ff,66,ff,66,ff,66,ff,3c,ff,00,ff + db 00,ff,66,ff,66,ff,66,ff,6c,ff,78,ff,70,ff,00,ff + db 00,ff,6a,ff,6a,ff,6a,ff,6a,ff,7e,ff,3c,ff,00,ff + db 00,ff,66,ff,7e,ff,18,ff,3c,ff,66,ff,66,ff,00,ff + db 00,ff,66,ff,66,ff,3c,ff,18,ff,18,ff,18,ff,00,ff + db 00,ff,7e,ff,06,ff,1c,ff,38,ff,60,ff,7e,ff,00,ff +14//dma_CustomFontPunct: | + db 00,ff,00,ff,00,ff,00,ff,00,ff,00,ff,00,ff,00,ff + db 00,ff,10,ff,10,ff,7c,ff,10,ff,10,ff,00,ff,00,ff + db 00,ff,00,ff,00,ff,7c,ff,00,ff,00,ff,00,ff,00,ff + db 00,ff,00,ff,00,ff,00,ff,00,ff,18,ff,18,ff,00,ff +14//dma_FileSelectStringAttrs: | + db 0a,0a,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,2a,2a + db 0a,0a,0a,0a,0a,0a,0a,0a,0a,0a,0a,0a ; offscreen + db 0a,0a,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,0e,2a,2a + db 0a,0a,0a,0a,0a,0a,0a,0a,0a,0a,0a,0a ; offscreen +14//dma_FileSelectStringTiles: /include dma_FileSelectStringTiles + +# queues a DMA transfer for the custom string to be displayed at the top of +# the file select string. +02//displayFileSelectString: | + call loadUncompressedGfxHeader + + ; Load base font + ld b,$19 ; 26*16 bytes + ld c,$14 ; Bank 14 + ld de,$8e21 + ld hl,dma_CustomFontLetters + call queueDmaTransfer + + ; Load punctuation + ld b,$03 ; 4*16 bytes + ld c,$14 ; Bank 14 + ld de,$8fc1 + ld hl,dma_CustomFontPunct + call queueDmaTransfer + + ; Load string attributes + ld b,$03 ; 4*16 bytes + ld c,$14 ; Bank 14 + ld de,$9c21 + ld hl,dma_FileSelectStringAttrs + call queueDmaTransfer + + ; Load string tiles + ld b,$03 ; 4*16 bytes + ld c,$14 ; Bank 14 + ld de,$9c20 + ld hl,dma_FileSelectStringTiles + jp queueDmaTransfer +02/0206/: call displayFileSelectString +02/0a54/: jp displayFileSelectString \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/gasha_loot.yaml b/worlds/tloz_oos/patching/asm/gasha_loot.yaml new file mode 100644 index 000000000000..688e520d5d5a --- /dev/null +++ b/worlds/tloz_oos/patching/asm/gasha_loot.yaml @@ -0,0 +1,52 @@ +0a//deterministicGashaLootTable: /include deterministicGashaLootTable + +# Inject an override at the beginning of INTERACID_GASHA_SPOT @state5 to use +# deterministic Gasha Nut loot instead of regular logic if the matching option +# is enabled. +# Current deterministic loot index is stored inside the 6 upper bits of wGashaSpotFlags, +# which means we need to perform some bit shifts before being able to read/write it. +0a//checkDeterministicGashaLoot: | + ; Read the top 6 bits of wGashaSpotFlags to get the currently harvested count + ld a,(hl) + rra + rra + ld b,a ; Store deterministic treasure index in b + and $cf + ; Test if there are still deterministic loot to give + cp option.deterministicGashaLootCount + jr c,@deterministic + jp $47c5 ; No more, go back to vanila loot logic + + @deterministic: + ; Spawn the appropriate treasure + ld a,b + and $cf + add a,a + ld hl,deterministicGashaLootTable + rst 10 ; addAToHL + ldi a,(hl) + push bc + ld b,a + ld c,(hl) + call spawnTreasureOnLink + pop bc + jr nz,@done + ld l,$71 ; Set forced collect mode in var31 + ld (hl),COLLECT_PICKUP ; Make the item set the room flag when collected + + ; Increment count, then rotate back to its initial state and store it + ld a,b + inc a + rla + rla + ld (wGashaSpotFlags),a + ; Increase nut state to make the tree wither + call interactionIncState + + @done: + ret + +# Replace the first check of INTERACID_GASHA_SPOT @state5 to check for +# deterministic Gasha Nut loot instead of regular logic if the matching option +# is enabled +0a/07bb/: jp checkDeterministicGashaLoot \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/get_item_behavior.yaml b/worlds/tloz_oos/patching/asm/get_item_behavior.yaml new file mode 100644 index 000000000000..e5a2915eb449 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/get_item_behavior.yaml @@ -0,0 +1,62 @@ +# Extend grab mode D which checks if quantity has bit 7 on. +# If it does, also increase next byte (max quantity for bombs, +# which is the only item which uses mode D) +3f//extendedModeD: | + ; If max bombs == 0, increase max bombs even if it is a wild bomb drop (like the one in D2) + ld h,d + ld l,e + inc l + ld a,(hl) + or a + jr nz,@normalCase + ld a,$10 + ldd (hl),a + jr @done + + ; Otherwise, only increase max bombs if quantity has bit 7 set (only for "treasure" bombs, not drops) + @normalCase: + bit 7,c + jr z,@done + + @increaseMaxQuantity: + res 7,c + add a,c + daa + jr nc,@writeNewMaxValue + ld a,$99 + @writeNewMaxValue: + ldd (hl),a + + @done: + jp $4594 ; mode4 +3f/05c2/: call extendedModeD + +# Put a fixed limit on @modea to prevent max health to go past the point where +# HUD starts looking buggy. Heart Containers being the only ones to use that mode, +# we can do a pretty hacky fix without worrying too much +3f//incrementAndCapMaxHealth: | + ld a,(de) + add a,c + cp $40 + jr c,@belowMax + ld a,$40 + @belowMax: + ld (de),a + ret +3f/05aa/: jp incrementAndCapMaxHealth + +# Put a limit on Satchel level to prevent HUD from going bonkers by having +# a wrong max seed count. This edits @mode2 and checks satchel level for +# all few items sharing this mode (for simplicity's sake). +3f//incrementAndCapSatchelLevel: | + ld a,(de) + inc a + ld (de),a + + ld a,(wSeedSatchelLevel) + cp $03 + jr c,@belowMax + ld a,$03 + @belowMax: + ret +3f/058e/: jp incrementAndCapSatchelLevel diff --git a/worlds/tloz_oos/patching/asm/gfx.yaml b/worlds/tloz_oos/patching/asm/gfx.yaml new file mode 100644 index 000000000000..9b0aff2703b6 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/gfx.yaml @@ -0,0 +1,133 @@ +# Most of the code here has to do with interactions other than +# the id $60 "treasure" interaction that must use treasure interaction +# graphics. + +# set hl = the address of the sprite for treasure with ID b and subID c. +3f//lookupItemSprite_body: | + ld e,$15 + ld hl,getTreasureDataSprite + call interBankCall + ld a,e + ld hl,$66dc ; interaction60SubidData + add a,a + rst 10 + ld a,e + rst 10 + ret + +# format (ID, subID, jump address). these functions *must* pop af as the last +# instruction before returning. +3f//customSpriteJumpTable: | + dwbe 4700, locations.memberShop1 + dwbe 4702, locations.memberShop2 + dwbe 4705, locations.memberShop3 + dwbe 4704, locations.horonShop1 + dwbe 4703, locations.horonShop2 + dwbe 4701, locations.horonShop3 + dwbe 470e, locations.advanceShop1 + dwbe 470f, locations.advanceShop2 + dwbe 4710, locations.advanceShop3 + dwbe 4709, locations.syrupShop2 + dwbe 470a, locations.syrupShop3 + dwbe 470b, locations.syrupShop1 + dwbe 5900, locations.lostWoodsPedestal + dwbe 8100, locations.subrosianMarket1 + dwbe 8104, locations.subrosianMarket2 + dwbe 810a, locations.subrosianMarket3 + dwbe 810c, locations.subrosianMarket4 + dwbe 810d, locations.subrosianMarket5 + dwbe c600, locations.d0SwordChest + dwbe e602, locations.templeOfSeasons + db ff + +# overrides the sprite data loaded for certain interactions. this is mostly +# used for "non-item" interactions that depict items, like the ones in shops. +3f//checkLoadCustomSprite: | + call $4437 ; _interactionGetData + push af + push hl + push bc + + ld e,$41 + ld a,(de) + ld b,a + inc e + ld a,(de) + ld c,a + ld e,$02 + ld hl,customSpriteJumpTable + call searchDoubleKey + jr c,@customSprite + pop bc + pop hl + pop af + ret + + @customSprite: + pop bc + ldi a,(hl) + ld b,a + ld c,(hl) + pop hl + call lookupItemSprite_body + pop af + ret +3f/0356/: call checkLoadCustomSprite + + +# Make the rod of seasons interaction behave like a regular item, graphically. +# Otherwise, asymmetric wide items can't go there. +14//overrideAnimationId: | + ld e,$41 + ld a,(de) # ID + cp $e6 + ret nz + ld e,$42 + ld a,(de) # sub ID + cp $02 + ld a,$e6 + ret nz + ld a,$60 + ret +00/25d9/: call overrideAnimationId +00/2600/: call overrideAnimationId + +# give the noble sword (lost woods pedestal) object OAM pointers compatible +# with normal treasure graphics. +14/13d7/: dw 5719 +14/15a7/: dw 684f + + +# give items that don't normally appear as treasure interactions entries in +# the treasure graphics table. +3f/2790/: db 5f,16,33 # Ricky's flute (GFX #3c) +3f/2793/: db 5f,16,23 # Dimitri's flute (GFX #3d) +3f/2796/: db 5f,16,13 # Moosh's flute (GFX #3e) +3f/2799/: db 5d,10,26 # Rare Peach Stone (GFX #3f) + +3f/27b4/: db 5d,0c,13 # Member's Card (GFX #48) +3f/27b7/: db 65,14,33 # Treasure Map (GFX #49) +3f/27ba/: db 60,14,00 # Fool's Ore (GFX #4a) +3f/27bd/: db 5d,00,23 # Potion (GFX #4b) +3f/27c0/: db 65,0c,23 # Ribbon (GFX #4c) +3f/27c3/: db 5c,0c,20 # Ore Chunks x10 (GFX #4d) +3f/27c6/: db 5c,0c,10 # Ore Chunks x25 (GFX #4e) + +# Add custom palette variants for rod of seasons to be able to know which +# season it gives at first sight +3f/27c9/: | + db $60,$10,$00 ; GFX #4f (rod of spring) + db $60,$10,$30 ; GFX #50 (rod of autumn) + db $60,$10,$10 ; GFX #51 (rod of winter) + +# Use that custom sprite with a red palette inside GFX#52 (will be used for progression remote items) +3f/27d2/: db $61,$06,$53 +# Use that custom sprite with a blue palette inside GFX#53 (will be used for regular remote items) +3f/27d5/: db $61,$06,$43 + +# Inject the "Archipelago Item" sprite in an unused sector of last items spritesheet +1b/3a80/: | + db $03,$03,$07,$04,$0F,$08,$3F,$3C,$7E,$43,$F9,$87,$FD,$BF,$FA,$C6 + db $FB,$87,$F7,$8C,$EF,$98,$8F,$F8,$4F,$78,$38,$3F,$04,$07,$03,$03 + db $C0,$C0,$E0,$20,$90,$70,$3C,$FC,$7E,$C2,$F9,$87,$FD,$BF,$7F,$43 + db $F9,$C7,$F1,$2F,$D1,$3F,$91,$7F,$12,$FE,$1C,$FC,$20,$E0,$C0,$C0 diff --git a/worlds/tloz_oos/patching/asm/impa_refill.yaml b/worlds/tloz_oos/patching/asm/impa_refill.yaml new file mode 100644 index 000000000000..f4bb07739778 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/impa_refill.yaml @@ -0,0 +1,85 @@ +15//impaRefill: | + push de + push hl + + ld b,TREASURE_EMBER_SEEDS + ld hl,wNumEmberSeeds + + @refillSeedsLoop: + ld a,b + call checkTreasureObtained + jr nc,@nextSeed + ld a,(hl) ; currently owned seeds + cp $20 + jr nc,@nextSeed + ld (hl),$20 + + @nextSeed: + ld a,b + cp TREASURE_MYSTERY_SEEDS + jr z,@refillBombs + inc hl + inc b + jr @refillSeedsLoop + + @refillBombs: + ld a,TREASURE_BOMBS + call checkTreasureObtained + jr nc,@refillShield + ld hl,wMaxBombs + ldd a,(hl) + ld (hl),a + + @refillShield: + ld a,TREASURE_SHIELD + call checkTreasureObtained + jr nc,@refillHealth + ld a,TREASURE_SHIELD + ldh ($8b),a ; put item ID in FF8B + ld e,$3f + ld hl,$46bc ; addTreasureToInventory in bank 3F + call interBankCall + + @refillHealth: + ld hl,wLinkMaxHealth + ldd a,(hl) + ld (hl),a + + ; Play a sound and update status bar to give feedback + ld a,SND_GETSEED + call playSound + ld a,$03 + ld (wStatusBarNeedsRefresh),a + + pop hl + pop de + ret + +0b/34ef/: | + db checkabutton + db disableinput + db showtext,$25,$03 + + db asm15 + dw impaRefill + + db enableinput + db $74,$ef ; jump back to the beginning of the script + +# Change Impa text to explain that she is providing a refill +1f/0247/: | + db $04,$b6,$04,$06,$05,$a0,$05,$07,$03,$d2,$03,$bd,$61,$01,$72,$65,$66,$69,$6c,$6c,$2e,$00 + +# Make Like-Like only remove the shield without unsetting the treasure flag +0c//loseShieldWithoutLosingFlag: | + call loseTreasure + ld hl,wObtainedTreasureFlags + set 1,(hl) + ret +0c/1dd2/: call loseShieldWithoutLosingFlag + +# Make Impa always stand in front of her house +0a/1d57/: | + db $00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00 \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/item_events.yaml b/worlds/tloz_oos/patching/asm/item_events.yaml new file mode 100644 index 000000000000..f55e9d5b3b67 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/item_events.yaml @@ -0,0 +1,71 @@ +# basically anything that has to do with determining whether an item should be +# created/given, how to create/give the item, and what happens when the item is +# obtained. + +# have seed satchel inherently refill all seeds. +3f//testSatchelRefillOnItemGet: | + push bc + call $44c8 ; giveTreasure_body + ld a,b + pop bc + push af + ld a,b + cp TREASURE_SEED_SATCHEL + jr nz,@notSatchel + push bc + push de + call refillSeedSatchel + pop de + pop bc + @notSatchel: + pop af + ld b,a + ret +00/16f6/: call testSatchelRefillOnItemGet + +# setting a flute's icon and song when obtained. also makes the corresponding +# animal companion rideable, etc. +3f//activateFlute: | + push af + push de + push hl + ld a,b + cp TREASURE_FLUTE + jr nz,@done + ld e,$af + ld a,c + sub $0a ; get animal index item parameter + ld (de),a + add a,$42 + ld h,$c6 + ld l,a ; hl = flags for relevant animal + cp $45 + jr nz,@moosh + set 5,(hl) + jr @done + @moosh: + set 7,(hl) + @done: + pop hl + pop de + pop af + call $454e ; applyParameter + ret +3f/052b/: call activateFlute + +# Set room flag 0x20 when knocking off tree seeds (mostly for tracking purpose) +10//extendedKnockOffTree: | + push hl + call getThisRoomFlags + or $20 + ld (hl),a + pop hl + ld bc,$fec0 ; original instruction replaced by injected call + ret +10/0b29/: call extendedKnockOffTree + +# remove interactions from jewel caves so that they act as normal chests, +# meaning no linked changes. this also removes the vire event. +11/2ca2/: db $ff # swamp +11/2ca8/: db $ff # lake +11/2d0f/: db $ff # woods diff --git a/worlds/tloz_oos/patching/asm/layouts.yaml b/worlds/tloz_oos/patching/asm/layouts.yaml new file mode 100644 index 000000000000..98e94c099583 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/layouts.yaml @@ -0,0 +1,152 @@ +# Data for applyAllTileSubstitutions: +# - room group +# - room ID +# - room flag mask required for the layout change to occur (00 means "always") +# - tile position as "yx" +# - tile to put in place of the original one +04//tileReplacementsTable: | + db $00,$01,$01,$52,$04 ; Permanently remove flower outside D6 when cut + db $00,$9a,$00,$14,$12 ; Remove rock across pit blocking exit from D5 + db $00,$8a,$00,$66,$64 ; Add rock at bottom of cliff to block ricky + db $00,$9a,$00,$34,$04 ; Remove bush next to rosa portal + + ; Add ledge down from temple remains lower portal + db $00,$25,$00,$32,$3a + db $00,$25,$00,$33,$cf + db $00,$25,$00,$34,$4b + + ; Replace summer vines near d2 by stairs + db $00,$8e,$00,$35,$d0 + db $00,$8e,$00,$45,$d0 + db $00,$8e,$00,$34,$36 + db $00,$8e,$00,$44,$51 + db $00,$8e,$00,$36,$35 + db $00,$8e,$00,$46,$50 + + ; Add walkable tile outside temple remains door and replace the rest of the tree with rocks + db $00,$16,$01,$18,$0f + db $00,$16,$01,$17,$64 + db $00,$16,$01,$27,$64 + + /include additionalTileReplacements + + db $ff + +# look up tiles in custom replacement table after loading a room. the format +# is (group, room, bitmask, YX, tile ID), with ff ending the table. if the +# bitmask AND the current room flags is nonzero, the replacement is not made. +04//applyExtraTileSubstitutions: | + push bc + push de + call getThisRoomFlags + ld e,a + ld hl,tileReplacementsTable + ld a,(wActiveGroup) + ld b,a + ld a,(wActiveRoom) + ld c,a + @loop: + ldi a,(hl) + cp $ff + jr z,@done + cp b + jr nz,@groupMismatch + ldi a,(hl) + cp c + jr nz,@roomMismatch + ldi a,(hl) + or a + jr z,@alwaysOn + and e + jr z,@flagMismatch + @alwaysOn: + push de + ld d,$cf + ldi a,(hl) + ld e,a + ldi a,(hl) + ld (de),a + pop de + jr @loop + @groupMismatch: + inc hl + @roomMismatch: + inc hl + @flagMismatch: + inc hl + inc hl + jr @loop + @done: + pop de + pop bc + call $5d94 ; applyAllTileSubstitutions + ret +00/3854/: call applyExtraTileSubstitutions + +# bank 21 = spring, 22 = summer, 23 = autumn, 24 = winter +# Remove most snow in Woods of Winter middle room +24/2c68/: db $04 +24/2c70/: db $04 +24/2c7e/: db $04 +24/2c71/: db $9e,$8b +24/2c74/: db $c0,$04,$80,$81,$99,$9b,$70,$71 + +# Change temple remains door upper-left tree tile per-layout because the generic tile replacement +# was causing a glitched palette in winter +21/134e/: db $70 # Spring +22/102a/: db $70 # Summer +23/0e08/: db $70 # Autumn +24/0b22/: db $65 # Winter + +# set a room flag when the flower outside D6 is broken to make the pillar go away forever +06//checkBreakD6Flower: | + push af + push bc + ld bc,$0100 + call compareRoom + pop bc + jr nz,@done + ldh a,($93) ; hBrokenTilePosition + cp $52 + jr nz,@done + push hl + ld hl,$c701 + set 0,(hl) + pop hl + @done: + pop af + jp setTile +06/0774/: call checkBreakD6Flower + +# change water tiles outside d4 from deep to shallow, to prevent softlock +# from entering without flippers or default summer. +21/14a9/: db $fa,$6b,$6b,$53,$fa,$3f,$fa +22/1197/: db $fa,$6b,$6b,$53,$fa,$3f,$fa +23/0f6c/: db $fa,$6b,$6b,$53,$fa,$3f,$fa +24/0cec/: db $dc,$00,$fc,$06,$dc,$dc,$dc,$dc + +# replace some currents in spool swamp in spring so that the player isn't +# trapped by them. +21/3ab1/: db $d3,$d3 +21/3ab6/: db $d4,$d4,$d4 +21/3abe/: db $d1 + +# replace the stairs outside the portal in eyeglass lake summer with a +# railing, since if the player jumps off they fall into lost woods. +22/391b/: db $40,$40,$40 +# instead add a ledge to the left side of the platform, so that entering the +# portal without feather and resetting the season to summer isn't a softlock. +22/38fd/: db $37 +22/3905/: db $25 +22/3910/: db $47 + +# remove snow pile outside d6 to prevent softlock in default winter if statue +# is pushed to the left. +24/05d2/: db $04 + +# remove the snow piles in front of holly's house so that shovel isn't +# required to leave. +24/246f/: db $04 +# remove some snow outside d7 for the same reason - but off the right ledge, +# not in front of the stairs. +24/3910/: db $2b,$a0,$b9,$2b diff --git a/worlds/tloz_oos/patching/asm/locations.yaml b/worlds/tloz_oos/patching/asm/locations.yaml new file mode 100644 index 000000000000..041c72fdd2c6 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/locations.yaml @@ -0,0 +1,521 @@ +0b/2a6c/: dwbe locations.hollyGift +0b/24cc/: dwbe locations.blainoPrize +09/260f/: dw locations.underwaterItemBelowNatzuBridge +09/2281/: dw locations.floodgateKeeperHouse +09/26c6/: dw locations.springBananaTree +09/22a3/: dw locations.goronMountainPits +0b/3332/: dwbe locations.oldManInTreehouse +0b/2418/: dwbe locations.lostWoodsPedestal +08/0ce4/: dwbe locations.syrupShop1 +08/0ce0/: dwbe locations.syrupShop2 +08/0ce2/: dwbe locations.syrupShop3 +08/0cd6/: dwbe locations.horonShop1 +08/0cd4/: dwbe locations.horonShop2 +08/0cd0/: dwbe locations.horonShop3 +08/0cce/: dwbe locations.memberShop1 +08/0cd2/: dwbe locations.memberShop2 +08/0cd8/: dwbe locations.memberShop3 +08/0cea/: dwbe locations.advanceShop1 +08/0cec/: dwbe locations.advanceShop2 +08/0cee/: dwbe locations.advanceShop3 +0b/0fc5/: dwbe locations.towerOfWinter +0b/0fb9/: dwbe locations.towerOfSummer +0b/0fb5/: dwbe locations.towerOfSpring +0b/0fc1/: dwbe locations.towerOfAutumn +0b/2646/: dwbe locations.subrosianDanceHall +09/37da/: dwbe locations.subrosianMarket1 +09/37e2/: dwbe locations.subrosianMarket2 +09/37ee/: dwbe locations.subrosianMarket3 +09/37f2/: dwbe locations.subrosianMarket4 +09/37f4/: dwbe locations.subrosianMarket5 +09/26b1/: dw locations.d1Basement +0b/0c0b/: dwbe locations.d4DiveSpot +09/1051/: dwbe locations.d5ArmosChest +0b/0c22/: dwbe locations.d5Basement +09/1689/: dw locations.d7ArmosPuzzle +0b/0fd5/: dwbe locations.mayorGift +0b/1c1b/: dwbe locations.goronGift +0b/1094/: dwbe locations.drLeftReward +0b/10e3/: dwbe locations.malonTrade +0b/1067/: dwbe locations.mrsRuulTrade +14/0bda/: dwbe locations.subrosianChefTrade +0b/21d3/: dwbe locations.biggoronTrade +0b/2353/: dwbe locations.ingoTrade +0b/1263/: dwbe locations.yellingOldManTrade +0b/1f41/: dwbe locations.syrupTrade +0b/121a/: dwbe locations.tickTockTrade +0b/23e5/: dwbe locations.guruguruTrade + +# Seed trees (ID only as they can only receive seed items) +0d/28fb/: db locations.horonVillageSeedTree.id - $20 +0d/28fe/: db locations.woodsOfWinterSeedTree.id - $20 +0d/2901/: db locations.northHoronSeedTree.id - $20 +0d/2904/: db locations.spoolSwampSeedTree.id - $20 +0d/2907/: db locations.sunkenCitySeedTree.id - $20 +0d/290a/: db locations.tarmRuinsSeedTree.id - $20 + +# D0 end chest +0a/3b90/: db locations.d0SwordChest.id +0a/3b92/: db locations.d0SwordChest.subid + +# Temple of Seasons +15/30ce/: db locations.templeOfSeasons.id +15/30cc/: db locations.templeOfSeasons.subid + +# Samasa Desert Pit +09/248c/: dw locations.samasaDesertPit +0b/20b1/: db locations.samasaDesertPit.id + +# Maku Tree Gift +15/213a/: db locations.makuTree.id +15/213d/: db locations.makuTree.subid +09/3e16/: db locations.makuTree.id +09/3e19/: db locations.makuTree.subid + +# Diving spot outside D4 +0b/334e/: dwbe locations.divingSpotOutsideD4 +0b/3358/: dwbe locations.divingSpotOutsideD4 + +##### SUBROSIA SEASIDE ######################################## + +# check room flags to determine whether to create star ore instead of +# whatever global flag 0e is. this also fixes a vanilla bug causing star ore +# to be infinitely rediggable (but only when the first screen is rolled? or +# that getting it on the first screen doesn't count? something). +08//starOreRooms: db $65,$66,$75,$76,$ff +08//checkBeachItemObtained: | + ld de,starOreRooms + ld h,$c8 + call scanItemGetFlagsForMaps + ret +08/22a7/: | # Replace "checkGlobalFlag" on GLOBALFLAG_STAR_ORE_FOUND + push de + call checkBeachItemObtained + pop de + ret nz +08/22fd/: | # Replace "call checkTreasureObtained" + push de + call checkBeachItemObtained + pop de + db $20 ; Replace "jr c" by "jr nz" + +# Vanilla Star Ore location doesn't set a subid, so we need to extend this to allow for items with a subid +08//setStarOreIds: | + inc l + ld (hl),locations.subrosiaSeaside.id + inc l + ld (hl),locations.subrosiaSeaside.subid + ret +08/22f2/: call setStarOreIds + + +##### BOMB FLOWER ############################################ + +09/0397/: call objectSetInvisible +09/03be/: | + ld bc,locations.bombFlower + jp spawnTreasureOnLink + +# Remove the problematic check where Bomb Flower interaction is removed if you own +# Bomb Flower (even in vanilla, this seems useless since the "GOT_ITEM" room flag +# is checked first...) +09/035c/: | + nop + nop + nop + nop + nop + +##### GOLDEN BEASTS OLD MAN #################################### + +# Make the golden beasts old man give a treasure instead of a ring +15/2ad9/: | + ld bc,locations.goldenBeastsOldMan + jp spawnTreasureOnLink + +# Change the golden beasts old man requirement (both checked value and text) +15/2acf/goldenBeastsRequirement: db option.goldenBeastsRequirement +# Change the "jr z" into a "jr nc" to allow having more beasts killed than needed +15/2ad2/: db $30 +# Change not yet completed text value +1e/3247/goldenBeastsText: db $30+option.goldenBeastsRequirement,$20,$05,$8e,$20 +# Change completion text value +1e/3295/goldenBeastsRewardText: db $30+option.goldenBeastsRequirement,$20,$05,$8e,$20 + + +##### TREEHOUSE OLD MAN ############################################ + +# Change the essence requirement for treehouse old man +0a/0f8b/: db option.treehouseOldManRequirement +# Change the number of essences mentioned in text +1f/337b/treehouseOldManText: | + db $68,$61,$73,$20 + db $30 + option.treehouseOldManRequirement + db $20,$02,$35,$2e,$00 + + +##### GREAT FURNACE ############################################### + +# Great Furnace location usually doesn't need a subID since it gives a Hard Ore with subid 0. +# Change that to allow randomized items with nonzero subID. +15//setHardOreIds: | + inc l + ld (hl),locations.greatFurnace.id + inc l + ld (hl),locations.greatFurnace.subid + ret +15/1b83/: call setHardOreIds + +# 09/26ea/: dwbe locations.greatFurnace + +##### HARD ORE SMITHY ############################################### + +# Skip "has shield" check for forging hard ore +0b/35c6/: db jumpifitemobtained,TREASURE_PUNCH + +# Rework of the "subrosianSmith_giveUpgradedShield" function to handle randomized +# item and set a proper flag for client tracking +15/22a7/: | + call getThisRoomFlags + set 6,(hl) ; +0x40 on room flag + ld bc,locations.subrosianSmithyOre + jp spawnTreasureOnLink + + +##### BELL SMITHY ############################################### + +# Remove the "post-endgame" discussion when no items are reforgeable +0b/3596/: dw $7598 +# The full "post-endgame" script space is free (from 2f5f0 to 2f641 included) +0b/35f0/reforgeRustyBell: | + db orroomflag,$80 + db loseitem,TREASURE_RUSTY_BELL + db giveitem,locations.subrosianSmithyBell.id,locations.subrosianSmithyBell.subid + dwbe $75b6 +# (2f635 to 2f641 remains) +0b/35b3/: dwbe reforgeRustyBell + + +##### TEMPLE OF SEASONS ############################################## + +# don't display normal fixed text for temple of seasons item. +15/30be/: | + nop + nop + nop +# rod cutscene +15/30cf/: call giveTreasureCustom + + +##### MASTER DIVER'S TRADE ######################################### + +# Change Master Diver's condition from "has flippers" to "has obtained item" +0b/32f0/: db jumpifroomflagset,$80 +# remove master's plaque from inventory to prevent confusion. +0b//script_diverGiveItem: | + db giveitem, locations.masterPlaqueTrade.id, locations.masterPlaqueTrade.subid + db orroomflag,$80 + db loseitem,TREASURE_MASTERS_PLAQUE + db retscript +0b/330d/: | + db callscript + dw script_diverGiveItem + + +##### MT CUCCO PLATFORM CAVE ######################################## + +# mt. cucco platform cave item is normally created using ring-specific code. +# values are replaced at randomization. +09/241a/: | + ld bc,locations.mtCuccoPlatformCave + call createTreasure + + +##### LOST WOODS PEDESTAL ########################################### + +# ignore sword level for lost woods pedestal. +08/3e62/: ld a,$01 +# remove second sword (used to trigger spin slash) from lost woods script. +0b/241a/: db retscript +0b/2421/: db retscript + +# This function is written in place of @checkTransition and does all the work +# in handling Lost Woods transitions, deciding in which room should the player +# be warped and whether related counter should be incremented. +# ---------------- +# in(C): room index to warp to if sequence is complete +# in(DE): address to the transition counter +# in(HL): address to expected transitions table +# out(A): counter value +# out(Z): set if transition is complete +01/1e01/checkSpecialLostWoodsTransitions: | + ld a,(de) + rst 18 ; rst_addDoubleIndex + + ; Test the transition direction + ld b,(hl) ; b <- expected direction + ld a,(wScreenTransitionDirection) + cp b + jr nz,@wrongTransition + + ; Test the transition season + inc hl + ld b,(hl) ; b <- expected season + ld a,(wRoomStateModifier) + cp b + jr nz,@wrongTransition + + ; Successful transition, increment counter + ld a,(de) + cp $03 + jr z,@complete + + ; On progress (not completion), increase counter and loop back to same room + inc a + ld (de),a ; Update transition counter + ld (wCustomBuffer),a ; Set some nonzero value in custom buffer to schedule the loopback + xor a ; Clear C flag to ensure we are testing the other sequence + ret + + ; On completion, warp to room in C and reset counter + @complete: + xor a + ld (de),a ; Update transition counter + ld a,c + ld (wActiveRoom),a + scf ; Set carry flag to indicate calling function we are overriding the destination + ret + + ; Reset counter on wrong transition + @wrongTransition: + xor a + ld (de),a + ret + +# This function is written in the middle of @checkSwordUpgradeTransitions letting +# some space before and after for safety +01/1e48/checkNorthTransitions: | + ; Clear the custom buffer used to store whether we need to loopback to the same map for complex cases + ; (e.g. we are making progress in the sequence but transitioning to the right) + xor a + ld (wCustomBuffer),a + + ld de,wLostWoodsTransitionCounter1 + ld hl,@northTransitions + ld c,$30 + jp checkSpecialLostWoodsTransitions + + @northTransitions: + /include lostWoodsMainSequence +01/1dd3/: call checkNorthTransitions + +# This function is written in place of @checkSwordUpgradeTransitions +01/1e31/checkPedestalTransitions: | + ld de,wLostWoodsTransitionCounter2 + ld hl,@pedestalTransitions + ld c,$c9 + call checkSpecialLostWoodsTransitions + ret c + ; Since this is the second function being called, check if something was store inside the custom buffer + ; to know if we need to force loopback before going back to vanilla code + jp enforceLoopbackOnProgress + + @pedestalTransitions: + /include lostWoodsPedestalSequence +01/1dd7/: call checkPedestalTransitions + +# This function reads the custom buffer to enforce a loopback to the same room if some progress was made on one of the +# sequences +01//enforceLoopbackOnProgress: | + ld a,(wCustomBuffer) + or a + ret z + ld a,$40 + ld (wActiveRoom),a + scf + ret + +##### HERO'S CAVE CHEST ############################################ + +# stop hero's cave chest from giving a second sword that causes a spin slash. +0a/3bb9/: | + db $00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00 + + +##### SUBROSIAN HIDE AND SEEK ############################################ + +# removes the event where the subrosians steal your feather. +11/2f8c/: db $ff + +# set fixed items for the first dig and all subsequent digs +15/1dd6/: | + ; First dig: put randomized item + ld bc,locations.subrosiaHideAndSeek + jr $25 + ; Subsequent digs: put 50 Ore Chunks + ld bc,$3700 + jr $20 + +# allow to transition away from the screen even if the item is not dug +09/259b/: | + xor a + ld ($ccab),a + jp interactionDelete + + +##### VASU'S GIFT ################################################### + +# Vasu usually gives a ring, we need him to give a treasure instead +15/09a6/: | + ld bc,locations.vasuGift + jp spawnTreasureOnLink + + +##### LONELY GORON'S GIFT ################################################### + +# Instructions to remove most of the Goron's ring box upgrade script +0b/1bff/: db $eb +0b/1c00/: db $eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb +0b/1c10/: db $eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb,$eb + + +##### SUBROSIAN SIGN-LOVING GUY ######################################## + +# Edit subrosian_checkSignsDestroyed to edit the condition +15/192c/: | + ld a,(wTotalSignsDestroyed) + ld b,a + ld c,$05 # condition met text + cp option.signGuyRequirement + jr nc,$14 + ld c,$00 # tell requirement text + jr $10 + +# Change the item given +15/195d/: | + ld bc,locations.subrosianSignLovingGuy + jp spawnTreasureOnLink + +# Edit text to show the requirement +#################### +# You have broken # +# XXX signs!\n # +# You'd better not # +# break more than # +# XXX, or else... # +#################### +20/1160/: | + db 03,e8 ; "You " + db 02,f5 ; "have " + db 03,82,01 ; "broken\n" + db 02,7e ; "XXX signs!\n" + + db 05,9d,27,64,20 ; "You'd " + db 03,5b ; "better" + db 20,05,0c ; " not\n" + + db 03,f0 ; "break" + db 03,2b ; " more" + db 20,74,68,61,6e,01 ; " than\n" + + db 09,01 ; red color + /include signGuyRequirementDigits + db 09,00 ; default color + db 2c,20,6f,72,20 ; ", or" + db 65,6c,73,65,2e,2e,2e ; "else..." + db 00 + + +##### MAPLE TRADE ###################################################### + +# Make Maple trade a randomized item which sets a custom flag, both for client tracking purpose +# and to prevent from obtaining this item several times (@state3 of INTERACID_GHASTLY_DOLL) +0a/13cc/: | + call interactionIncState + + # Test flag to skip treasure creation if item has already been obtained (from several Lon Lon Eggs in pool) + ld a,(wBoughtShopItems2) + bit 3,a + ret nz + + # Create a treasure with a forced collect mode (using multiworld collect override) + ld bc,locations.mapleTrade + call spawnTreasureOnLink + ld l,$71 + ld (hl),COLLECT_PICKUP_NOFLAG + + # Set a flag to tell it has been collected + ld a,(wBoughtShopItems2) + or $08 + ld (wBoughtShopItems2),a + ret +# Remove timer after getting Maple trade item +0a/140e/: | + nop + nop + nop + nop + + +##### TALON TRADE ###################################################### + +# Make Talon spawn a real treasure interaction instead of a fixed trade item +15//spawnTalonTreasure: | + call getFreeInteractionSlot + ret nz + ld (hl),INTERACID_TREASURE + inc l + ld (hl),locations.talonTrade.id + inc l + ld (hl),locations.talonTrade.subid + ld l,$4b ; y + ld (hl),$68 + ld l,$4d ; x + ld (hl),$48 + ; Put a forced collect mode which overrides any kind of deduction from room ID + ld l,$71 + ld (hl),COLLECT_PICKUP_NOFLAG + ret +# Edit caveTalonScript to call that specific function +0b/207d/: | + db asm15 + dw spawnTalonTreasure + db $eb,$eb ; do nothing (initcollisions for nothing) + +# Look for an interaction of type treasure in the room, and move the first +# one found on Link for him to collect it. +15//moveTreasureOnLink: | + push hl + push de + push bc + + ld hl,$d041 + @loop: + ld a,(hl) + cp INTERACID_TREASURE + jr nz,@next + + @found: + ld de,$d00b ; wLink.yh + call objectCopyPosition_rawAddress + jr @end + + @next: + ld a,h + cp $df + jr z,@end + inc h + jr @loop + + @end: + pop bc + pop de + pop hl + ret +# Edit talon_giveMushroomAfterWaking to move the treasure behind him on Link +# instead of giving an item spawned on the fly +14/09f5/: | + db asm15 + dw moveTreasureOnLink \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/maku_tree.yaml b/worlds/tloz_oos/patching/asm/maku_tree.yaml new file mode 100644 index 000000000000..41d66f82300c --- /dev/null +++ b/worlds/tloz_oos/patching/asm/maku_tree.yaml @@ -0,0 +1,79 @@ +09//makuTreeRooms: db $0b,$0c,$7b,$2b,$2c,$2d,$5b,$5c,$5d,$5e,$ff +09/3dc0/checkMakuItemObtained: | + push de + push hl + ; Force smallest Maku Tree room "visited flag" to be set, because map menu only checks for that specific room + ; variant to decide if the tile should be revealed or not. + ld hl,$c80b + set 4,(hl) + ; Scan all room flags for all Maku Tree map variants to check if the item was obtained in at least one of them + ld de,makuTreeRooms + ld h,$c8 + call scanItemGetFlagsForMaps + pop hl + pop de + ret +09/3dfc/: | + nop + nop + call checkMakuItemObtained + ret z + +# Rework makuTree_setAppropriateStage to map a small amount of stages to all +# possible situations in rando: +# - Item not yet obtained +# - Item obtained, not enough essences +# - Item obtained, enough essences collected (give Maku Seed) +# - Maku Seed obtained +# - Game completed +09/3d8b/: | + call checkMakuItemObtained + jr nz,@gnarledObtained + xor a + jr @setStage ; If Maku Tree item was not obtained, set stage 0x0 + + @gnarledObtained: + ld a,$28 ; GLOBALFLAG_FINISHEDGAME + call checkGlobalFlag + jr z,@gameNotFinished + ld a,$0e + jr @setStage ; If game is finished, set stage 0xE + + @gameNotFinished: + ld a,TREASURE_MAKU_SEED + call checkTreasureObtained + jr nc,@makuSeedNotObtained + ld a,$0c + jr @setStage ; If Maku Seed was obtained, set stage 0xC + + @makuSeedNotObtained: + call getEssenceCount + cp option.requiredEssences + jr c,@notEnoughEssences + ld a,$08 ; Enough essences are owned to get Maku Seed, set stage 0x8 + jr @setStage + + @notEnoughEssences: + ld a,$01 ; Not enough essences in inventory, set stage 0x1 + + @setStage: + ld ($cc39),a + ret + +# Change the sign text to tell how many essences are required to get the Maku seed +1f/102a/makuSignText: | + db $05,$b0,$20 + db $30 + option.requiredEssences + db $20,$02,$35,$01,$05,$79,$05,$0f,$03,$94,$05,$59,$21 + db $00 + +# In applyWArpDest_b04, change the variable that is checked to know in which Maku Tree room the player must be warped +04//determineMakuTreeRoomOffset: | + ; Room of Rites has been visited, use $09 as a fixed room offset + ld a,($c6df) + cp $09 + ; Otherwise, use essence count as room offset + jp c,getEssenceCount + ret +04/05e8/: call determineMakuTreeRoomOffset + diff --git a/worlds/tloz_oos/patching/asm/map_menu.yaml b/worlds/tloz_oos/patching/asm/map_menu.yaml new file mode 100644 index 000000000000..7c5c6ab20c40 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/map_menu.yaml @@ -0,0 +1,276 @@ +# map pop-up icons for seed trees +02/2c60/: db locations.horonVillageSeedTree.id - $0b +02/2c5d/: db locations.woodsOfWinterSeedTree.id - $0b +02/2c57/: db locations.northHoronSeedTree.id - $0b +02/2c5a/: db locations.spoolSwampSeedTree.id - $0b +02/2c54/: db locations.sunkenCitySeedTree.id - $0b +02/2c51/: db locations.tarmRuinsSeedTree.id - $0b + +# Store a few additional variables when checking the SELECT button to open map +02//extendedMenuOpenCheck: | + ld a,(wDungeonIndex) + ld (wOriginalDungeonIndex),a + ld a,(wMinimapGroup) + ld (wOriginalMinimapGroup),a + ld a,(wKeysJustPressed) + ret +# Replace the second `ld a,(wKeysJustPressed)` in b2_updateMenus by an extension +02/0fc5/: call extendedMenuOpenCheck + +# When booting the map, handle a "cycling order" that can be issued using the keybinding defined +# by above functions. Pressing that button reopens the map with another map mode, +# and the cycling between modes is addressed by this function. +02//extendedBootMapMenu: | + ; Wait for palette change to complete + ld a,(wPaletteThread_mode) + or a + ret nz + + ; Change the current map mode in case we are reopening the map after a "cycling order" + ld a,(wMinimapCycleToNextMode) + or a + jr z,@open + + ld a,(wMinimapGroup) + cp $04 + jr c,@notDungeon + + @dungeon: + ld a,(wDungeonIndex) + inc a + cp $09 + jr z,@invalidDungeon + + ld (wDungeonIndex),a + jr @openDungeon + + @invalidDungeon: + xor a + ld (wMinimapGroup),a + jr @open + + @notDungeon: + ld a,(wMinimapGroup) + inc a + cp $02 + jr nc,@swapToDungeon + + ld (wMinimapGroup),a + jr @open + + @swapToDungeon: + ld a,$05 + ld (wMinimapGroup),a + xor a + ld (wDungeonIndex),a + + @openDungeon: + xor a + ld (wDungeonMapScroll),a + ld (wDungeonMapScrollState),a + + @open: + call disableLcd + jp $5ee3 ; mapMenu_state0 +# Change the pointer to state0 inside runMapMenu jump table +02/1edf/: dw extendedBootMapMenu + +# Handle a keybinding to change map type (overworld, Subrosia, dungeons) +02//worldMapStartButtonSupport: | + jp nz,$4f7b ; _closeMenu + ld a,(wKeysJustPressed) + cp BTN_START + ret nz + + ; Put back the menu in "loading" state while cycling to next mode + ld a,$01 + ld (wMinimapCycleToNextMode),a + xor a + ld (wMenuActiveState),a + call fastFadeoutToWhite + ret +# Replace `jp nz,closeMenu` in @noDirectionButtonPressed by an extension (outside maps) +02/2089/: call worldMapStartButtonSupport +# Replace `jp nz,closeMenu` in @dungeon by an extension (dungeon maps) +02/202c/: call worldMapStartButtonSupport + +# Extend the close menu procedure to reset temporarily changed variable with the value +# they had when initially opening the map +02//extendedMenuClose: | + ; Reset minimap group & dungeon index to their original value + ld a,(wOriginalDungeonIndex) + ld (wDungeonIndex),a + ld a,(wOriginalMinimapGroup) + ld (wMinimapGroup),a + xor a + ld (wMinimapCycleToNextMode),a + + ld hl,wMenuLoadState + ret +# Replace `ld hl,wMenuLoadState` in menuStateFadeOutOfMenu by an extension +02/108b/: call extendedMenuClose + +# Tests if player really is in the dungeon that is being looked at through the dungeon +# map (sets flag Z if inside) +02//checkIfInsideDungeon: | + ld a,(wDungeonIndex) + cp $ff + jr z,@invalid + + @valid: + ld b,a + ld a,(wOriginalDungeonIndex) + cp b + ret + + @invalid: + or a + ret + +# Only draw dungeon map if it's the dungeon we are currently in (because technical reasons) +02//dontDrawDungeonIfNotInside: | + call checkIfInsideDungeon + jr nz,@done + call $6743 ; dungeonMap_generateScrollableTilemap + call $66e1 ; dungeonMap_drawFloorList + call $6791 ; dungeonMap_updateScroll + @done: + ret +# replace the unique calls of the 3 functions contained inside dontDrawDungeonIfNotInside by a +# call to it +02/1f64/: | + call dontDrawDungeonIfNotInside + jr $04 + +# Only draw annex sprites in dungeon map (Link icon, cursors and boss symbols...) if we currently +# are in the dungeon +02//dontDrawDungeonSpritesIfNotInside: | + call checkIfInsideDungeon + jr nz,@done + call $64a3 ; dungeonMap_drawLinkIcons + call $64f9 ; dungeonMap_drawCursor + call $6521 ; dungeonMap_drawArrows + call $648a ; dungeonMap_drawBossSymbolForFloor + call $646e ; dungeonMap_drawFloorCursor + @done: + ret +# Replace the only call to dungeonMap_drawLinkIcons by the extension above +02/23dc/: jp dontDrawDungeonSpritesIfNotInside + +# Prevent from scrolling floors if not inside dungeon (and therefore map is not displayed) +02//onlyAllowScrollingIfInsideDungeon: | + call checkIfInsideDungeon + jr nz,@done + jp $0294 ; getInputWithAutofire + + @done: + pop af ; pop return address from stack + ret +# Replace first call of dungeonMap_scrollingState0 by this extension +02/22fe/: call onlyAllowScrollingIfInsideDungeon + +# On overworld & Subrosia maps, don't draw the arrow if we aren't in that dimension +02//drawWorldArrowOnlyIfInDimension: | + ld a,(wMinimapCycleToNextMode) + or a + jr nz,@differentDimension + ld a,(wFrameCounter) + ret + @differentDimension: + pop af ; pop return address from stack + ret +# Replace the first instruction of mapMenu_drawArrow by a call to above extension +02/257d/: call drawWorldArrowOnlyIfInDimension + +# Override initialization of cursor position to set it to origin position if we have cycled +# mode at least once +02//initializeCursorPosition: | + ld (wMapMenuCursorIndex),a + ld a,(wMinimapCycleToNextMode) + or a + jr z,@done + xor a + ld (wMapMenuCursorIndex),a + @done: + ret +# Replace the instruction `ld (wMapMenu.cursorIndex),a` inside mapMenu_state0 by a call to above function +02/1f2d/: call initializeCursorPosition + +# If dungeon map is owned, return with flag Z unset to indicate dungeon has +# been visited and needs to be displayed on map. Otherwise, perform the usual +# @checkDungeonEntered which tests if dungeon has REALLY been visited +02//extendedCheckForDungeonDisplay: | + ld hl,wDungeonMaps + call checkFlag + ld a,c + ret nz + jp $611a ; @checkDungeonEntered +# Replace the unique call to @checkDungeonEntered by a call to above extension +02/20f3/: call extendedCheckForDungeonDisplay + +# ========== PORTAL TEXT HANDLING + +# Put a fixed "Maku Tree" text on its tile instead of being able to ask for remote advice by selecting its tile. +# This way, we can reuse the "@specialCode0" to handle dynamic names for Subrosian Portals +02/2ae2/: db $17 + +# Stop calling func_6e06 to set portal bit when it is spawned +15/2dfb/: | + nop + nop + nop + +# Make minimapPopupType_portalSpot always show the portal icon +02/21e0/: | + ld a,e + ret + +# Generic function setting portal bit for the current room (or the matching overworld room, when in Horon basement or +# in Temple Remains summit) +05//playSoundAndSetPortalBit: | + call playSound ; Function call which was overwritten + ; Call the function which was initially used to set the flag when spawning the portal + ld hl,$6e0b + ld e,$15 + jp interBankCall +# Set portal bit when entering a portal +05/0c8d/: call playSoundAndSetPortalBit +# Set portal bit when landing out of a portal +05/0d32/: call playSoundAndSetPortalBit + +# Replace @specialCode0 (used in vanilla for Maku Tree text on map, which is +# useless in rando) by dynamic portal text, showing the portal destination if +# portal has already been visited, or "Unknown Portal" otherwise. +# --------------------------- +# in(c) = portal index +# out(b) = text group +# out(c) = text index +02//getPortalText: | + ld a,(wMapMenuCursorIndex) + ld b,a + ld a,(wMapMenuMode) + + call getRoomFlags + bit 3,a + jr z,@unknownPortal + + ; if "Portal visited" flag is set, show the full portal label + ld b,$56 ; textgroup + ld a,$01 ; base_textid + add a,c + ld c,a + ret + + @unknownPortal: + ; otherwise, show a generic "Unknown Portal" text + ld bc,$5600 + ret +# Overwrite @specialCode0 to call above function +02/20dd/: | + ld a,c + add a,a + swap a + and $0f + ld c,a + + jp getPortalText diff --git a/worlds/tloz_oos/patching/asm/misc.yaml b/worlds/tloz_oos/patching/asm/misc.yaml new file mode 100644 index 000000000000..ee34c380571f --- /dev/null +++ b/worlds/tloz_oos/patching/asm/misc.yaml @@ -0,0 +1,121 @@ +# allow skipping the capcom screen after one second by pressing start. +03//skipCapcom: | + push hl + ld a,($cbb3) + cp $94 + jr nc,@noSkip + call forceEnableIntroInputs + @noSkip: + pop hl + jp decHlRef16WithCap +03/0d6b/: call skipCapcom + +# use different seed capacity table, so that level zero satchel can still +# hold 20 seeds. +3f//seedCapacityTable: | + db $20,$20,$50,$99 + +# make link actionable as soon as he drops into the world. +05/0d98/: jp setLinkIdOverride + +# let link jump down the cliff outside d7, in case of winter sans shovel. +# also let link jump down the snow cliff added in woods of winter. +05//cliffLookup: | + push af + ld a,(wActiveGroup) + or a + jr nz,@noJumpPopAf + ld a,(wActiveRoom) + cp $d0 + jr nz,@notD7Entrance + pop af + cp $a8 + jr nz,@noJump + ld a,$08 + scf + ret + @notD7Entrance: + cp $9d + jr nz,@noJumpPopAf + pop af + cp $99 + jr z,@snowJump + cp $9b + jr nz,@noJump + @snowJump: + ld a,$10 + scf + ret + @noJumpPopAf: + pop af + @noJump: + jp lookupCollisionTable +05/1fe8/: call cliffLookup + +# replace a random item drop with gale seeds 1/0 of the time if the player is +# out of gale seeds. just to be nice since warping out of one-ways is in +# logic. +# 06//dropExtraGalesOnEmpty: | +# ld a,TREASURE_GALE_SEEDS +# call checkTreasureObtained +# jr nc,@done +# ld l,$b8 +# or (hl) +# jr nz,@done +# call getRandomNumber +# cp $40 +# jr nc,@done +# ld c,$08 +# @done: +# call getFreePartSlot +# ret +# 06/07f5/: call dropExtraGalesOnEmpty + +# custom script command to use on d1 entrance screen: disable warp tiles +# until bit of cfc0 is set. fixes a vanilla bug where dismounting an animal +# on that screen allowed you to enter without the key. +0b//d1EntranceScriptCmd: | + pop hl + push bc + ld bc,$9600 + call compareRoom + pop bc + ret nz + ld a,$01 + ld (wDisableWarpTiles),a + xor a + jp $432d +# new script command address and id +0b/006d/: dw d1EntranceScriptCmd +0b/0dea/: db $b2 + +# make all seeds grow in all seasons. +0d/28b5/: cp a + +# allow harvesting seeds from trees with either slingshot or satchel. +10//checkCanHarvestSeeds: | + call checkTreasureObtained + ret c + ld a,TREASURE_SLINGSHOT + jp checkTreasureObtained +10/0b1a/: call checkCanHarvestSeeds + +# move the hitbox for the bridge lever from holodrum plain to natzu to the +# top-left corner of the screen, where it can't be hit, and replace the lever +# tile as well. this prevents the bridge from blocking the waterway. +11/2737/: db $00,$00 # object +21/2267/: db $04 # ricky tile +23/1cb7/: db $04 # moosh tile + +# move d8 magnet ball one tile to the left, so you don't get stuck if +# you go up the stairs without magnet glove. +15/0f62/: db $38 + +3f/060d/: ld hl,seedCapacityTable + +# Remove whole Bipin & Blossom child mechanic +08/2392/: jp interactionDelete +# 08/2395/ up to 08/26fd/ (included) => FREE SPACE + +# Remove Moblin King text when he sees Link to the right of his fort (near Sunken City) +0b/3705/: db $00 diff --git a/worlds/tloz_oos/patching/asm/multi.yaml b/worlds/tloz_oos/patching/asm/multi.yaml new file mode 100644 index 000000000000..42f1bdb231cd --- /dev/null +++ b/worlds/tloz_oos/patching/asm/multi.yaml @@ -0,0 +1,93 @@ +# if the item buffer is nonzero, spawn the item at link and reset the buffer. +# var INTERAC_MULTI_BYTE is used to signal the destination player number, and +# var 71 is set to override normal collect/player properties lookup. +05//checkNetItemBuffer: | + push bc + push de + push hl + + ld a,(wMenuDisabled) + and a + jr nz,@done + ld a,(wLinkGrabState) + and a + jr nz,@done + ld hl,wNetTreasureIn + ldi a,(hl) + or a + jr z,@done + cp $ff + jr nz,@notDeathlink + + ; If item in buffer is 0xFF, it's a deathlink signal so make Link die + ld a,$fe + ld ($cc34),a + jr @done + + @notDeathlink: + ld b,a + ld c,(hl) + call spawnTreasureOnLink + jr nz,@done + ld l,$71 ; Set forced collect mode in var31 + ld (hl),COLLECT_PICKUP_NOFLAG + + ld hl,wNetCountInL + inc (hl) + ld a,(hl) + or a + jr nz,@noOverflow + ld hl,wNetCountInH + inc (hl) + + @noOverflow: + ld hl,wNetTreasureIn + xor a + ldi (hl),a + ld (hl),a + + @done: + pop hl + pop de + pop bc + call linkInteractWithAButtonSensitiveObjects + ret +05/14ca/: call checkNetItemBuffer + +# run treasure state 1 code immediately following treasure state 0 code, so +# that link can pick up items on the same frame they're spawned. this avoids +# issues for treasures spawning on the frame before link gets warped, like +# after subrosian dancing and when failing poe skip. maku seed visuals don't +# quite work right this way, so don't do this for maku seed. +09//treasureCollideImmediately: | + call objectSetVisiblec2 + ld e,$70 + ld a,(de) + cp TREASURE_MAKU_SEED + ret z + jp $4000 ; interactionCode60 +09/0033/: jp treasureCollideImmediately + +# if var 71 is already nonzero, use that as the collect mode instead of +# whatever this is. +15//keepNonzeroCollectMode: | + ld e,$71 + ld a,(de) + and a + jr nz,@next + ld a,b + @next: + ld b,a + swap a + and $07 + ld (de),a + ret +15/065f/: call keepNonzeroCollectMode + +# don't set global flag 30 when defeating onox in multiworld +0f/3145/: | + nop + nop + nop +# don't set file to completed when saving after credits in multiworld +15/09bc/: ret diff --git a/worlds/tloz_oos/patching/asm/new_game.yaml b/worlds/tloz_oos/patching/asm/new_game.yaml new file mode 100644 index 000000000000..ab65661efbc0 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/new_game.yaml @@ -0,0 +1,115 @@ +# setting up a new file - this is done when link is dropped into the world, not +# at actual file creation. + +# Change Link starting point +07/0193/: db option.startingGroup # Room +07/0195/: db option.startingRoom # Room +07/0197/: db option.startingPosY # Y +07/0199/: db option.startingPosX # X +07/01a6/: db option.animalCompanion # Natzu layout + +# Set new game max bombs to 0 instead of 10 +07/018d/: db $00 + +# overwrite initial health and max health (since they are overwritten by another function anyway) to set +# the initial seed selection instead. first byte is low c6xx address, second is index. +07/018e/satchelInitialSelection: db $be, option.defaultSeedType - $20 +07/019a/slingshotInitialSelection: db $bf, option.defaultSeedType - $20 + +# On file creation, set additional flags to skip a few events/cutscenes/tutorials +# from the vanilla game which are cumbersome in a rando context. +07//setInitialFlags: | + ; No linked gaming + xor a + ld (wIsLinkedGame),a + + ; Mark intro as seen + ld a,$0a + call setGlobalFlag + ld a,$1c + call setGlobalFlag + + ; Animal vars + ld a,$ff + ld (wAnimalTutorialFlags),a + + ; Starting Maps & Compasses + ld a,option.startingMapsCompasses + or a + jr z,@skipMapsCompasses + ld a,$ff + ld hl,wDungeonCompasses + ldi (hl),a + ldi (hl),a + ldi (hl),a + ld (hl),a + @skipMapsCompasses: + + ; Remove the requirement to go in the screen under Sunken City tree to make Dimitri bullies appear + ld a,$20 + ld (wDimitriState),a + + ; Change seeds selected by default inside slingshot and satchel + ld a,option.defaultSeedType + ld hl,wSatchelSelectedSeeds + ldi (hl),a ; Satchel initial selection + ld (hl),a ; Slingshot initial selection + + ; Room flag 6 + ld a,$40 + ld ($c796),a ; Remove post-dungeon cutscene at D1 entrance + ld ($c78d),a ; Remove post-dungeon cutscene at D2 entrance + ld ($c760),a ; Remove post-dungeon cutscene at D3 entrance + ld ($c71d),a ; Remove post-dungeon cutscene at D4 entrance + ld ($c78a),a ; Remove post-dungeon cutscene at D5 entrance + ld ($c700),a ; Remove post-dungeon cutscene at D6 entrance + ld ($c7d0),a ; Remove post-dungeon cutscene at D7 entrance + ld ($c800),a ; Remove post-dungeon cutscene at D8 entrance + ld ($c829),a ; Remove Maku Tree cutscene at temple of seasons gate + ld ($c82a),a ; Remove Maku Tree cutscene at winter tower + ld ($c79b),a ; Sokra stump + ld ($c7e9),a ; Sokra in town + ld ($c7a7),a ; Vanilla start room + + ; Room flags 5 | 6 | 7 + ld a,$e0 + ld ($c79a),a # Rosa portal + + ; Room flags 6 | 7 + ld a,$c0 + ld ($c798),a # Troupe + ld ($c7cb),a # Rosa (first encounter) + + ; Room flag 0 + ld a,$01 + ld ($c716),a ; Enable a tile replacement in Temple Remains to prevent softlock when exiting cave + + ; Give L-3 ring box + ld a,$10 + ld ($c697),a # treasure flag + ld a,$03 + ld (wRingBoxLevel),a + + # automatically open tarm gate if requirement is 0 jewels + ld a,option.tarmGateRequiredJewels + or a + jr nz,@done + ld a,$80 + ld ($c763),a + + @done: + ld hl,$4182 ; initialFileVariables + ret +07/000a/: call setInitialFlags + +# Remove the "Accept our quest, hero!" cutscene when launching the game +# for the first time +01/1874/: | + nop + nop + xor a + cp $01 + +# Disable Impa intro script by jumping directly to @impaIntroDone +0b/34aa/: db $74,$ef +# Space from 0x2F4AC to 0x2F4EE (included) is free diff --git a/worlds/tloz_oos/patching/asm/new_treasures.yaml b/worlds/tloz_oos/patching/asm/new_treasures.yaml new file mode 100644 index 000000000000..9ff3caffe224 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/new_treasures.yaml @@ -0,0 +1,165 @@ +############################################################ +# OBJECT DATA +############################################################ +# Change treasure object data for trade items +# objdata_addr = 0x5129 + (TREASURE_ID * 0x4) => treasureObjectData +# Write object info +15/127d/: db $0a,$00,$5a,$70 # Cuccodex (#55) +15/1281/: db $0a,$00,$5b,$71 # Lon Lon Egg (#56) +15/1285/: db $0a,$00,$5c,$72 # Ghastly Doll (#57) +15/11fd/: db $0a,$00,$5d,$73 # Iron Pot (#35) +15/1209/: db $0a,$00,$5e,$74 # Lava Soup (#38) +15/120d/: db $0a,$00,$5f,$75 # Goron Vase (#39) +15/1211/: db $0a,$00,$60,$76 # Fish (#3A) +15/1215/: db $0a,$00,$61,$77 # Megaphone (#3B) +15/1219/: db $0a,$00,$62,$78 # Mushroom (#3C) +15/121d/: db $0a,$00,$63,$79 # Wooden Bird (#3D) +15/1221/: db $0a,$00,$64,$7a # Engine Grease (#3E) +15/1225/: db $0a,$00,$65,$7b # Phonograph (#3F) +15/11bd/: db $0a,$01,$56,$5c # Pirate's Bell (#25) + +# Add custom palette variants for ore chunks to give more variety in values that +# can be found as treasures, just like rupees. +# This block changes the fixed definition for Ore Chunks by a pointer to a group of subids. +15//oreChunksSubidsBlock: | + db $02,$0b,$6b,$2f ; Ore Chunks x50 (orange sprite) + db $02,$0a,$58,$4e ; Ore Chunks x25 (blue sprite) + db $02,$04,$4e,$4d ; Ore Chunks x10 (red sprite) +15/1205/: | + db $80 + dw oreChunksSubidsBlock + db $00 + +# Add essences as real items to enable obtaining them elsewhere +15//essencesTreasureData: | + db $02,$00,$0e,$60 + db $02,$01,$0f,$61 + db $02,$02,$10,$62 + db $02,$03,$11,$63 + db $02,$04,$12,$64 + db $02,$05,$13,$65 + db $02,$06,$14,$66 + db $02,$07,$15,$67 +15/122a/: dw essencesTreasureData + +# Add dungeon items variants to support keysanity +15//smallKeyObjectsTable: | + db $00,$00,$8f,option.smallKeySprite ; D0 (Hero's Cave) + db $00,$01,$90,option.smallKeySprite ; D1 + db $00,$02,$91,option.smallKeySprite ; D2 + db $00,$03,$92,option.smallKeySprite ; D3 + db $00,$04,$93,option.smallKeySprite ; D4 + db $00,$05,$94,option.smallKeySprite ; D5 + db $00,$06,$95,option.smallKeySprite ; D6 + db $00,$07,$96,option.smallKeySprite ; D7 + db $00,$08,$97,option.smallKeySprite ; D8 +15/11ea/: dw smallKeyObjectsTable + +15//bossKeyObjectsTable: | + db $00,$01,$98,$43 ; D1 + db $00,$02,$99,$43 ; D2 + db $00,$03,$9a,$43 ; D3 + db $00,$04,$9b,$43 ; D4 + db $00,$05,$9c,$43 ; D5 + db $00,$06,$9d,$43 ; D6 + db $00,$07,$9e,$43 ; D7 + db $00,$08,$9f,$43 ; D8 +15/11ee/: dw bossKeyObjectsTable + +15//dungeonMapObjectsTable: | + db $00,$00,$18,$40 ; D0 (Hero's Cave) + db $00,$01,$a0,$40 ; D1 + db $00,$02,$a1,$40 ; D2 + db $00,$03,$a2,$40 ; D3 + db $00,$04,$a3,$40 ; D4 + db $00,$05,$a4,$40 ; D5 + db $00,$06,$a5,$40 ; D6 + db $00,$07,$a6,$40 ; D7 + db $00,$08,$a7,$40 ; D8 +15/11f6/: dw dungeonMapObjectsTable + +15//compassObjectsTable: | + db $00,$00,$19,$41 ; D0 (Hero's Cave) + db $00,$01,$a8,$41 ; D1 + db $00,$02,$a9,$41 ; D2 + db $00,$03,$aa,$41 ; D3 + db $00,$04,$ab,$41 ; D4 + db $00,$05,$ac,$41 ; D5 + db $00,$06,$ad,$41 ; D6 + db $00,$07,$ae,$41 ; D7 + db $00,$08,$af,$41 ; D8 +15/11f2/: dw compassObjectsTable + +# Change @mode6 (compasses, maps & boss keys) to make it set the bit related +# to their subid instead of the dungeon we're currently in +3f/0584/: | + nop + nop + nop + nop + +# Change @mode7 (small keys) to make it increment the counter related to their +# subid instead of the dungeon we're currently in +3f/059d/: | + ld a,c + nop + nop + +############################################################ +# INVENTORY DISPLAY DATA +############################################################ +# Change treasure display data for trade items +# displaydata_addr = 0x6da1 + (TREASURE_ID * 7) => treasureDisplayData +# Write treasure ID + display info +3f/2ff4/: db $55,$c0,$00,$c1,$00,$ff,$09 # Cuccodex (#55) +3f/2ffb/: db $56,$c2,$03,$c2,$23,$ff,$0a # Lon Lon Egg (#56) +3f/3002/: db $57,$c3,$00,$c4,$00,$ff,$0b # Ghastly Doll (#57) +3f/2f14/: db $35,$c5,$04,$c6,$04,$ff,$0c # Iron Pot (#35) +3f/2f29/: db $38,$da,$05,$db,$05,$ff,$0d # Lava Soup (#38) +3f/2f30/: db $39,$c7,$05,$c8,$05,$ff,$0e # Goron Vase (#39) +3f/2f37/: db $3a,$c9,$01,$ca,$01,$ff,$0f # Fish (#3A) +3f/2f3e/: db $3b,$d0,$01,$d1,$01,$ff,$10 # Megaphone (#3B) +3f/2f45/: db $3c,$d2,$05,$d3,$05,$ff,$11 # Mushroom (#3C) +3f/2f4c/: db $3d,$d4,$03,$d5,$03,$ff,$12 # Wooden Bird (#3D) +3f/2f53/: db $3e,$d6,$01,$d7,$01,$ff,$13 # Engine Grease (#3E) +3f/2f5a/: db $3f,$d8,$00,$d9,$00,$ff,$14 # Phonograph (#3F) +3f/2ea4/: db $25,$ee,$01,$ef,$01,$ff,$49 # Pirate's Bell (#25) + +############################################################ +# TREASURE ID REFERENCES +############################################################ + +# Change trade sequence NPC checks to look for the new treasure IDs +0b/10c6/: db $df,TREASURE_CUCCODEX # Cuccodex (#55) +05/28e0/: db TREASURE_LON_LON_EGG # Lon Lon Egg (#56) +0b/104a/: db $df,TREASURE_GHASTLY_DOLL # Ghastly Doll (#57) +0b/2552/: db $df,TREASURE_IRON_POT # Iron Pot (#35) +0b/2192/: db $df,TREASURE_LAVA_SOUP # Lava Soup (#38) +0b/2332/: db $df,TREASURE_GORON_VASE # Goron Vase (#39) +0b/1230/: db $df,TREASURE_FISH # Fish (#3A) +0b/2087/: db $df,TREASURE_MEGAPHONE # Megaphone (#3B) +0b/1f26/: db $df,TREASURE_MUSHROOM # Mushroom (#3C) +0b/11fd/: db $df,TREASURE_WOODEN_BIRD # Wooden Bird (#3D) +0b/23b9/: db $df,TREASURE_ENGINE_GREASE # Engine Grease (#3E) +0b/2519/: db $df,TREASURE_PHONOGRAPH # Phonograph (#3F) + +# Pirate's Bell / Rusty Bell check by Piratian Captain +08/2c3c/: | + ld a,TREASURE_PIRATES_BELL + call checkTreasureObtained + ld a,$02 + jr c,@done + + ld a,TREASURE_RUSTY_BELL + call checkTreasureObtained + ld a,$01 + ret nc + ; Returning here exploits the face that var3b of the captain's interaction + ; defaults to 0, meaning we don't need to overwrite 0 by another 0. + ; This enabled saving previous bytes to avoid having to make a new function + ; in this bank which is already *packed* with data. + + @done: + ld e,$7b + ld (de),a + ret \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/progressives.yaml b/worlds/tloz_oos/patching/asm/progressives.yaml new file mode 100644 index 000000000000..31f9446dca12 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/progressives.yaml @@ -0,0 +1,197 @@ + +# progressive item upgrade data (old ID, old related var, new ID, new addr) +15//progressiveUpgrades: | + ; Iron Shield + db TREASURE_SHIELD,$01,TREASURE_SHIELD + dw $52c1 + ; Mirror Shield + db TREASURE_SHIELD,$02,TREASURE_SHIELD + dw $52c5 + + ; Noble Sword + db TREASURE_SWORD,$01,TREASURE_SWORD + dw $52dd + ; Master Sword + db TREASURE_SWORD,$02,TREASURE_SWORD + dw $52e1 + + ; Magic Boomerang + db TREASURE_BOOMERANG,$01,TREASURE_BOOMERANG + dw $52f5 + + ; Hyper Slingshot + db TREASURE_SLINGSHOT,$01,TREASURE_SLINGSHOT + dw 5329 + + ; Roc's Cape + db TREASURE_FEATHER,$01,TREASURE_FEATHER + dw $5331 + + ; Satchel Upgrades + db TREASURE_SEED_SATCHEL,$01,TREASURE_SEED_SATCHEL + dw $52b9 + db TREASURE_SEED_SATCHEL,$02,TREASURE_SEED_SATCHEL + dw $52b9 + + db $ff + +# given a treasure id & subid in b & c, if the treasure needs to be upgraded, +# set hl = the start of the upgraded treasure data + 1 and b = the new +# treasure ID. +15//getUpgradedTreasure: | + ; Filter out the fake "Sword spinslash" treasure given at Hero's Cave chest + ; and while opening Maku Tree gate solely for cosmetic purpose + ld a,b + cp TREASURE_SWORD + jr nz,@notSpinSlash + ld a,c + cp $03 + ld a,b + ret nc + @notSpinSlash: + + ; Check that item was obtained before + call checkTreasureObtained + ld c,a + ld a,b + ret nc + + ; cp TREASURE_TUNE_OF_ECHOES + ; jr nz,@harpDone + ; ld a,TREASURE_TUNE_OF_CURRENTS + ; ld e,a + ; call checkTreasureObtained + ; jr nc,@harpDone + ; ld b,e + ; @harpDone: + + push hl + ld hl,progressiveUpgrades + ld e,$03 + call searchDoubleKey + jr nc,@done + + ; We found a matching entry in progressiveUpgrades table + ldi a,(hl) + ld b,a + ldi a,(hl) + ld e,(hl) + pop hl + ld h,e + ld l,a + inc hl + ret + + @done: + pop hl + ret + +# set hl = the address of the treasure with ID b and sub ID c, accounting for +# progressive upgrades. call through getTreasureDataBCE or +# getTreasureDataSprite! +15//getTreasureData_body: | + ld hl,$5129 ; treasureObjectData in bank 15 + ld a,b + add a,a + rst 10 + ld a,b + add a,a + rst 10 + bit 7,(hl) + jr z,@next + inc hl + ldi a,(hl) + ld h,(hl) + ld l,a + @next: + ld a,c + add a,a + rst 18 + inc hl + jp getUpgradedTreasure + +# load fianl treasure ID, param, and text into b, c, and e. +15//getTreasureDataBCE: | + call getTreasureData_body + ld c,(hl) + inc hl + ld e,(hl) + ret + +# load final treasure sprite into e. +15//getTreasureDataSprite: | + call getTreasureData_body + inc hl + inc hl + ld e,(hl) + ret + +# return treasure data address and collect mode modified as necessary, given +# a treasure ID in dx42. lookupCollectMode must happen before upgradeTreasure +# for multiworld things to work correctly. +15//modifyTreasure: | + call lookupCollectMode + push af + call upgradeTreasure + pop af + ld b,a + swap a + ret +15/065a/: call modifyTreasure + +# given a treasure at dx40, return hl = the start of the treasure data + 1, +# accounting for progressive upgrades. also writes the new treasure ID to +# d070, which is used to set the treasure obtained flag. +15//upgradeTreasure: | + ld e,$42 + ld a,(de) + ld b,a + inc de + ld a,(de) + ld c,a + ; call getMultiworldItemDest + ; call z,getUpgradedTreasure + call getUpgradedTreasure + ld e,$70 + ld a,b + ld (de),a + ret + +# this is a replacement for giveTreasure that accounts for item progression. +# call through giveTreasureCustom or giveTreasureCustomSilent, since this +# function doesn't xor the a that it returns. importantly, this replacement +# treats c as a subID, not a param, so this should *not* be called by +# non-randomized whatevers. +00//giveTreasureCustom_body: | + ld b,a + push hl + ld e,$15 + ld hl,getTreasureDataBCE + call interBankCall + pop hl + ld a,b + jp giveTreasure + +# just gives the treasure, no sound or text. +00//giveTreasureCustomSilent: | + call giveTreasureCustom_body + xor a + ret + +# gives the treasure, plays its sound, and shows its text. +00//giveTreasureCustom: | + call giveTreasureCustom_body + jr z,@noSound + push hl + call playSound + pop hl + @noSound: + ld a,e + cp $ff + ret z + ld b,$00 + ld c,e + call showText + xor a + ret +0a/3b93/: call giveTreasureCustom diff --git a/worlds/tloz_oos/patching/asm/remove_items_on_use.yaml b/worlds/tloz_oos/patching/asm/remove_items_on_use.yaml new file mode 100644 index 000000000000..26b0a64c413c --- /dev/null +++ b/worlds/tloz_oos/patching/asm/remove_items_on_use.yaml @@ -0,0 +1,85 @@ +# This patch injects "consumption" of some items when they are used in-game, removing them from inventory. +# This makes sense in correlation with subscreen_1_improvement, where all owned items are displayed +# and having a reduced set of owned items make the screen more manageable. +# This is especially useful for items the vanilla game considered as one with different subids (e.g. trade items) + +# Lose dungeon keys when used in keyholes for inventory cleansing +06//loseKeyInsideKeyhole: | + ld (wSubscreen1CurrentSlotIndex),a + call checkTreasureObtained + jr nc,@done + ld a,(wSubscreen1CurrentSlotIndex) + call loseTreasure + scf + @done: + ret +06/020d/: call loseKeyInsideKeyhole + +# Inject removal subscripts in some unused space (post end-game smithy secret dialogue, see +# specific_checks.yaml for more info) +0b/35f9/removeCuccodex: | + db orroomflag,$40 + db loseitem,TREASURE_CUCCODEX + dwbe $50e7 ; jump back to 50e7 +0b/10e5/: dwbe removeCuccodex + +05//loseLonLonEgg: | + ld a,TREASURE_LON_LON_EGG + jp loseTreasure +05/28e6/: | + nop + call loseLonLonEgg + +0b/35ff/removeGhastlyDoll: | + db orroomflag,$40 + db loseitem,TREASURE_GHASTLY_DOLL + dwbe $506b ; jump back to 506b +0b/1069/: dwbe removeGhastlyDoll + +0b/3605/removeIronPot: | + db loseitem,TREASURE_IRON_POT + db loadscript,$14 + dw 4b8e ; lavaSoupSubrosianScript_fillPot +0b/2568/: dwbe removeIronPot + +0b/360b/removeLavaSoup: | + db orroomflag,$40 + db loseitem,TREASURE_LAVA_SOUP + dwbe $61d7 ; jump back to 61d7 +0b/21d5/: dwbe removeLavaSoup + +0b/3611/removeGoronVase: | + db orroomflag,$40 + db loseitem,TREASURE_GORON_VASE + dwbe $635c ; jump back to 635c +0b/235a/: dwbe removeGoronVase + +0b/3617/removeFish: | + db orroomflag,$40 + db loseitem,TREASURE_FISH + dwbe $5267 ; jump back to 5267 +0b/1265/: dwbe removeFish + +0b/361d/removeMegaphone: | + db loseitem,TREASURE_MEGAPHONE + db loadscript,$14 + dwbe $db49 ; talon_giveMushroomAfterWaking +0b/20a0/: dwbe removeMegaphone + +0b/3623/removeMushroom: | + db orroomflag,$40 + db loseitem,TREASURE_MUSHROOM + dwbe $5f49 ; jump back to 5f49 +0b/1f47/: dwbe removeMushroom + +0b/3629/removeWoodenBird: | + db orroomflag,$40 + db loseitem,TREASURE_WOODEN_BIRD + dwbe $521e ; jump back to 521e +0b/121c/: dwbe removeWoodenBird + +0b/362f/removeEngineGrease: | + db orroomflag,$40 + db loseitem,TREASURE_ENGINE_GREASE + dwbe $63e9 ; jump back to 63e9 +0b/23e7/: dwbe removeEngineGrease diff --git a/worlds/tloz_oos/patching/asm/rings.yaml b/worlds/tloz_oos/patching/asm/rings.yaml new file mode 100644 index 000000000000..fadc6043e26c --- /dev/null +++ b/worlds/tloz_oos/patching/asm/rings.yaml @@ -0,0 +1,151 @@ +# allow ring list to be accessed through the ring box icon. +02//openRingList: | + ld a,(wInventorySubmenu1CursorPos) + cp $0f + ret nz + ld a,$81 + ld (wRingMenu_mode),a + ld a,$04 + call openMenu + pop hl + ret + +# auto-equip rings when selected in ring list. +02//autoEquipRing: | + call $716c ; _ringMenu_updateSelectedRingFromList + ld (wActiveRing),a + ret + +# don't save gfx when opening ring list from subscreen (they were already +# saved when opening the item menu), and clear screen scroll variables (which +# are saved anyway). +02//ringListGfxFix: | + call setMusicVolume + ld a,(wRingMenu_mode) + bit 7,a + ret z + and $7f + ld (wRingMenu_mode),a + xor a + ldh ($a8),a ; hCameraY + ldh ($aa),a ; hCameraX + ld hl,wScreenOffsetY + ldi (hl),a + ldi (hl),a + jp $5072 ; clearMenu + +# put obtained rings directly into ring list (no need for appraisal), and +# tell the player what type of ring it is. +3f//autoAppraiseRing: | + ld hl,wRingsObtained + ld a,c + and $3f + call setFlag + ld a,c + add a,$40 + ld ($cbb1),a ; part of wTextSubstitutions + ld bc,$301c + call showText + ret + +# blaino normally unequips rings by setting bit 6, which turns the friendship +# ring into the dev ring. don't do that. +00/2376/: ld (hl),$ff + +02/1035/: call ringListGfxFix +02/16a1/: call openRingList +02/2f4a/: call autoEquipRing +3f/061a/: | + nop + jp autoAppraiseRing + +# use expert's or fist ring with only one button unequipped. +06/090e/: nop + +# remove regular text box when getting a ring from a gasha nut so that the +# auto-appraisal text can display instead. +0a//removeGashaNutRingText: | + ld a,c + cp $04 + jp nz,showText + pop hl + ret +0a/0863/: jp removeGashaNutRingText + +# skip forced ring appraisal and ring list with vasu (prevents softlock). +0b/0a2b/: dw $394a + +# replace ring appraisal text with "you got the {ring}". +1f/1d99/: db $02,$03,$0f,$fd,$21,$00 + +# inject a new ring object data providing one subid per ring type to TREASURE_RING +15//ringsObjectData: | + db $09,$ff,$ff,$0e + db $29,$ff,$ff,$0e + db $49,$ff,$ff,$0e + db $59,$ff,$ff,$0e + db $38,$00,$ff,$0e + db $38,$01,$ff,$0e + db $38,$02,$ff,$0e + db $38,$03,$ff,$0e + db $38,$04,$ff,$0e + db $38,$05,$ff,$0e + db $38,$06,$ff,$0e + db $38,$07,$ff,$0e + db $38,$08,$ff,$0e + db $38,$09,$ff,$0e + db $38,$0a,$ff,$0e + db $38,$0b,$ff,$0e + db $38,$0c,$ff,$0e + db $38,$0d,$ff,$0e + db $38,$0e,$ff,$0e + db $38,$0f,$ff,$0e + db $38,$10,$ff,$0e + db $38,$11,$ff,$0e + db $38,$12,$ff,$0e + db $38,$13,$ff,$0e + db $38,$14,$ff,$0e + db $38,$15,$ff,$0e + db $38,$16,$ff,$0e + db $38,$17,$ff,$0e + db $38,$18,$ff,$0e + db $38,$19,$ff,$0e + db $38,$1a,$ff,$0e + db $38,$1b,$ff,$0e + db $38,$1c,$ff,$0e + db $38,$1d,$ff,$0e + db $38,$1e,$ff,$0e + db $38,$1f,$ff,$0e + db $38,$20,$ff,$0e + db $38,$21,$ff,$0e + db $38,$22,$ff,$0e + db $38,$23,$ff,$0e + db $38,$24,$ff,$0e + db $38,$25,$ff,$0e + db $38,$26,$ff,$0e + db $38,$27,$ff,$0e + db $38,$28,$ff,$0e + db $38,$29,$ff,$0e + db $38,$2a,$ff,$0e + db $38,$2b,$ff,$0e + db $38,$2c,$ff,$0e + db $38,$2d,$ff,$0e + db $38,$2e,$ff,$0e + db $38,$2f,$ff,$0e + db $38,$30,$ff,$0e + db $38,$31,$ff,$0e + db $38,$32,$ff,$0e + db $38,$33,$ff,$0e + db $38,$34,$ff,$0e + db $38,$35,$ff,$0e + db $38,$36,$ff,$0e + db $38,$37,$ff,$0e + db $38,$38,$ff,$0e + db $38,$39,$ff,$0e + db $38,$3a,$ff,$0e + db $38,$3b,$ff,$0e + db $38,$3c,$ff,$0e + db $38,$3d,$ff,$0e + db $38,$3e,$ff,$0e + db $38,$3f,$ff,$0e +15/11de/: dw ringsObjectData diff --git a/worlds/tloz_oos/patching/asm/samasa_combination.yaml b/worlds/tloz_oos/patching/asm/samasa_combination.yaml new file mode 100644 index 000000000000..0489d93baa95 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/samasa_combination.yaml @@ -0,0 +1,19 @@ +# Change the actual combination required to open the door +# Inject the combination checked when pressing door buttons... +0a//samasaCombination: /include samasaCombination +# ...then reference it where relevant +0a/2017/: db samasaCombinationLengthMinusOne ; defined in code +0a/2006/: dw samasaCombination + +# Inject a new cutscene for the piratian to show the randomized +# combination by pulling and pressing drawers, just like in vanilla +15//showSamasaCutscene: /include showSamasaCutscene +# Call this new script from the base piratian script +0b/1e4c/: | + db checkabutton + db setdisabledobjectsto91 + db showtextlowindex,$0c + db writeobjectbyte,$7c,$01 + db setspeed,$50 + db loadscript,$15 + dw showSamasaCutscene diff --git a/worlds/tloz_oos/patching/asm/seasons_handling.yaml b/worlds/tloz_oos/patching/asm/seasons_handling.yaml new file mode 100644 index 000000000000..d340e6771daf --- /dev/null +++ b/worlds/tloz_oos/patching/asm/seasons_handling.yaml @@ -0,0 +1,149 @@ +# Natzu and Samasa Desert are summer only, and goron mountain is winter only. +# Northern Peak doesn't matter (it might become spring after you beat the game). +# Note that these names don't correspond 1:1 with the names used on the overworld map, +# which aren't delineated based on season boundaries. +01/3e50/: db defaultSeason.HORON_VILLAGE +01/3e60/: db defaultSeason.EYEGLASS_LAKE # eyeglass lake / d1 sector +01/3e61/: db defaultSeason.EASTERN_SUBURBS +01/3e62/: db defaultSeason.WOODS_OF_WINTER # from d2 to holly's house +01/3e63/: db defaultSeason.SPOOL_SWAMP +01/3e64/: db defaultSeason.HOLODRUM_PLAIN # from blaino to mrs. ruul +01/3e65/: db defaultSeason.SUNKEN_CITY # also mt. cucco +01/3e67/: db defaultSeason.LOST_WOODS # from jewel gate to lost woods +01/3e68/: db defaultSeason.TARM_RUINS # d6 sector +01/3e6b/: db defaultSeason.WESTERN_COAST +01/3e6c/: db defaultSeason.TEMPLE_REMAINS + +# [Warp Group, Warp Dest, Arrival Position, Season to apply] +# An arrival pos of "00" means it isn't checked +# A season of "FF" means nothing changes +04//specificWarpSeasons: | + db 03,ab,44,defaultSeason.HORON_VILLAGE ; subrosia to horon + db 03,a8,14,defaultSeason.TEMPLE_REMAINS ; subrosia to temple remains + db 85,12,00,defaultSeason.WOODS_OF_WINTER ; sunken city to woods of winter + db 80,a7,00,defaultSeason.EYEGLASS_LAKE ; warp to start (holodrum) + db ff + +# This function aims to fix season handling for Subrosia -> interior map warps +# There are two occurences of this in the game +# 1) Subrosia -> Horon warp (mainly for Fixed Horon Season setting) +# 2) Subrosia -> Upper Temple Remains warp (to put back default Temple Remains season when coming from that portal) +04//checkSpecificWarps: | + push bc + push hl + + ld a,(wWarpDestGroup) + ld b,a + ld a,(wWarpDestRoom) + ld c,a + ld e,$02 + ld hl,specificWarpSeasons + call searchDoubleKey + jr nc,@done + + ldi a,(hl) + or a + jr z,@noPosCheck + ld b,a + ld a,(wWarpDestPos) + cp b + jr nz,@done + + @noPosCheck: + ld a,(hl) + cp $04 ; if season is >= 4, it's an invalid placeholder season -> don't change anything + jr nc,@done + ld (wRoomStateModifier),a + + @done: + pop hl + pop bc + jp loadScreenMusicAndSetRoomPack +04/065f/: jp checkSpecificWarps + +# Change setHoronVillageSeason to remove the random component if fixedHoronSeason != 0xff +01/3e2c/: | + ld a,defaultSeason.HORON_VILLAGE + cp $ff + nop + nop + +# Rewrite checkRoomPackAfterWarp_body to process "chaotic" state which is +# now represented by 0xFF value. +01/3e6e/: | + ld a,(wActiveRoomPack) + cp $f0 + jp nc,$7e09 ; determineCompanionRegionSeason + + ld hl,$7e50 ; roomPackSeasonTable + rst 10 ; addAToHL + ld a,(hl) + + ; If season is 0xff, pick a random season instead + cp $ff + jr nz,@setSeason + call getRandomNumber + and $03 + + @setSeason: + ld (wRoomStateModifier),a + ret + + +# Allow for a "quick-switch" to a specific season if the player is holding a specific +# diagonal during the season transition. This diagonal matches the season icon layout +# displayed next to the Rod of Seasons in the game top bar. +14//smartSeasonSwitch_body: | + ld a,(wKeysPressed) + ld b,a + + ; NW => summer + ld a,BTN_UP | BTN_LEFT + and b + cp BTN_UP | BTN_LEFT + jr nz,@testNE + ld b,SEASON_SPRING ; Make it as if current season was spring to try switching to summer + ret + + ; NE => autumn + @testNE: + ld a,BTN_UP | BTN_RIGHT + and b + cp BTN_UP | BTN_RIGHT + jr nz,@testSE + ld b,SEASON_SUMMER ; Make it as if current season was summer to try switching to autumn + ret + + ; SE => winter + @testSE: + ld a,BTN_DOWN | BTN_RIGHT + and b + cp BTN_DOWN | BTN_RIGHT + jr nz,@testSW + ld b,SEASON_AUTUMN ; Make it as if current season was autumn to try switching to winter + ret + + ; SW => spring + @testSW: + ld a,BTN_DOWN | BTN_RIGHT + and b + cp BTN_DOWN | BTN_RIGHT + jr nz,@vanillaCycle + ld b,SEASON_WINTER ; Make it as if current season was winter to try switching to spring + ret + + ; No special input, use the real current season to deduce next season + @vanillaCycle: + ld a,(wRoomStateModifier) + ld b,a + ret +08//smartSeasonSwitch: | + push hl + ld e,$14 + ld hl,smartSeasonSwitch_body + call interBankCall + pop hl + ld a,b + ret +# Inject this in place of "ld a,(wRoomStateModifier)" inside interactionCode15 (INTERACID_USED_ROD_OF_SEASONS) +08/114f/: call smartSeasonSwitch diff --git a/worlds/tloz_oos/patching/asm/shops_handling.yaml b/worlds/tloz_oos/patching/asm/shops_handling.yaml new file mode 100644 index 000000000000..7ca822ecff91 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/shops_handling.yaml @@ -0,0 +1,241 @@ +# Change shopItemReplacementTable so that each item can only be bought once +08/0cf6/shopItemReplacementTable: | + db $3f,$01,$ff,$00 ; Member shop 1 + db $40,$80,$ff,$00 ; Horon shop 3 + db $3f,$02,$ff,$00 ; Member shop 2 + db $40,$40,$ff,$00 ; Horon shop 2 + db $40,$20,$ff,$00 ; Horon shop 1 + db $3f,$08,$ff,$00 ; Member shop 3 + db $00,$00,$00,$00 + db $92,$04,$09,$18 ; Test TREASURE_PUNCH obtained (always) to redirect to entry 0x9 + db $92,$04,$0a,$10 ; Test TREASURE_PUNCH obtained (always) to redirect to entry 0xa + db $3f,$20,$ff,$00 ; Syrup 2 + db $3f,$40,$ff,$00 ; Syrup 3 + db $3f,$80,$ff,$00 ; Syrup 1 + db $00,$00,$00,$00 + db $00,$00,$00,$00 + db $40,$01,$ff,$00 ; Advance 1 + db $40,$02,$ff,$00 ; Advance 2 + db $40,$04,$ff,$00 ; Advance 3 + db $00,$00,$00,$00 + db $00,$00,$00,$00 + db $00,$00,$00,$00 + +# Remove the bits that are altered on-the-fly inside wBoughtShopItems2 when inside a shop +08/0af1/: | + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + +# remove generic text from shopItemTextTable so that replacement text can be displayed. +08/0d46/: db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + +# Remove all "you are already full" checks for shop slots that usually carry hearts, bombs or a shield +08/0a66/: | + ld a,$01 + or a + ret nz + +# Neutralize all vanilla "wBoughtItems1" flag set +0b/07e7/: db $00 +0b/0803/: db $00 +0b/080d/: db $00 +0b/0819/: db $00 +0b/0857/: db $00 +0b/08c1/: db $00 +# Neutralize all vanilla "wBoughtItems2" flag set +0b/082d/: db $00 +0b/0837/: db $00 +0b/0841/: db $00 + +# Call giveTreasureCustom instead of giveTreasure for shop slots, and also +# set the flag related to that shop item so it can't be bought again +08//shopGiveTreasure: | + push bc + push hl + + call giveTreasureCustom + + ld e,$42 + ld a,(de) ; interaction subid + ld hl,shopItemReplacementTable + add a,a + rst 18 ; rst_addDoubleIndex + inc hl + ldd a,(hl) ; bitmask -> a + or a + jr z,@done + + ld l,(hl) ; flag_addr -> l + ld h,$c6 + or (hl) ; bitmask | flag_value -> a + ld (hl),a ; store flag_value with added bit + + @done: + pop hl + pop bc + + push de + call saveFile + pop de + ret +08/0bfb/: call shopGiveTreasure + + +### HORON SHOP ##################################################################### + +# have horon village shop stock and sell its items from the start, and don't +# stop the flute appearing because of animal flags. + +# sword check +08/08d7/: | + nop + nop +# sword check +08/0adf/: | + nop + nop + nop + +# Don't set a ricky flag when buying 150-rupee shop item. +# This has become useless since we randomize the cheap item instead of the special Flute replacement +# 0b/0823/: db ormemory; dw wRickyState; db 00 + + +### MEMBER'S SHOP ################################################################## + +# don't refill seeds when getting the first member's shop item. +08/0c02/: | + nop + nop + nop + + +### ADVANCE SHOP ################################################################### + +# If "open_advance_shop" setting is set, make advance shop always open. +# Otherwise, make it always closed, even on GBA +04/2195/advanceShopDoor: | + ld a,option.openAdvanceShop + or a + ret z + +# Change the initial text the game uses for Advance Shop 1 because... it uses a generic one +# instead of the specific one that exists for it inside textbanks?! +0b/0829/: db $22 + + +### SYRUP'S SHOP ################################################################### + +# Remove Syrup quantity checks, and set price textbox substitution instead to allow for variable prices +08/2d82/: | + ld hl,wTextNumberSubstitution + ld (hl),c + inc l + ld (hl),b + jr $1d + + +### SUBROSIAN MARKET ############################################################### + +# A dictionary with: +# - market item descriptor low address as key +# - flag mask as value +09//marketItemFlags: | + db $db,$01 + db $e3,$02 + db $ef,$04 + db $f3,$08 + db $00 +# 5th item defaults at value 10 + +# Give a randomized item and set the matching flag for the item not to reappear +09//marketGiveTreasure: | + push af + push hl + ld a,l + ld e,a + ld hl,marketItemFlags + call lookupKey + jr c,@found + ld a,$10 + @found: + ld hl,wBoughtSubrosianItems + or (hl) + ld (hl),a + pop hl + pop af + + call giveTreasureCustom + + push de + call saveFile + pop de + + ret +09/388a/: | + call marketGiveTreasure + jr $12 + +# Remove star ore from inventory when buying the first subrosian market +# item. this can't go in the gain/lose items table, since the given item +# doesn't necessarily have a unique ID. +09//tradeStarOre: | + or a + jr nz,@next + push hl + ld hl,$c69a + res 5,(hl) + pop hl + @next: + rst 18 + ldi a,(hl) + ld c,(hl) + ret +09/3887/: call tradeStarOre + +# Remove conditions on market slots (remove 2nd if owning shield, and remove Member's Card if under a +# specific essence requirement) +09/3744/: | + jr $2a + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 + db $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 +# In Subrosian Market, remove item if it was already bought instead of putting the next one in the list +09/376d/: jp nz,interactionDelete +09/377a/: jr nz,$f1 + +# Change the tested flags that make the items disappear +09/37c6/: db $04 # Item 3 +09/37ce/: db $08 # Item 4 +09/37d2/: db $10 # Item 5 + +# Remove ember seeds from prices +09/37af/: db $00,$00 # Item 2 +09/37c7/: db $00,$00 # Item 3 + + +### PRICES ####################################################################### + +08/0c97/: db shopPrices.horonShop1 +08/0c96/: db shopPrices.horonShop2 +08/0c94/: db shopPrices.horonShop3 + +08/0c93/: db shopPrices.memberShop1 +08/0c95/: db shopPrices.memberShop2 +08/0c98/: db shopPrices.memberShop3 + +08/0c9c/: db shopPrices.syrupShop1 +08/0c9d/: db shopPrices.syrupShop2 +08/0c9e/: db shopPrices.syrupShop3 + +08/0ca1/: db shopPrices.advanceShop1 +08/0ca2/: db shopPrices.advanceShop2 +08/0ca3/: db shopPrices.advanceShop3 + +09/37b1/: db shopPrices.subrosianMarket2 +09/37c9/: db shopPrices.subrosianMarket3 +09/37d1/: db shopPrices.subrosianMarket4 +09/37d5/: db shopPrices.subrosianMarket5 + \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/static_items.yaml b/worlds/tloz_oos/patching/asm/static_items.yaml new file mode 100644 index 000000000000..6ac84117a4a5 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/static_items.yaml @@ -0,0 +1,189 @@ +# Format is group,room,treasure_id,treasure_subid +0a//staticItemsReplacementsTable: | + # ------- Freestanding items ------- + dwbe $00d8, locations.horonHeartPiece + dwbe $00af, locations.woodsOfWinterHeartPiece + dwbe $002d, locations.mtCuccoHeartPiece + dwbe $05b2, locations.windmillHeartPiece + dwbe $00d1, locations.graveyardHeartPiece + dwbe $00b1, locations.spoolSwampHeartPiece + dwbe $05c7, locations.templeRemainsHeartPiece + dwbe $0387, locations.mayorsHouseSecretRoom + dwbe $03a1, locations.subrosianHouse + dwbe $07e3, locations.subrosian2dCave + dwbe $0601, locations.d0HiddenBasement + dwbe $04e9, locations.makuTree3Essences + dwbe $04ea, locations.makuTree5Essences + dwbe $04ee, locations.makuTree7Essences + # ------- Digging spots ------- + dwbe $0140, locations.subrosianWildsDiggingSpot + dwbe $0082, locations.spoolSwampDiggingSpot + dwbe $0106, locations.subrosiaBathOreDiggingSpot + dwbe $0157, locations.subrosiaMarketPortalOreDiggingSpot + dwbe $0147, locations.subrosiaWorkerOreDiggingSpot + dwbe $013a, locations.subrosiaTempleOreDiggingSpot + dwbe $0107, locations.subrosiaNorthernVolcanoesOreDiggingSpot + dwbe $0120, locations.subrosiaD8PortalOreDiggingSpot + dwbe $0142, locations.subrosiaWesternVolcanoesOreDiggingSpot + # ------- Drops / spawned items ------- + dwbe $041b, locations.d1StalfosDrop + dwbe $0434, locations.d2RopeDrop + dwbe $047b, locations.d4PotPuzzle + dwbe $0475, locations.d4Pool + dwbe $04ab, locations.d6MagnetBallDrop + dwbe $0545, locations.d7ZolButton + dwbe $0535, locations.d7ArmosPuzzle + dwbe $053d, locations.d7DropNorthOfStairMaze + dwbe $0582, locations.d8EyeDrop + dwbe $0575, locations.d8HardhatDrop + dwbe $057f, locations.d8GhostArmosDrop + db $ff + +0a//staticItemsReplacementsLookup_body: | + push bc + ld a,(wActiveGroup) + ld b,a + ld a,(wActiveRoom) + ld c,a + ld e,$02 + ld hl,staticItemsReplacementsTable + call searchDoubleKey + pop bc + ret nc + ld b,(hl) ; item id + inc hl + ld c,(hl) ; item subid + ret +00//staticItemsReplacementsLookup: | + push de + push hl + ld e,$0a + ld hl,staticItemsReplacementsLookup_body + call interBankCall + pop hl + pop de + ret + + +### FREESTANDING ITEMS ########################################### + +# staticHeartPiece (0x26381) +09/2381/: call staticItemsReplacementsLookup +# staticGashaSeed (0x26608) +09/2608/: call staticItemsReplacementsLookup +# sidescrollingStaticGashaSeed (0x266aa) +09/26aa/: call staticItemsReplacementsLookup + + +### DIGGABLE ITEMS ########################################### + +# Replace oreChunkDigSpot with generic behavior for randomized digging spot stuff +09/2345/handleRandomizedDigSpot: | + call getThisRoomFlags + and $20 + jp nz,interactionDelete + + call staticItemsReplacementsLookup + + call getFreeInteractionSlot + ret nz + ld (hl),INTERACID_TREASURE + inc l + ld (hl),b + inc l + ld (hl),c + call objectCopyPosition + jp interactionDelete + +# randomRingDigSpot (0x265F5) +09/25f5/: jp handleRandomizedDigSpot + + +### SPAWNED ITEMS (Drops, etc...) ########################################### + +0b//handleRandomizedSpawnedItem: | + ld (hl),INTERACID_TREASURE + inc l + jp staticItemsReplacementsLookup + +# Call our custom handler in scriptCmd_spawnItem +0b/0416/: call handleRandomizedSpawnedItem + +# stop d4 pool item from incrementing subindex when it hits the water. +09/00fb/: | + nop + nop + nop + nop + nop + +# for the item dropped in the room *above* the trampoline. +15/15d8/aboveD7ZolButtonId: db locations.d7ZolButton.id +15/15db/aboveD7ZolButtonSubid: db locations.d7ZolButton.subid + + +### MAKU TREE MISSABLE GASHA SEEDS ########################################### + +# Remove access to stairs behind Farore on 2 essences (this replaces the function that adds +# a sign in front of Bipin & Blossom's house when the game is completed) +04/2189/: | + call getEssenceCount + cp $03 + ret nc + jp removeFaroreStairs +04//removeFaroreStairs: | + ld hl,$cf0c + ld (hl),$b0 + ret + +# Remove the original reference to the function above on the exterior of Bipin & Blossom house map +04/214e/: db $00 +# Attach the modified function above to Farore's room by extending the group4 tile changer table +04//roomTileChangerCodeGroup4DataExt: | + db $61,$2e + db $78,$02 + db $2e,$04 + db $64,$05 + db $89,$06 + db $bb,$07 + db $e8,$08 + db $00 +04/211c/: dw roomTileChangerCodeGroup4DataExt + +# Remove access to first refill room on 4 essences +25/09d8/: db $80,$80,$80 +25/09df/: db $64 +25/09f6/: db $64 +25/0a02/: db $40 + +# Remove access to second refill room on 6 essences +25/160a/: db $63 + +# Remove 3 essence original item +11/2373/: db $4c,$02,$38,$10,$ff +# Add an item inside the hallway (accessible at 3+ essences) that replaces the item removed above +11//hallwayMapObjects: | + db $f2 + db $6b,$19,$58,$c8 + db $ff +11/211d/: dw hallwayMapObjects + +# Remove 5 essences original item +11/23ad/: db $4c,$02,$68,$10,$ff +# Add an item inside the first refill pool (accessible at 5+ essences) that replaces the item removed above +11//refillPool1Objects: | + db $f2 + db $6b,$19,$80,$78 + db $f3,$76,$40 ; spawn usual items + db $ff +11/211f/: dw refillPool1Objects + +# Remove 7 essences original item +11/2387/: db $4c,$02,$28,$10,$4c,$03,$18,$90,$ff +# Add an item inside the second refill pool (accessible at 7+ essences) that replaces the item removed above +11//refillPool2Objects: | + db $f2 + db $6b,$19,$80,$78 + db $f3,$60,$40 ; spawn usual items + db $ff +11/2127/: dw refillPool2Objects diff --git a/worlds/tloz_oos/patching/asm/subscreen_1_improvement.yaml b/worlds/tloz_oos/patching/asm/subscreen_1_improvement.yaml new file mode 100644 index 000000000000..d6d042e86fb1 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/subscreen_1_improvement.yaml @@ -0,0 +1,157 @@ +# Subscreen 1 display changes to accomodate with trade items +# ========================================================== + +# Overwrite subscreen1TreasureData into a plain list of items that are allowed to be displayed +# inside subscreen 1. +02/1d70/subscreen1TreasureDataNew: | + db TREASURE_ROUND_JEWEL + db TREASURE_PYRAMID_JEWEL + db TREASURE_SQUARE_JEWEL + db TREASURE_X_SHAPED_JEWEL + db TREASURE_BOMB_FLOWER + db TREASURE_BOMB_FLOWER_LOWER_HALF + db TREASURE_GASHA_SEED + + db TREASURE_CUCCODEX + db TREASURE_LON_LON_EGG + db TREASURE_GHASTLY_DOLL + db TREASURE_IRON_POT + db TREASURE_LAVA_SOUP + db TREASURE_GORON_VASE + db TREASURE_FISH + db TREASURE_MEGAPHONE + db TREASURE_MUSHROOM + db TREASURE_WOODEN_BIRD + db TREASURE_ENGINE_GREASE + db TREASURE_PHONOGRAPH + db TREASURE_SPRING_BANANA + db TREASURE_MASTERS_PLAQUE + db TREASURE_RED_ORE + db TREASURE_HARD_ORE + db TREASURE_BLUE_ORE + db TREASURE_GNARLED_KEY + db TREASURE_FLOODGATE_KEY + db TREASURE_DRAGON_KEY + db TREASURE_RUSTY_BELL + db TREASURE_PIRATES_BELL + db TREASURE_STAR_ORE + db TREASURE_RIBBON + db TREASURE_MEMBERS_CARD + db TREASURE_FLIPPERS + db TREASURE_POTION + + db $00 + +# Table containing the position on screen where to draw sprites for each slot index. +# A position of $00 means the slot is reserved for hardcoded items (see "hardcodedPositions"), $ff means end of table +02//slotPositionsTable: | + db $01,$04,$07,$0a,$00 + db $31,$34,$37,$3a,$00 + db $61,$64,$67,$6a,$00 + db $ff + +# Table containing the hardcoded positions for a few specific items. +# Format is : (treasure_id, screen_position, slot_index) +02//hardcodedPositions: | + db TREASURE_ROUND_JEWEL, $0d, $04 + db TREASURE_SQUARE_JEWEL, $0e, $04 + db TREASURE_PYRAMID_JEWEL, $1d, $04 + db TREASURE_X_SHAPED_JEWEL, $1e, $04 + db TREASURE_BOMB_FLOWER, $2d, $09 + db TREASURE_BOMB_FLOWER_LOWER_HALF, $4d, $09 + db TREASURE_GASHA_SEED, $6d, $0e + db $ff + +# This extension procedure injected at the beginning of inventorySubscreen1_drawTreasures initializes +# the "current slot" value in RAM with an appropriate value +02//inventorySubscreen1_drawTreasures_setup: | + ld hl,subscreen1TreasureDataNew + xor a + ld (wSubscreen1CurrentSlotIndex),a + ret +02/1b2b/: call inventorySubscreen1_drawTreasures_setup + +# Instead of reading position where to draw sprites from the input table, we are sequentially +# drawing sprites in empty slot from left to right, top to bottom. +# This function specifically take the "current slot index" from RAM, increment it in most cases +# and determines the position where to draw the sprite from the ID (using the slotPositionsTable declared above). +# It also handles a few specific cases described in the "hardcodedPositions" table. +02//inventorySubscreen1_drawTreasures_computeAddr: | + ldh ($8b),a + + push hl + dec hl + ld a,(hl) ; treasure ID -> a + ld hl,hardcodedPositions + ld e,$02 + call searchKey + ld a,(hl) ; potential hardcoded position -> a + jr c,@return ; match was found, return hardcoded position + + ; Not a hardcoded position, draw it in the next free slot + @readSlotPosition: + ld hl,wSubscreen1CurrentSlotIndex + ld a,(hl) + inc (hl) + + ld hl,slotPositionsTable + rst 10 ; rst_addAToHL + ld a,(hl) + or a + jr z,@readSlotPosition ; A zero position means slot is reserved for hardcoded stuff, just skip it + + @nonZeroPosition: + inc a + or a + jr z,@endOfTable ; A $ff position means we reached end of table, stop drawing + dec a + @return: + pop hl + ret + + ; We reached end of table, pop return address and jump onto next step + @endOfTable: + pop hl + pop de + jp $5b58 ; undrawRingBox +# Replace "ldh ( c + + ; pop HL while preserving return address + pop de + pop hl + push de + + ; Determine if last item position was hardcoded or used wSubscreen1CurrentSlotIndex + push hl + dec hl + ld a,(hl) ; treasure ID -> a + ld hl,hardcodedPositions + ld e,$02 + call searchKey + jr nc,@regularItem ; match was found, return hardcoded position + + @hardcodedItem: + inc hl + ld a,(hl) ; hardcoded slot index -> a + pop hl + ret + + @regularItem: + ld a,(wSubscreen1CurrentSlotIndex) ; current slot index -> a + dec a + pop hl + ret +02/1b49/: call inventorySubscreen1_drawTreasures_setCurrentSlotForText + +# Remove the two "inc" instructions at the end of the function +02/1b54/: | + nop + nop + +# Remove Maku Seed from the subscreen +02/1d26/: ret \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/tarm_gate_requirement.yaml b/worlds/tloz_oos/patching/asm/tarm_gate_requirement.yaml new file mode 100644 index 000000000000..1d26b6b24ba8 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/tarm_gate_requirement.yaml @@ -0,0 +1,29 @@ +# Instead of testing a fixed value (0x0f) on a bitfield, we add a function +# which counts the number of inserted jewels... +0a//getInsertedJewelsCount: | + ld a,(wInsertedJewels) + jp getNumSetBits +# ...then call it before comparing with the required amount to open the +# gates +0a/1092/: | + call getInsertedJewelsCount + cp option.tarmGateRequiredJewels + +#################### +# Bring X precious # +# items for the # +# door to open. # +#################### +1f/1169/tarmRequirementText: | + db $42,$03,$3e ; "Bring " + db $09,$01 ; color start + db $30 + option.tarmGateRequiredJewels ; required jewel count digit + db $05,$e8 ; space + color end + db $03,$4a,$01 ; "precious\n" + + db $04,$cc ; "items" + db $02,$a1,$01 ; " for the" + + db $05,$a7 ; "door" + db $03,$b0 ; " to " + db $05,$02,$2e,$00 ; "open." diff --git a/worlds/tloz_oos/patching/asm/text.yaml b/worlds/tloz_oos/patching/asm/text.yaml new file mode 100644 index 000000000000..163995d02ebe --- /dev/null +++ b/worlds/tloz_oos/patching/asm/text.yaml @@ -0,0 +1,308 @@ +# "Got Archipelago Item" text, replacing Ring Box L1 vanilla string +1d/1b01/TX_0057: | + db $03,$e8,$04,$42,$05,$ea ; You found an + db $69,$74,$65,$6d,$20,$04,$91,$61,$6e,$03,$0f,$01 ; item for another + db $03,$75,$21,$00 ; world! + +# "Got Ore Chunks (x25)" text, replacing Ring Box L2 vanilla string +1d/1b1e/TX_0058: | + db $02,$12,$32,$35,$01 ; You got 25 + db $02,$09,$21,$00 ; Ore Chunks! + +# ??? +0a/3b9e/: | + ret + nop + nop + +# Handle default seed text which doesn't exist in vanilla game +1e/2265/TX_1704: | # 1704, cutscene after d1 + db $02,$12,$04,$79,$01 ; You got Ember + db $02,$53,$21,$20,$05,$a9,$01 ; Seeds! Open + db $79,$02,$65 ; your Seed + db $02,$6e,$05,$da,$04,$aa,$01 ; Satchel to use + db $74,$68,$65,$6d,$2e,$00 ; them. +10//useEmberSeedText: | + cp $e5 + jr nz,@done + ld bc,$1704 + @done: + jp showText +10/0ade/: call useEmberSeedText + +# Remove the mention of seeds being included with the seed satchel, since Ember was most likely +# the wrong one in random given seed type +1d/1730/: db $00 + +1e/24d7/TX_1716: /include text.horonShop1 # (Cutscene after d8) +1e/281d/TX_172d: /include text.horonShop2 # (Cutscene after d6) +1e/2361/TX_1707: /include text.horonShop3 # (Cutscene after d2) + +1e/0c05/TX_0c12: /include text.advanceShop1 # (Onox finding Din in linked intro troupe cutscene) +1e/0c3f/TX_0c13: /include text.advanceShop2 # (Onox taunting Link in linked intro troupe cutscene) +1e/1497/TX_0e25: /include text.advanceShop3 # (vanilla text) + +1e/1052/TX_0d0c: /include text.syrupShop1 # (text shown AFTER buying bombchus in vanilla) +1e/0f19/TX_0d01: /include text.syrupShop2 # (vanilla text) +1e/0fac/TX_0d05: /include text.syrupShop3 # (vanilla text) + +1e/1375/TX_0e1c: /include text.memberShop1 # (vanilla text) +1e/23e0/TX_1709: /include text.memberShop2 # (cutscene after d3) +1e/13a0/TX_0e1e: /include text.memberShop3 # (vanilla text) + +# Hint for the sequence going to pedestal +1e/056d/TX_0b50: /include text.lostWoodsPedestalSequence +# Hint for the sequence going to D6 +20/1fff/TX_4500: | + db $04,$59,$03,$b3,$3a,$01 ; Replace "travel west," by "follow this: " + /include text.lostWoodsMainSequence +1f/33f6/TX_3604: | # This one looks similar to the previous one, but I don't know where it's used. Linked games maybe? + db $04,$59,$03,$b3,$3a,$01 ; Replace "travel west," by "follow this: " + /include text.lostWoodsMainSequence + +# Replace cutscene after d4 is d5 < d4 by a copy of the end text inside textbank 17, which +# will be used by text injections performed in that textbank +1e/27ba/TX_172b: | + db $04,$fc,$02,$8b,$04,$b7 ; How about it? + db $02,$fe,$03,$bf,$00 ; Sure / No + +1e/2699/TX_1726: | # (cutscene after d4 if d4 < d5) + /include text.subrosianMarket1 + db $07,$2b # jump to TX_172b (end text copy) +1e/26f6/TX_1728: | # (cutscene after d5 if d5 < d4) + /include text.subrosianMarket2 + db $07,$2b # jump to TX_172b (end text copy) +1f/0df1/TX_2b05: | # (vanilla text slot) + /include text.subrosianMarket3 + db $07,$0b # jump to TX_2b0b (original end text) +1e/2777/TX_172a: | # (cutscene after d5 if d4 < d5) + /include text.subrosianMarket4 + db $07,$2b # jump to TX_172b (end text copy) +1f/0ed4/TX_2b10: | # (vanilla text slot) + /include text.subrosianMarket5 + db $07,$0b # jump to TX_2b0b (original end text) + +# Change D8 introduction text to “Sword & Shield Dungeon” from “Sword & Shield Maze”, since every other mention of it +# was using “Dungeon” naming +1d/21a1/TX_0208: db $44,$05,$8a,$00 # "Dungeon" + +# Shorten initial Maku Tree text script when giving Gnarled Key, replacing lots of instructions by ineffective +# "disableinput" instructions +0b/316a/: db $bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd,$bd +# (TX_1700 & TX_1701 are therefore free to use) + +# replace text in script command 9a (showtextnonexitable) using +# scriptTextReplacements: four-byte entries, first two bytes are old text +# index, second two bytes are new text index. +0b//scriptTextReplacements: | + db $0e,$04,$17,$16 ; Horon shop 1 + db $0e,$03,$17,$2d ; Horon shop 2 + db $0e,$02,$17,$07 ; Horon shop 3 + db $0e,$22,$0c,$12 ; Advance shop 1 + db $0e,$23,$0c,$13 ; Advance shop 2 + db $0d,$0a,$0d,$0c ; Syrup shop 1 + db $0e,$1d,$17,$09 ; Member shop 2 + db $2b,$00,$17,$26 ; Market 1 + db $2b,$01,$17,$28 ; Market 2 + db $2b,$06,$17,$2a ; Market 4 + db $ff +0b//scriptShowTextNonExitableCustom: | + push de + ld e,$02 + ld hl,scriptTextReplacements + call searchDoubleKey + pop de + jr nc,@done + ldi a,(hl) + ld b,a + ld c,(hl) + @done: + jp showTextNonExitable +0b/02e8/: call scriptShowTextNonExitableCustom + +# In case where dungeon has not been visited, make popup indicate a generic +# "Unknown Dungeon" instead of the original region where the dungeon is located, +# which can be a spoiler in runs with shuffled dungeons +02/20f8/: | + ld b,$06 + ld c,$02 +# Replace TX_0602 (linked story text) by "Unknown Dungeon" +1d/2735/TX_0602: db $55,$6e,$04,$98,$6e,$20,$44,$05,$8a,$00 + + +# =================================================================== +# TRADE ITEMS OBTENTION TEXT +# =================================================================== + +# Vanilla trade items obtention text mention the fact that a trade item was exchanged with another, also adding +# fancy icons and not so useful descriptions. These patches aim to provide simpler and straightforward text +# for those items. +1d/1b8d/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$04,$32,$20,$45,$67,$67,$04,$ef,$00 ; "Lon Lon Egg!" +1d/1bc2/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$47,$68,$05,$ad,$79,$20,$44,$6f,$6c,$6c,$04,$ef,$00 ; "Ghastly Doll!" +1d/1bfb/: | + db $02,$1b,$6e,$01 ; "You got an" + db $09,$01,$05,$83,$04,$ef,$00 ; "Iron Pot!" +1d/1c26/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$04,$0b,$04,$ef,$00 ; "Lava Soup!" +1d/1c46/: | + db $02,$03 ; "You got the" + db $09,$01,$03,$7b,$56,$61,$73,$65,$04,$ef,$00 ; "Goron Vase!" +1d/1c75/: | + db $02,$57,$20,$73,$6f,$6d,$65,$01 ; "You got some" + db $05,$91,$21,$00 ; "Fish!" +1d/1c9f/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$03,$63,$04,$ef,$00 ; "Megaphone!" +1d/1cc5/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$4d,$03,$20,$04,$ef,$00 ; "Mushroom!" +1d/1cee/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$03,$77,$04,$ef,$00 ; "Wooden Bird!" +1d/1d15/: | + db $02,$57,$20,$73,$6f,$6d,$65,$01 ; "You got some" + db $09,$01,$04,$29,$04,$ef,$00 ; "Engine Grease!" +1d/1d31/: | + db $02,$1b,$01 ; "You got a" + db $09,$01,$03,$c8,$04,$ef,$00 ; "Phonograph!" + +# =================================================================== +# CUSTOM TEXT SYSTEM +# =================================================================== + +# Add portal tooltips for the map +3f//TX_5600: db $55,$6e,$04,$98,$6e,$20,$50,$6F,$72,$74,$61,$6C,$00 ; "Unknown Portal" +3f//TX_5601: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $45,$61,$73,$74,$65,$72,$6E,$20,$53,$75,$62,$75,$72,$62,$73,$00 ; "Eastern Suburbs" +3f//TX_5602: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$21,$00 ; "Spool Swamp" +3f//TX_5603: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $4D,$74,$2E,$20,$43,$75,$63,$63,$6F,$00 ; "Mt. Cucco" +3f//TX_5604: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $45,$04,$33,$20,$4C,$05,$E4,$00 ; "Eyeglass Lake" +3f//TX_5605: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$14,$00 ; "Horon Village" +3f//TX_5606: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $54,$04,$7d,$20,$52,$04,$d7,$00 ; "Temple Remains" +3f//TX_5607: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $74,$04,$7d,$20,$05,$92,$00 ; "temple summit" + +3f//TX_5608: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$83,$20,$02,$b2,$00 ; "Subrosia Village" +3f//TX_5609: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$19,$20,$4D,$61,$72,$6B,$65,$74,$00 ; "Subrosian Market" +3f//TX_560a: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$19,$20,$57,$69,$6C,$64,$73,$00 ; "Subrosian Wilds" +3f//TX_560b: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $47,$04,$09,$20,$46,$04,$58,$00 ; "Great Furnace" +3f//TX_560c: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $48,$05,$f5,$03,$e3,$50,$69,$72,$61,$74,$65,$73,$00 ; "House of Pirates" +3f//TX_560d: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$19,$20,$56,$05,$85,$00 ; "Subrosian Volcanoes" +3f//TX_560e: | + db $50,$6F,$72,$74,$61,$6C,$03,$b2 ; "Portal to" + db $02,$19,$20,$44,$05,$8a,$00 ; "Subrosian Dungeon" + +3f//TX_560f: /include text.smallKeyD0 +3f//TX_5610: /include text.smallKeyD1 +3f//TX_5611: /include text.smallKeyD2 +3f//TX_5612: /include text.smallKeyD3 +3f//TX_5613: /include text.smallKeyD4 +3f//TX_5614: /include text.smallKeyD5 +3f//TX_5615: /include text.smallKeyD6 +3f//TX_5616: /include text.smallKeyD7 +3f//TX_5617: /include text.smallKeyD8 +3f//TX_5618: /include text.bossKeyD1 +3f//TX_5619: /include text.bossKeyD2 +3f//TX_561a: /include text.bossKeyD3 +3f//TX_561b: /include text.bossKeyD4 +3f//TX_561c: /include text.bossKeyD5 +3f//TX_561d: /include text.bossKeyD6 +3f//TX_561e: /include text.bossKeyD7 +3f//TX_561f: /include text.bossKeyD8 +3f//TX_5620: /include text.dungeonMapD1 +3f//TX_5621: /include text.dungeonMapD2 +3f//TX_5622: /include text.dungeonMapD3 +3f//TX_5623: /include text.dungeonMapD4 +3f//TX_5624: /include text.dungeonMapD5 +3f//TX_5625: /include text.dungeonMapD6 +3f//TX_5626: /include text.dungeonMapD7 +3f//TX_5627: /include text.dungeonMapD8 +3f//TX_5628: /include text.compassD1 +3f//TX_5629: /include text.compassD2 +3f//TX_562a: /include text.compassD3 +3f//TX_562b: /include text.compassD4 +3f//TX_562c: /include text.compassD5 +3f//TX_562d: /include text.compassD6 +3f//TX_562e: /include text.compassD7 +3f//TX_562f: /include text.compassD8 + +3f//customTextTable: | + dw TX_5600, TX_5601, TX_5602, TX_5603 + dw TX_5604, TX_5605, TX_5606, TX_5607 + dw TX_5608, TX_5609, TX_560a, TX_560b + dw TX_560c, TX_560d, TX_560e, TX_560f + dw TX_5610, TX_5611, TX_5612, TX_5613 + dw TX_5614, TX_5615, TX_5616, TX_5617 + dw TX_5618, TX_5619, TX_561a, TX_561b + dw TX_561c, TX_561d, TX_561e, TX_561f + dw TX_5620, TX_5621, TX_5622, TX_5623 + dw TX_5624, TX_5625, TX_5626, TX_5627 + dw TX_5628, TX_5629, TX_562a, TX_562b + dw TX_562c, TX_562d, TX_562e, TX_562f + +# Override getTextAddress to have a specific custom bank_id point to text somewhere else +3f//checkCustomTextOverride: | + ld (w7ActiveBank),a + ld a,(wTextIndexH) + cp $5a ; 0x56 is the custom text group, and there are 4 dictionaries before them + ret nz + ld hl,customTextTable + ld a,(wTextIndexL) + rst 18 ; rst_addDoubleIndex + ldi a,(hl) + ld h,(hl) + ld l,a + ld a,$3f ; 0x3f is the ROM bank where custom text is located + ld (w7ActiveBank),a + ret +3f/0fd9/: call checkCustomTextOverride + +# "Get item" text is confined to text group $00, which makes it hard to add new +# entries without rebuilding the whole textbanks. We inject this small function +# to check for text IDs > 0x80, in which case we unset the 7th bit and redirect +# to our custom textbank. +09//checkCustomTreasureTextId: | + bit 7,a + jr nz,@customText + + ; Usual behavior: take the specified text_id from text group 0 + ld c,a + ld b,$00 + ret + + @customText: + ; Custom behavior: take the specified (text_id-0x80) from custom text group + sub $80 + ld c,a + ld b,$56 ; CUSTOM_TEXT_GROUP_ID + ret +09/02f6/: call checkCustomTreasureTextId \ No newline at end of file diff --git a/worlds/tloz_oos/patching/asm/triggers.yaml b/worlds/tloz_oos/patching/asm/triggers.yaml new file mode 100644 index 000000000000..703d52ee35a9 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/triggers.yaml @@ -0,0 +1,38 @@ +# this file is for removal of triggers that would arbitrarily restrict the +# game, and in some cases even cause softlocks. essences are the biggest +# culprit, but there are some other weird things, like not/having a certain +# item or not/having a certain global flag set (that wouldn't logically cause +# whatever event that it causes). + +# initiate all these events without requiring essences: +08/1886/: ld a,$02 # master diver +0a/0be9/: ld a,$02 # ^ +0a/0bf4/: cp $00 # ^ + +08/2c31/: cp $00 # piratian captain + +08/3c40/: cp $00 # subrosian at volcano +08/3cd2/: cp $00 # ^ + +09/0e36/: jp $4eab # spawn moosh +0f/3428/: jr nz,$00 # ^ + +09/0e40/: or $57 # spawn dimitri + +09/0e72/: or $4f # spawn ricky + +# Dimitri doesn't spawn in vanilla if you have flippers. +09/0e55/: | + xor a ; clears flag C so the following `jr nc` always jumps + nop + nop + nop + nop + +# Allow desert pits to work even if player has the actual bell already. +08/33a2/: | + nop + nop + +# don't require rod to get items from season spirits. +0b/0eb1/: db jumpifitemobtained,TREASURE_PUNCH diff --git a/worlds/tloz_oos/patching/asm/util.yaml b/worlds/tloz_oos/patching/asm/util.yaml new file mode 100644 index 000000000000..ac8f508321c4 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/util.yaml @@ -0,0 +1,138 @@ +# return z iff the current group and room match c and b. +00//compareRoom: | + ld a,(wActiveGroup) + cp c + ret nz + ld a,(wActiveRoom) + cp b + ret + +# returns the byte at e:hl into e. +00//readByte: | + ldh a,(hRomBank) + push af + ld a,e + ldh (hRomBank),a + ld ($2222),a + ld e,(hl) + pop af + ldh (hRomBank),a + ld ($2222),a + ret + +# searches for a value in a table starting at hl, with an entry matching +# keys b and subkey c, and values e bytes long. sets c if found. a key of +# ff ends the table. +00//searchDoubleKey: | + @loop: + ldi a,(hl) + cp $ff + ret z + cp b + jr nz,@next + ldi a,(hl) + cp c + jr nz,@done + scf + ret + @next: + inc hl + @done: + ld a,e + rst 10 + jr @loop + +# Look for an interaction of type treasure in the room, and return the address of the first one found +# in HL. Carry flag is set if found. +00//findTreasure: | + ld hl,$d041 + @loop: + ld a,(hl) + cp INTERACID_TREASURE + jr nz,@next + scf + ret + + @next: + ld a,h + cp $df + jr z,@endNotFound + inc h + jr @loop + + @endNotFound: + scf + ccf + ret + +# searches for a value in a table starting at hl, with an entry matching +# key "a" and values "e" bytes long. sets c if found. a key of ff ends the table. +00//searchKey: | + ld b,a + @loop: + ldi a,(hl) + cp $ff + ret z + cp b + jr nz,@next + scf + ret + @next: + ld a,e + rst 10 + jr @loop + +# (de) = array of map IDs ($FF terminated) +# (hl) = beginning of map flags array for map group +# returns nz if any of the maps has item flag set, z otherwise +00//scanItemGetFlagsForMaps: | + @loop: + ld a,(de) + cp $ff + jr z,@done + ld l,a + bit 5,(hl) + jr nz,@done + inc de + jr @loop + @done: + ret + +# b = treasure id +# c = treasure subid +00//spawnTreasureOnLink: | + call createTreasure + ret nz + push de + ld de,w1Link.yh + call objectCopyPosition_rawAddress + pop de + xor a + ret + +# call a function hl in bank 02, preserving af. e can't be used as a +# parameter to that function, but it can be returned. this function only +# exists because banks 08 and 09 are so tight on space. +00//callBank2: | + push af + ld e,$02 + call interBankCall + pop af + ret + +# Add a "remove item" script command attached to unused byte 0xdc +0b//scriptCmd_removeItem: | + pop hl + inc hl + ldi a,(hl) + call loseTreasure + ret +0b/00c1/: dw scriptCmd_removeItem + +# Puts the current amount of essences possessed into a +00//getEssenceCount: | + push bc + ld a,(wEssencesObtained) + call getNumSetBits + pop bc + ret diff --git a/worlds/tloz_oos/patching/asm/vars.yaml b/worlds/tloz_oos/patching/asm/vars.yaml new file mode 100644 index 000000000000..890f7421bf8b --- /dev/null +++ b/worlds/tloz_oos/patching/asm/vars.yaml @@ -0,0 +1,44 @@ +# locations of sparkles on treasure map +# 02/2663/jewelCoordsRound: db $b5 +# 02/2664/jewelCoordsPyramid: db $1d +# 02/2665/jewelCoordsSquare: db $c2 +# 02/2666/jewelCoordsXShaped: db $f4 + +# set initial season correctly in the init variables. +07/0188/initialSeason: db $2d,defaultSeason.EYEGLASS_LAKE + +# Edit @extraItemsToAddTable to set seed type given with the Seed Satchel +3f/053b/: db option.defaultSeedType +# Add a new entry to @extraItemsToAddTable to give the player seeds when they +# get the slingshot. This overwrites the beginning of @itemsToRemoveTable, +# but that table is useless in rando. +3f/0543/: | + db TREASURE_SLINGSHOT + db option.defaultSeedType + db $20 + + db $00 ; End of table +# Change the pointer of @itemsToRemoveTable to point to its end of table byte, +# virtually emptying the table (I told you it was useless!) +3f/04ce/: ld hl,$454d + +15/2233/oldManRupeeValues: | + db $10 ; Goron mountain + db $0d ; North Horon + db $0c ; D1 stump + db $10 ; Western Coast + db $0c ; Horon + db $0d ; Tarm Ruins + db $0b ; Woods of Winter + db $0c ; Ghastly stump +0a/187b/oldManGiveTake: | + dw $7472 ; Goron mountain + dw $7472 ; North Horon + dw $7472 ; D1 stump + dw $7472 ; Western Coast + dw $7472 ; Horon + dw $7488 ; Tarm Ruins + dw $7488 ; Woods of Winter + dw $7488 ; Ghastly stump + +07/23cb/foolsOreDamage: db option.foolsOreDamage diff --git a/worlds/tloz_oos/patching/asm/warp_to_start.yaml b/worlds/tloz_oos/patching/asm/warp_to_start.yaml new file mode 100644 index 000000000000..df4228f11453 --- /dev/null +++ b/worlds/tloz_oos/patching/asm/warp_to_start.yaml @@ -0,0 +1,75 @@ +02//checkTreeVisited: | + cp STARTING_TREE_MAP_INDEX + jp nz,$6560 ; _mapMenu_checkRoomVisited + or a + ret +02/1ec8/: call checkTreeVisited +02/25e1/: call checkTreeVisited + +# always treat starting seed tree as visited for warping purposes. +02//checkCursorVisited: | + ld a,(wMapMenuCursorIndex) + jp checkTreeVisited +02/209b/: call checkCursorVisited + +# warp to horon village tree if holding start when opening the map screen. +02//checkWarpToStart: | + ld a,option.warpToStart + or a + jr z,@done + + ld a,(wKeysPressed) + and BTN_B | BTN_A + cp BTN_B | BTN_A + jr nz,@done + + ld a,option.startingGroup + set 7,a + ld (wWarpDestGroup),a + ld a,option.startingRoom ; Starting room ID + ld (wWarpDestRoom),a + ld a,option.startingPos ; Position in starting room + ld (wWarpDestPos),a + ld a,$05 ; TRANSITION_DEST_FALL + ld (wWarpTransition),a + ld a,$03 + ld (wWarpTransition2),a + ld a,$ff + ld (wDisabledObjects),a + ld a,option.startingSeason + ld (wRoomStateModifier),a + + ; Setup respawn to prevent save-scumming + ld hl,wDeathRespawnBuffer + ld a,option.startingGroup + ldi (hl),a ; Room group + ld a,option.startingRoom + ldi (hl),a ; Room + ld a,option.startingSeason + ldi (hl),a ; Season + ld a,$02 + ldi (hl),a ; Direction + ld a,option.startingPosY + ldi (hl),a ; Y + ld a,option.startingPosX + ld (hl),a ; X + + ld a,SND_TELEPORT + call playSound + + ld a,$03 + call setMusicVolume + call clearStaticObjects + + ld a,$d0 + ld (wLinkObjectIndex),a + + ld a,$03 + ld (wMenuLoadState),a + pop af ; pop return addr from stack + ret + + @done: + jp $5029 ; @openMenu +# Replace the unique call to @openMenu by the above extension checking for warp to start +02/101f/: call checkWarpToStart diff --git a/worlds/tloz_oos/patching/z80asm/Assembler.py b/worlds/tloz_oos/patching/z80asm/Assembler.py new file mode 100644 index 000000000000..97523ba6c2ef --- /dev/null +++ b/worlds/tloz_oos/patching/z80asm/Assembler.py @@ -0,0 +1,300 @@ +from copy import copy +from typing import Dict +from .Util import * +from .Errors import * +from .MnemonicsTree import MNEMONICS +from ..Util import hex_str + + +class GameboyAddress: + def __init__(self, bank: int, offset: int): + self.bank = bank + self.offset = offset + + def address_in_rom(self): + return (self.bank * 0x4000) + self.offset + + def to_word(self): + mapped_offset = self.offset + if self.bank > 0: + mapped_offset += 0x4000 + return f"${hex_str(mapped_offset,2)}" + + +class Z80Block: + local_labels: Dict[str, GameboyAddress] + + def __init__(self, metalabel: str, contents: str): + split_metalabel = metalabel.split("/") + if len(split_metalabel) != 3: + raise Exception(f"Invalid metalabel '{metalabel}'") + if split_metalabel[1] == "": + split_metalabel[1] = "ffff" # <-- means that it needs to be injected in some code cave + + offset = int(split_metalabel[1], 16) + if offset >= 0x4000 and offset != 0xffff: + raise InvalidAddressError(offset) + self.addr = GameboyAddress(int(split_metalabel[0], 16), offset) + + self.label = split_metalabel[2] + + stripped_lines = [strip_line(line) for line in contents.split("\n")] + self.content_lines = [line for line in stripped_lines if line] + + self.local_labels = {} + self.byte_array = [] + self.precompiled_size = 0 + + def set_base_offset(self, new_offset): + old_offset = self.addr.offset + self.addr.offset = new_offset + + shifted_labels = {} + for name, addr in self.local_labels.items(): + shifted_offset = addr.offset - old_offset + new_offset + shifted_labels[name] = GameboyAddress(addr.bank, shifted_offset) + self.local_labels = shifted_labels + + def requires_injection(self): + return self.addr.offset == 0xffff + + +class Z80Assembler: + def __init__(self, end_of_banks: List[int], defines: Dict[str, str]): + self.defines = {} + for key, value in defines.items(): + self.define(key, value) + + self.end_of_banks = copy(end_of_banks) + + self.floating_chunks = {} + self.global_labels = {} + self.blocks = [] + + def define(self, key: str, replacement_string: str): + if key in self.defines: + raise Exception(f"Attempting to re-define a value for key '{key}'.") + self.defines[key] = replacement_string + + def define_byte(self, key: str, byte: int): + while byte < 0: + byte += 0x100 + while byte >= 0x100: + byte -= 0x100 + self.define(key, f"${hex_str(byte)}") + + def define_word(self, key: str, word: int): + while word < 0: + word += 0x10000 + while word >= 0x10000: + word -= 0x10000 + self.define(key, f"${hex_str(word, 2)}") + + def add_floating_chunk(self, name: str, byte_array: List[int]): + """ + Add a named byte array to the collection of "floating chunks", which can then be inserted anywhere + using the "/include" directive in assembly + """ + if name in self.floating_chunks: + raise f"Attempting to re-define a floating chunk with name '{name}'." + self.floating_chunks[name] = byte_array + + def add_global_label(self, name: str, addr: GameboyAddress): + if name in self.global_labels: + raise Exception(f"Attempting to re-define a global label with name '{name}'.") + self.global_labels[name] = addr + + def add_block(self, block: Z80Block): + # Perform a first "precompilation" pass to determine block size once compiled and local labels' offsets. + self._precompile_block(block) + + if block.requires_injection(): + injection_offset = self.end_of_banks[block.addr.bank] + # If block is meant to be loaded in the graphics memory, it needs to be aligned particularly + if block.label.startswith("dma_") and injection_offset % 0x10 != 0: + injection_offset += 0x10 - (injection_offset % 0x10) + + if injection_offset + block.precompiled_size > 0x4000: + raise Exception(f"Not enough space for block {block.label} in bank {hex_str(block.addr.bank)} " + f"({hex(injection_offset + block.precompiled_size)})") + block.set_base_offset(injection_offset) + self.end_of_banks[block.addr.bank] = injection_offset + block.precompiled_size + + if block.label: + self.add_global_label(block.label, block.addr) + self.blocks.append(block) + + def resolve_names(self, arg: str, current_addr: GameboyAddress, local_labels: Dict[str, GameboyAddress], opcode: str): + arg = arg.strip() + if arg.startswith("(") and arg.endswith(")"): + return f"({self.resolve_names(arg[1:-1], current_addr, local_labels, opcode)})" + + HANDLED_OPERATORS = ["+", "-", "*", "|"] + for operator in HANDLED_OPERATORS: + if operator in arg: + split = arg.split(operator) + arg_1 = self.resolve_names(split[0], current_addr, local_labels, opcode) + arg_2 = self.resolve_names(split[1], current_addr, local_labels, opcode) + return f"{arg_1}{operator}{arg_2}" + + output = arg + if arg in self.defines: + output = self.defines[arg] + else: + addr = None + if arg in local_labels: + addr = local_labels[arg] + elif arg in self.global_labels: + addr = self.global_labels[arg] + if addr: + if opcode == "jr" and current_addr.bank == addr.bank: + # If opcode is "jr", we need to use an 8-bit relative offset instead of a 16-bit absolute address + difference = addr.offset - (current_addr.offset + 2) + if difference > 0x7f or difference < (-1 * 0x7f): + raise Exception(f"Label {arg} is too far away, offset cannot be expressed as a single byte ({difference})") + if difference < 0: + difference = 0x100 + difference + output = f"${hex_str(difference)}" + else: + output = addr.to_word() + + return output + + def compile_all(self): + """ + Perform a full compilation of all previously added blocks. + """ + for block in self.blocks: + self._compile_block(block) + + def _precompile_block(self, block: Z80Block): + block.byte_array = [] + current_offset = 0 + for line in block.content_lines: + addr = GameboyAddress(block.addr.bank, block.addr.offset + current_offset) + current_offset += self._evaluate_line_size(line, addr, block) + block.precompiled_size = current_offset + + def _compile_block(self, block: Z80Block): + block.byte_array = [] + for line in block.content_lines: + addr = GameboyAddress(block.addr.bank, block.addr.offset + len(block.byte_array)) + block.byte_array.extend(self._compile_line_to_bytes(line, addr, block)) + + if block.precompiled_size != len(block.byte_array): + raise Exception(f"Block {block.label} size prediction was wrong: " + f"{block.precompiled_size} -> {len(block.byte_array)}") + + def _evaluate_line_size(self, line: str, current_addr: GameboyAddress, block: Z80Block): + opcode = line.split(" ")[0] + + # If it ends with ':', it's a local label and needs to be registered as such + if opcode.endswith(":"): + block.local_labels[opcode[:-1]] = current_addr + return 0 + + args = line[len(opcode)+1:].split(",") + if len(args) == 0: + args = [""] + + if opcode == "/include": + if args[0] not in self.floating_chunks: + raise UnknownFloatingChunkError(args[0]) + return len(self.floating_chunks[args[0]]) + if opcode == "db": + return len(args) + if opcode == "dw" or opcode == "dwbe": + return len(args) * 2 + + # ...then try matching a mnemonic + extra_size = 0 + mnemonic_tree = MNEMONICS[opcode] + for arg in args: + if not isinstance(mnemonic_tree, collections.abc.Mapping): + raise TooManyArgsError(line) + + if arg not in mnemonic_tree: + # Argument could not be found in mnemonic tree, this means it's either a literal or a + # yet-unknown label / define. In that case, assume the size to be the one for the literal + # type that can be used for this mnemonic (if it exists) + for size in [8, 16]: + generic_arg = f"${size}" + if arg.startswith("("): + generic_arg = f"({generic_arg})" + if generic_arg in mnemonic_tree: + arg = generic_arg + extra_size = int(size/8) + break + if extra_size == 0: + raise UnknownMnemonicError(arg, line) + + mnemonic_tree = mnemonic_tree[arg] + + if isinstance(mnemonic_tree, collections.abc.Mapping): + raise IncompleteMnemonicError(line) + if isinstance(mnemonic_tree, list): + # Multi-byte opcode (CB prefix case) + return 2 + extra_size + else: + # Single-byte opcode + return 1 + extra_size + + def _compile_line_to_bytes(self, line: str, current_addr: GameboyAddress, block: Z80Block): + split = line.split(" ") + opcode = split[0] + + # If it ends with ':', it's a local label and needs to be ignored (since it was already registered + # during precompilation) + if opcode.endswith(":"): + return [] + + args = [""] + if len(split) > 1: + args = ' '.join(split[1:]).split(",") + + # Perform includes before resolving names + if opcode == "/include": + if args[0] not in self.floating_chunks: + raise UnknownFloatingChunkError(args[0]) + return self.floating_chunks[args[0]] + + # Resolve defines & labels to actual values. The ones that could not be resolved are let as-is. + args = [self.resolve_names(arg, current_addr, block.local_labels, opcode) for arg in args] + + # First try matching a specific keyword + if opcode == "db": + # Declare byte + return [parse_hex_byte(arg) for arg in args] + if opcode == "dw": + # Declare word + return [b for arg in args for b in parse_hex_word(arg)] + if opcode == "dwbe": + # Declare word big endian (reversed) + return [b for arg in args for b in reversed(parse_hex_word(arg))] + + # ...then try matching a mnemonic + extra_bytes = [] + mnemonic_tree = MNEMONICS[opcode] + for arg in args: + if not isinstance(mnemonic_tree, collections.abc.Mapping): + raise TooManyArgsError(line) + + generic_arg, value_byte_array = parse_argument(arg, mnemonic_tree) + if generic_arg not in mnemonic_tree: + raise UnknownMnemonicError(generic_arg, line) + + mnemonic_tree = mnemonic_tree[generic_arg] + extra_bytes.extend(value_byte_array) + + if isinstance(mnemonic_tree, collections.abc.Mapping): + raise IncompleteMnemonicError(line) + if isinstance(mnemonic_tree, list): + # Multi-byte opcode (CB prefix case) + output = copy(mnemonic_tree) + else: + # Single-byte opcode + output = [mnemonic_tree] + + output.extend(extra_bytes) + + return output diff --git a/worlds/tloz_oos/patching/z80asm/Errors.py b/worlds/tloz_oos/patching/z80asm/Errors.py new file mode 100644 index 000000000000..c79f1cd9b89a --- /dev/null +++ b/worlds/tloz_oos/patching/z80asm/Errors.py @@ -0,0 +1,29 @@ + +class UnknownMnemonicError(Exception): + def __init__(self, argument, origin_line): + super().__init__(f"Could not find a valid mnemonic for `{argument}` in `{origin_line}`") + + +class IncompleteMnemonicError(Exception): + def __init__(self, origin_line): + super().__init__(f"Could not find a complete mnemonic for `{origin_line}`. Did you forget arguments?") + + +class TooManyArgsError(Exception): + def __init__(self, origin_line): + super().__init__(f"Too many arguments in line `{origin_line}`") + + +class UnknownFloatingChunkError(Exception): + def __init__(self, chunk_name): + super().__init__(f"Unknown floating chunk {chunk_name}") + + +class ArgumentOverflowError(Exception): + def __init__(self, value, expected_size): + super().__init__(f"Argument overflow: {hex(value)} cannot fit in {expected_size} byte(s)") + + +class InvalidAddressError(Exception): + def __init__(self, addr): + super().__init__(f"Invalid address: {hex(addr)}") diff --git a/worlds/tloz_oos/patching/z80asm/MnemonicsTree.py b/worlds/tloz_oos/patching/z80asm/MnemonicsTree.py new file mode 100644 index 000000000000..1fce6f1997a7 --- /dev/null +++ b/worlds/tloz_oos/patching/z80asm/MnemonicsTree.py @@ -0,0 +1,684 @@ +MNEMONICS = { + "nop": { + "": 0x00 + }, + "ld": { + "a": { + "a": 0x7f, + "b": 0x78, + "c": 0x79, + "d": 0x7a, + "e": 0x7b, + "h": 0x7c, + "l": 0x7d, + "(bc)": 0x0a, + "(de)": 0x1a, + "(hl)": 0x7e, + "$8": 0x3e, + "($16)": 0xfa, + "(ff00+c)": 0xf2, + }, + "b": { + "a": 0x47, + "b": 0x40, + "c": 0x41, + "d": 0x42, + "e": 0x43, + "h": 0x44, + "l": 0x45, + "(hl)": 0x46, + "$8": 0x06, + }, + "c": { + "a": 0x4f, + "b": 0x48, + "c": 0x49, + "d": 0x4a, + "e": 0x4b, + "h": 0x4c, + "l": 0x4d, + "(hl)": 0x4e, + "$8": 0x0e, + }, + "d": { + "a": 0x57, + "b": 0x50, + "c": 0x51, + "d": 0x52, + "e": 0x53, + "h": 0x54, + "l": 0x55, + "(hl)": 0x56, + "$8": 0x16, + }, + "e": { + "a": 0x5f, + "b": 0x58, + "c": 0x59, + "d": 0x5a, + "e": 0x5b, + "h": 0x5c, + "l": 0x5d, + "(hl)": 0x5e, + "$8": 0x1e, + }, + "h": { + "a": 0x67, + "b": 0x60, + "c": 0x61, + "d": 0x62, + "e": 0x63, + "h": 0x64, + "l": 0x65, + "(hl)": 0x66, + "$8": 0x26, + }, + "l": { + "a": 0x6f, + "b": 0x68, + "c": 0x69, + "d": 0x6a, + "e": 0x6b, + "h": 0x6c, + "l": 0x6d, + "(hl)": 0x6e, + "$8": 0x2e, + }, + "bc": { + "$16": 0x01, + }, + "de": { + "$16": 0x11 + }, + "hl": { + "$16": 0x21, + "sp+$8": 0xf8 + }, + "(bc)": { + "a": 0x02, + }, + "(de)": { + "a": 0x12 + }, + "(hl)": { + "a": 0x77, + "b": 0x70, + "c": 0x71, + "d": 0x72, + "e": 0x73, + "h": 0x74, + "l": 0x75, + "$8": 0x36 + }, + "($16)": { + "a": 0xea, + "sp": 0x08, + }, + "sp": { + "$16": 0x31, + "hl": 0xf9, + }, + "(ff00+c)": {"a": 0xe2}, + }, + "ldi": { + "(hl)": {"a": 0x22}, + "a": {"(hl)": 0x2a}, + }, + "ldd": { + "(hl)": { + "a": 0x32 + }, + "a": { + "(hl)": 0x3a + } + }, + "ldh": { + "($8)": {"a": 0xe0}, + "a": {"($8)": 0xf0}, + }, + "inc": { + "a": 0x3c, + "b": 0x04, + "c": 0x0c, + "d": 0x14, + "e": 0x1c, + "h": 0x24, + "l": 0x2c, + "bc": 0x03, + "de": 0x13, + "hl": 0x23, + "(hl)": 0x34, + "sp": 0x33, + }, + "dec": { + "a": 0x3d, + "b": 0x05, + "c": 0x0d, + "d": 0x15, + "e": 0x1d, + "h": 0x25, + "l": 0x2d, + "bc": 0x0b, + "de": 0x1b, + "hl": 0x2b, + "(hl)": 0x35, + "sp": 0x3b + }, + "add": { + "a": { + "a": 0x87, + "b": 0x80, + "c": 0x81, + "d": 0x82, + "e": 0x83, + "h": 0x84, + "l": 0x85, + "(hl)": 0x86, + "$8": 0xc6, + }, + "hl": { + "bc": 0x09, + "de": 0x19, + "hl": 0x29, + "sp": 0x39 + }, + "sp": {"$8": 0xe8} + }, + "adc": { + "a": { + "a": 0x8f, + "b": 0x88, + "c": 0x89, + "d": 0x8a, + "e": 0x8b, + "h": 0x8c, + "l": 0x8d, + "(hl)": 0x8e, + "$8": 0xce, + } + }, + "sub": { + "a": 0x97, + "b": 0x90, + "c": 0x91, + "d": 0x92, + "e": 0x93, + "h": 0x94, + "l": 0x95, + "(hl)": 0x96, + "$8": 0xd6, + }, + "sbc": { + "a": { + "a": 0x9f, + "b": 0x98, + "c": 0x99, + "d": 0x9a, + "e": 0x9b, + "h": 0x9c, + "l": 0x9d, + "(hl)": 0x9e, + "$8": 0xde, + } + }, + "and": { + "a": 0xa7, + "b": 0xa0, + "c": 0xa1, + "d": 0xa2, + "e": 0xa3, + "h": 0xa4, + "l": 0xa5, + "(hl)": 0xa6, + "$8": 0xe6, + }, + "xor": { + "a": 0xaf, + "b": 0xa8, + "c": 0xa9, + "d": 0xaa, + "e": 0xab, + "h": 0xac, + "l": 0xad, + "(hl)": 0xae, + "$8": 0xee + }, + "or": { + "a": 0xb7, + "b": 0xb0, + "c": 0xb1, + "d": 0xb2, + "e": 0xb3, + "h": 0xb4, + "l": 0xb5, + "(hl)": 0xb6, + "$8": 0xf6, + }, + "cp": { + "a": 0xbf, + "b": 0xb8, + "c": 0xb9, + "d": 0xba, + "e": 0xbb, + "h": 0xbc, + "l": 0xbd, + "(hl)": 0xbe, + "$8": 0xfe, + }, + "jr": { + "$8": 0x18, + "nz": {"$8": 0x20}, + "z": {"$8": 0x28}, + "nc": {"$8": 0x30}, + "c": {"$8": 0x38}, + }, + "jp": { + "$16": 0xc3, + "nz": {"$16": 0xc2}, + "z": {"$16": 0xca}, + "nc": {"$16": 0xd2}, + "c": {"$16": 0xda}, + "(hl)": 0xe9, + }, + "call": { + "$16": 0xcd, + "nz": {"$16": 0xc4}, + "z": {"$16": 0xcc}, + "nc": {"$16": 0xd4}, + "c": {"$16": 0xdc} + }, + "ret": { + "": 0xc9, + "z": 0xc8, + "nz": 0xc0, + "c": 0xd8, + "nc": 0xd0, + }, + "push": { + "af": 0xf5, + "bc": 0xc5, + "de": 0xd5, + "hl": 0xe5, + }, + "pop": { + "af": 0xf1, + "bc": 0xc1, + "de": 0xd1, + "hl": 0xe1, + }, + "rst": { + "00": 0xc7, + "08": 0xcf, + "10": 0xd7, + "18": 0xdf, + "20": 0xe7, + "28": 0xef, + "30": 0xf7, + "38": 0xff, + }, + "rlca": { + "": 0x07 + }, + "rrca": { + "": 0x0f + }, + "rla": { + "": 0x17 + }, + "rra": { + "": 0x1f + }, + "daa": { + "": 0x27 + }, + "cpl": { + "": 0x2f + }, + "scf": { + "": 0x37 + }, + "ccf": { + "": 0x3f + }, + "reti": { + "": 0xd9 + }, + "di": { + "": 0xf3 + }, + "ei": { + "": 0xfb + }, + "stop": {"0": 0x10}, + "halt": { + "": 0x76 + }, + + # CB-prefixed mnemonics + "rlc": { + "a": [0xcb, 0x07], + "b": [0xcb, 0x00], + "c": [0xcb, 0x01], + "d": [0xcb, 0x02], + "e": [0xcb, 0x03], + "h": [0xcb, 0x04], + "l": [0xcb, 0x05], + "(hl)": [0xcb, 0x06], + }, + "rrc": { + "a": [0xcb, 0x0f], + "b": [0xcb, 0x08], + "c": [0xcb, 0x09], + "d": [0xcb, 0x0a], + "e": [0xcb, 0x0b], + "h": [0xcb, 0x0c], + "l": [0xcb, 0x0d], + "(hl)": [0xcb, 0x0e], + }, + "rl": { + "a": [0xcb, 0x17], + "b": [0xcb, 0x10], + "c": [0xcb, 0x11], + "d": [0xcb, 0x12], + "e": [0xcb, 0x13], + "h": [0xcb, 0x14], + "l": [0xcb, 0x15], + "(hl)": [0xcb, 0x16], + }, + "rr": { + "a": [0xcb, 0x1f], + "b": [0xcb, 0x18], + "c": [0xcb, 0x19], + "d": [0xcb, 0x1a], + "e": [0xcb, 0x1b], + "h": [0xcb, 0x1c], + "l": [0xcb, 0x1d], + "(hl)": [0xcb, 0x1e], + }, + "sla": { + "a": [0xcb, 0x27], + "b": [0xcb, 0x20], + "c": [0xcb, 0x21], + "d": [0xcb, 0x22], + "e": [0xcb, 0x23], + "h": [0xcb, 0x24], + "l": [0xcb, 0x25], + "(hl)": [0xcb, 0x26], + }, + "sra": { + "a": [0xcb, 0x2f], + "b": [0xcb, 0x28], + "c": [0xcb, 0x29], + "d": [0xcb, 0x2a], + "e": [0xcb, 0x2b], + "h": [0xcb, 0x2c], + "l": [0xcb, 0x2d], + "(hl)": [0xcb, 0x2e], + }, + "swap": { + "a": [0xcb, 0x37], + "b": [0xcb, 0x30], + "c": [0xcb, 0x31], + "d": [0xcb, 0x32], + "e": [0xcb, 0x33], + "h": [0xcb, 0x34], + "l": [0xcb, 0x35], + "(hl)": [0xcb, 0x36], + }, + "srl": { + "a": [0xcb, 0x3f], + "b": [0xcb, 0x38], + "c": [0xcb, 0x39], + "d": [0xcb, 0x3a], + "e": [0xcb, 0x3b], + "h": [0xcb, 0x3c], + "l": [0xcb, 0x3d], + "(hl)": [0xcb, 0x3e], + }, + "bit": { + "0": { + "a": [0xcb, 0x47], + "b": [0xcb, 0x40], + "c": [0xcb, 0x41], + "d": [0xcb, 0x42], + "e": [0xcb, 0x43], + "h": [0xcb, 0x44], + "l": [0xcb, 0x45], + "(hl)": [0xcb, 0x46], + }, + "1": { + "a": [0xcb, 0x4f], + "b": [0xcb, 0x48], + "c": [0xcb, 0x49], + "d": [0xcb, 0x4a], + "e": [0xcb, 0x4b], + "h": [0xcb, 0x4c], + "l": [0xcb, 0x4d], + "(hl)": [0xcb, 0x4e], + }, + "2": { + "a": [0xcb, 0x57], + "b": [0xcb, 0x50], + "c": [0xcb, 0x51], + "d": [0xcb, 0x52], + "e": [0xcb, 0x53], + "h": [0xcb, 0x54], + "l": [0xcb, 0x55], + "(hl)": [0xcb, 0x56], + }, + "3": { + "a": [0xcb, 0x5f], + "b": [0xcb, 0x58], + "c": [0xcb, 0x59], + "d": [0xcb, 0x5a], + "e": [0xcb, 0x5b], + "h": [0xcb, 0x5c], + "l": [0xcb, 0x5d], + "(hl)": [0xcb, 0x5e], + }, + "4": { + "a": [0xcb, 0x67], + "b": [0xcb, 0x60], + "c": [0xcb, 0x61], + "d": [0xcb, 0x62], + "e": [0xcb, 0x63], + "h": [0xcb, 0x64], + "l": [0xcb, 0x65], + "(hl)": [0xcb, 0x66], + }, + "5": { + "a": [0xcb, 0x6f], + "b": [0xcb, 0x68], + "c": [0xcb, 0x69], + "d": [0xcb, 0x6a], + "e": [0xcb, 0x6b], + "h": [0xcb, 0x6c], + "l": [0xcb, 0x6d], + "(hl)": [0xcb, 0x6e], + }, + "6": { + "a": [0xcb, 0x77], + "b": [0xcb, 0x70], + "c": [0xcb, 0x71], + "d": [0xcb, 0x72], + "e": [0xcb, 0x73], + "h": [0xcb, 0x74], + "l": [0xcb, 0x75], + "(hl)": [0xcb, 0x76], + }, + "7": { + "a": [0xcb, 0x7f], + "b": [0xcb, 0x78], + "c": [0xcb, 0x79], + "d": [0xcb, 0x7a], + "e": [0xcb, 0x7b], + "h": [0xcb, 0x7c], + "l": [0xcb, 0x7d], + "(hl)": [0xcb, 0x7e], + }, + }, + "res": { + "0": { + "a": [0xcb, 0x87], + "b": [0xcb, 0x80], + "c": [0xcb, 0x81], + "d": [0xcb, 0x82], + "e": [0xcb, 0x83], + "h": [0xcb, 0x84], + "l": [0xcb, 0x85], + "(hl)": [0xcb, 0x86], + }, + "1": { + "a": [0xcb, 0x8f], + "b": [0xcb, 0x88], + "c": [0xcb, 0x89], + "d": [0xcb, 0x8a], + "e": [0xcb, 0x8b], + "h": [0xcb, 0x8c], + "l": [0xcb, 0x8d], + "(hl)": [0xcb, 0x8e], + }, + "2": { + "a": [0xcb, 0x97], + "b": [0xcb, 0x90], + "c": [0xcb, 0x91], + "d": [0xcb, 0x92], + "e": [0xcb, 0x93], + "h": [0xcb, 0x94], + "l": [0xcb, 0x95], + "(hl)": [0xcb, 0x96], + }, + "3": { + "a": [0xcb, 0x9f], + "b": [0xcb, 0x98], + "c": [0xcb, 0x99], + "d": [0xcb, 0x9a], + "e": [0xcb, 0x9b], + "h": [0xcb, 0x9c], + "l": [0xcb, 0x9d], + "(hl)": [0xcb, 0x9e], + }, + "4": { + "a": [0xcb, 0xa7], + "b": [0xcb, 0xa0], + "c": [0xcb, 0xa1], + "d": [0xcb, 0xa2], + "e": [0xcb, 0xa3], + "h": [0xcb, 0xa4], + "l": [0xcb, 0xa5], + "(hl)": [0xcb, 0xa6], + }, + "5": { + "a": [0xcb, 0xaf], + "b": [0xcb, 0xa8], + "c": [0xcb, 0xa9], + "d": [0xcb, 0xaa], + "e": [0xcb, 0xab], + "h": [0xcb, 0xac], + "l": [0xcb, 0xad], + "(hl)": [0xcb, 0xae], + }, + "6": { + "a": [0xcb, 0xb7], + "b": [0xcb, 0xb0], + "c": [0xcb, 0xb1], + "d": [0xcb, 0xb2], + "e": [0xcb, 0xb3], + "h": [0xcb, 0xb4], + "l": [0xcb, 0xb5], + "(hl)": [0xcb, 0xb6], + }, + "7": { + "a": [0xcb, 0xbf], + "b": [0xcb, 0xb8], + "c": [0xcb, 0xb9], + "d": [0xcb, 0xba], + "e": [0xcb, 0xbb], + "h": [0xcb, 0xbc], + "l": [0xcb, 0xbd], + "(hl)": [0xcb, 0xbe], + }, + }, + "set": { + "0": { + "a": [0xcb, 0xc7], + "b": [0xcb, 0xc0], + "c": [0xcb, 0xc1], + "d": [0xcb, 0xc2], + "e": [0xcb, 0xc3], + "h": [0xcb, 0xc4], + "l": [0xcb, 0xc5], + "(hl)": [0xcb, 0xc6], + }, + "1": { + "a": [0xcb, 0xcf], + "b": [0xcb, 0xc8], + "c": [0xcb, 0xc9], + "d": [0xcb, 0xca], + "e": [0xcb, 0xcb], + "h": [0xcb, 0xcc], + "l": [0xcb, 0xcd], + "(hl)": [0xcb, 0xce], + }, + "2": { + "a": [0xcb, 0xd7], + "b": [0xcb, 0xd0], + "c": [0xcb, 0xd1], + "d": [0xcb, 0xd2], + "e": [0xcb, 0xd3], + "h": [0xcb, 0xd4], + "l": [0xcb, 0xd5], + "(hl)": [0xcb, 0xd6], + }, + "3": { + "a": [0xcb, 0xdf], + "b": [0xcb, 0xd8], + "c": [0xcb, 0xd9], + "d": [0xcb, 0xda], + "e": [0xcb, 0xdb], + "h": [0xcb, 0xdc], + "l": [0xcb, 0xdd], + "(hl)": [0xcb, 0xde], + }, + "4": { + "a": [0xcb, 0xe7], + "b": [0xcb, 0xe0], + "c": [0xcb, 0xe1], + "d": [0xcb, 0xe2], + "e": [0xcb, 0xe3], + "h": [0xcb, 0xe4], + "l": [0xcb, 0xe5], + "(hl)": [0xcb, 0xe6], + }, + "5": { + "a": [0xcb, 0xef], + "b": [0xcb, 0xe8], + "c": [0xcb, 0xe9], + "d": [0xcb, 0xea], + "e": [0xcb, 0xeb], + "h": [0xcb, 0xec], + "l": [0xcb, 0xed], + "(hl)": [0xcb, 0xee], + }, + "6": { + "a": [0xcb, 0xf7], + "b": [0xcb, 0xf0], + "c": [0xcb, 0xf1], + "d": [0xcb, 0xf2], + "e": [0xcb, 0xf3], + "h": [0xcb, 0xf4], + "l": [0xcb, 0xf5], + "(hl)": [0xcb, 0xf6], + }, + "7": { + "a": [0xcb, 0xff], + "b": [0xcb, 0xf8], + "c": [0xcb, 0xf9], + "d": [0xcb, 0xfa], + "e": [0xcb, 0xfb], + "h": [0xcb, 0xfc], + "l": [0xcb, 0xfd], + "(hl)": [0xcb, 0xfe], + }, + }, +} diff --git a/worlds/tloz_oos/patching/z80asm/Util.py b/worlds/tloz_oos/patching/z80asm/Util.py new file mode 100644 index 000000000000..278c606025b4 --- /dev/null +++ b/worlds/tloz_oos/patching/z80asm/Util.py @@ -0,0 +1,107 @@ +import collections +import re +from typing import List + +from worlds.tloz_oos.patching.z80asm.Errors import ArgumentOverflowError + + +def strip_line(line): + """ + Strips indent and comment from line, if present. + """ + line = line.strip() + return re.sub(r' *[;#].*\n?', '', line) + + +def parse_hex_string_to_value(string: str): + """ + Parse an hexadecimal string into a numeric value, handling some operators + """ + string = string.replace("$", "") + if "+" in string: + split = string.split("+") + return int(split[0], 16) + int(split[1], 16) + elif "-" in string: + split = string.split("-") + return int(split[0], 16) - int(split[1], 16) + elif "*" in string: + split = string.split("*") + return int(split[0], 16) * int(split[1], 16) + elif "|" in string: + split = string.split("|") + return int(split[0], 16) | int(split[1], 16) + else: + return int(string, 16) + + +def value_to_byte_array(value: int, expected_size: int): + """ + Converts a value into a little endian byte array + (e.g. "0x4Fa7DEadBEef" => [0xef, 0xbe, 0xad, 0xde, 0xa7, 0x4f]) + """ + output = [] + while value > 0: + output.append(value & 0xFF) + value >>= 8 + if len(output) > expected_size: + raise ArgumentOverflowError(value, expected_size) + while len(output) < expected_size: + output.append(0x00) + + return output + + +def parse_hex_byte(string: str): + """ + Converts a byte literal hexadecimal string into a byte value + (e.g. "$4F" => 0x4f) + """ + value = parse_hex_string_to_value(string) + return value_to_byte_array(value, 1)[0] + + +def parse_hex_word(string: str): + """ + Converts a word literal hexadecimal string into a little endian byte array + (e.g. "$4Fa7" => [0xa7, 0x4f]) + """ + value = parse_hex_string_to_value(string) + return value_to_byte_array(value, 2) + + +def parse_argument(arg: str, mnemonic_subtree: collections.abc.Mapping) -> (str, List[int]): + """ + Parse an argument to extract a generic form and potential extra bytes + "$1a" => ("$8", [0x1a]) + "($c43f)" => ("($16)", [0x3f,0xc4]) + "bc" => ("bc", []) + "$04+$29" => ("$8", [0x2d]) + """ + arg = arg.strip() + enclosed_in_parentheses = arg.startswith("(") and arg.endswith(")") + if enclosed_in_parentheses: + arg = arg[1:-1] + + # If argument is a literal, determine the expected size of that literal using the + # mnemonic subtree that was passed as parameter + if arg.startswith("$"): + value = 0 + try: + value = parse_hex_string_to_value(arg) + except ValueError: + pass + + for size in [8, 16]: + generic_arg = f"${size}" + if enclosed_in_parentheses: + generic_arg = f"({generic_arg})" + if generic_arg in mnemonic_subtree: + return generic_arg, value_to_byte_array(value, int(size/8)) + + # If we reached that point, this means we need to keep the symbol as it is: it can be a register name, + # or an invalid name which will get rejected at a later point + if enclosed_in_parentheses: + return f"({arg})", [] + else: + return arg, [] + diff --git a/worlds/tloz_oos/patching/z80asm/__init__.py b/worlds/tloz_oos/patching/z80asm/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1