From 94650a02de62956eee8e7e41f61e8a41506b5842 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:08:29 -0500 Subject: [PATCH 01/74] Core: implement APProcedurePatch and APTokenMixin (#2536) * initial work on procedure patch * more flexibility load default procedure for version 5 patches add args for procedure add default extension for tokens and bsdiff allow specifying additional required extensions for generation * pushing current changes to go fix tloz bug * move tokens into a separate inheritable class * forgot the commit to remove token from ProcedurePatch * further cleaning from bad commit * start on docstrings * further work on docstrings and typing * improve docstrings * fix incorrect docstring * cleanup * clean defaults and docstring * define interface that has only the bare minimum required for `Patch.create_rom_file` * change to dictionary.get * remove unnecessary if statement * update to explicitly check for procedure, restore compatible version and manual override * Update Files.py * remove struct uses * ensure returning bytes, add token type checking * Apply suggestions from code review Co-authored-by: Doug Hoskisson * pep8 --------- Co-authored-by: beauxq Co-authored-by: Doug Hoskisson --- worlds/Files.py | 284 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 248 insertions(+), 36 deletions(-) diff --git a/worlds/Files.py b/worlds/Files.py index b2ecb9afb880..6fee582c872d 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -3,10 +3,11 @@ import abc import json import zipfile +from enum import IntEnum import os import threading -from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO +from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload import bsdiff4 @@ -38,6 +39,32 @@ def get_handler(file: str) -> Optional[AutoPatchRegister]: return None +class AutoPatchExtensionRegister(abc.ABCMeta): + extension_types: ClassVar[Dict[str, AutoPatchExtensionRegister]] = {} + required_extensions: List[str] = [] + + def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchExtensionRegister: + # construct class + new_class = super().__new__(mcs, name, bases, dct) + if "game" in dct: + AutoPatchExtensionRegister.extension_types[dct["game"]] = new_class + return new_class + + @staticmethod + def get_handler(game: str) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]: + handler = AutoPatchExtensionRegister.extension_types.get(game, APPatchExtension) + if handler.required_extensions: + handlers = [handler] + for required in handler.required_extensions: + ext = AutoPatchExtensionRegister.extension_types.get(required) + if not ext: + raise NotImplementedError(f"No handler for {required}.") + handlers.append(ext) + return handlers + else: + return handler + + container_version: int = 6 @@ -157,27 +184,14 @@ def patch(self, target: str) -> None: """ create the output file with the file name `target` """ -class APDeltaPatch(APAutoPatchInterface): - """An implementation of `APAutoPatchInterface` that additionally - has delta.bsdiff4 containing a delta patch to get the desired file.""" - +class APProcedurePatch(APAutoPatchInterface): + """ + An APPatch that defines a procedure to produce the desired file. + """ hash: Optional[str] # base checksum of source file - patch_file_ending: str = "" - delta: Optional[bytes] = None source_data: bytes - procedure = None # delete this line when APPP is added - - def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: - self.patched_path = patched_path - super(APDeltaPatch, self).__init__(*args, **kwargs) - - def get_manifest(self) -> Dict[str, Any]: - manifest = super(APDeltaPatch, self).get_manifest() - manifest["base_checksum"] = self.hash - manifest["result_file_ending"] = self.result_file_ending - manifest["patch_file_ending"] = self.patch_file_ending - manifest["compatible_version"] = 5 # delete this line when APPP is added - return manifest + patch_file_ending: str = "" + files: Dict[str, bytes] = {} @classmethod def get_source_data(cls) -> bytes: @@ -190,21 +204,219 @@ def get_source_data_with_cache(cls) -> bytes: cls.source_data = cls.get_source_data() return cls.source_data + def __init__(self, *args: Any, **kwargs: Any): + super(APProcedurePatch, self).__init__(*args, **kwargs) + + def get_manifest(self) -> Dict[str, Any]: + manifest = super(APProcedurePatch, self).get_manifest() + manifest["base_checksum"] = self.hash + manifest["result_file_ending"] = self.result_file_ending + manifest["patch_file_ending"] = self.patch_file_ending + manifest["procedure"] = self.procedure + if self.procedure == APDeltaPatch.procedure: + manifest["compatible_version"] = 5 + return manifest + + def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + super(APProcedurePatch, self).read_contents(opened_zipfile) + with opened_zipfile.open("archipelago.json", "r") as f: + manifest = json.load(f) + if "procedure" not in manifest: + # support patching files made before moving to procedures + self.procedure = [("apply_bsdiff4", ["delta.bsdiff4"])] + else: + self.procedure = manifest["procedure"] + for file in opened_zipfile.namelist(): + if file not in ["archipelago.json"]: + self.files[file] = opened_zipfile.read(file) + + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: + super(APProcedurePatch, self).write_contents(opened_zipfile) + for file in self.files: + opened_zipfile.writestr(file, self.files[file], + compress_type=zipfile.ZIP_STORED if file.endswith(".bsdiff4") else None) + + def get_file(self, file: str) -> bytes: + """ Retrieves a file from the patch container.""" + if file not in self.files: + self.read() + return self.files[file] + + def write_file(self, file_name: str, file: bytes) -> None: + """ Writes a file to the patch container, to be retrieved upon patching. """ + self.files[file_name] = file + + def patch(self, target: str) -> None: + self.read() + base_data = self.get_source_data_with_cache() + patch_extender = AutoPatchExtensionRegister.get_handler(self.game) + assert not isinstance(self.procedure, str), f"{type(self)} must define procedures" + for step, args in self.procedure: + if isinstance(patch_extender, list): + extension = next((item for item in [getattr(extender, step, None) for extender in patch_extender] + if item is not None), None) + else: + extension = getattr(patch_extender, step, None) + if extension is not None: + base_data = extension(self, base_data, *args) + else: + raise NotImplementedError(f"Unknown procedure {step} for {self.game}.") + with open(target, 'wb') as f: + f.write(base_data) + + +class APDeltaPatch(APProcedurePatch): + """An APProcedurePatch that additionally has delta.bsdiff4 + containing a delta patch to get the desired file, often a rom.""" + + procedure = [ + ("apply_bsdiff4", ["delta.bsdiff4"]) + ] + + def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: + super(APDeltaPatch, self).__init__(*args, **kwargs) + self.patched_path = patched_path + def write_contents(self, opened_zipfile: zipfile.ZipFile): + self.write_file("delta.bsdiff4", + bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read())) super(APDeltaPatch, self).write_contents(opened_zipfile) - # write Delta - opened_zipfile.writestr("delta.bsdiff4", - bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()), - compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression - - def read_contents(self, opened_zipfile: zipfile.ZipFile): - super(APDeltaPatch, self).read_contents(opened_zipfile) - self.delta = opened_zipfile.read("delta.bsdiff4") - - def patch(self, target: str): - """Base + Delta -> Patched""" - if not self.delta: - self.read() - result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta) - with open(target, "wb") as f: - f.write(result) + + +class APTokenTypes(IntEnum): + WRITE = 0 + COPY = 1 + RLE = 2 + AND_8 = 3 + OR_8 = 4 + XOR_8 = 5 + + +class APTokenMixin: + """ + A class that defines functions for generating a token binary, for use in patches. + """ + tokens: List[ + Tuple[APTokenTypes, int, Union[ + bytes, # WRITE + Tuple[int, int], # COPY, RLE + int # AND_8, OR_8, XOR_8 + ]]] = [] + + def get_token_binary(self) -> bytes: + """ + Returns the token binary created from stored tokens. + :return: A bytes object representing the token data. + """ + data = bytearray() + data.extend(len(self.tokens).to_bytes(4, "little")) + for token_type, offset, args in self.tokens: + data.append(token_type) + data.extend(offset.to_bytes(4, "little")) + if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]: + assert isinstance(args, int), f"Arguments to AND/OR/XOR must be of type int, not {type(args)}" + data.extend(int.to_bytes(1, 4, "little")) + data.append(args) + elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]: + assert isinstance(args, tuple), f"Arguments to COPY/RLE must be of type tuple, not {type(args)}" + data.extend(int.to_bytes(4, 4, "little")) + data.extend(args[0].to_bytes(4, "little")) + data.extend(args[1].to_bytes(4, "little")) + elif token_type == APTokenTypes.WRITE: + assert isinstance(args, bytes), f"Arguments to WRITE must be of type bytes, not {type(args)}" + data.extend(len(args).to_bytes(4, "little")) + data.extend(args) + else: + raise ValueError(f"Unknown token type {token_type}") + return bytes(data) + + @overload + def write_token(self, + token_type: Literal[APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8], + offset: int, + data: int) -> None: + ... + + @overload + def write_token(self, + token_type: Literal[APTokenTypes.COPY, APTokenTypes.RLE], + offset: int, + data: Tuple[int, int]) -> None: + ... + + @overload + def write_token(self, + token_type: Literal[APTokenTypes.WRITE], + offset: int, + data: bytes) -> None: + ... + + def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]): + """ + Stores a token to be used by patching. + """ + self.tokens.append((token_type, offset, data)) + + +class APPatchExtension(metaclass=AutoPatchExtensionRegister): + """Class that defines patch extension functions for a given game. + Patch extension functions must have the following two arguments in the following order: + + caller: APProcedurePatch (used to retrieve files from the patch container) + + rom: bytes (the data to patch) + + Further arguments are passed in from the procedure as defined. + + Patch extension functions must return the changed bytes. + """ + game: str + required_extensions: List[str] = [] + + @staticmethod + def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str): + """Applies the given bsdiff4 from the patch onto the current file.""" + return bsdiff4.patch(rom, caller.get_file(patch)) + + @staticmethod + def apply_tokens(caller: APProcedurePatch, rom: bytes, token_file: str) -> bytes: + """Applies the given token file from the patch onto the current file.""" + token_data = caller.get_file(token_file) + rom_data = bytearray(rom) + token_count = int.from_bytes(token_data[0:4], "little") + bpr = 4 + for _ in range(token_count): + token_type = token_data[bpr:bpr + 1][0] + offset = int.from_bytes(token_data[bpr + 1:bpr + 5], "little") + size = int.from_bytes(token_data[bpr + 5:bpr + 9], "little") + data = token_data[bpr + 9:bpr + 9 + size] + if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]: + arg = data[0] + if token_type == APTokenTypes.AND_8: + rom_data[offset] = rom_data[offset] & arg + elif token_type == APTokenTypes.OR_8: + rom_data[offset] = rom_data[offset] | arg + else: + rom_data[offset] = rom_data[offset] ^ arg + elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]: + length = int.from_bytes(data[:4], "little") + value = int.from_bytes(data[4:], "little") + if token_type == APTokenTypes.COPY: + rom_data[offset: offset + length] = rom_data[value: value + length] + else: + rom_data[offset: offset + length] = bytes([value] * length) + else: + rom_data[offset:offset + len(data)] = data + bpr += 9 + size + return bytes(rom_data) + + @staticmethod + def calc_snes_crc(caller: APProcedurePatch, rom: bytes): + """Calculates and applies a valid CRC for the SNES rom header.""" + rom_data = bytearray(rom) + if len(rom) < 0x8000: + raise Exception("Tried to calculate SNES CRC on file too small to be a SNES ROM.") + crc = (sum(rom_data[:0x7FDC] + rom_data[0x7FE0:]) + 0x01FE) & 0xFFFF + inv = crc ^ 0xFFFF + rom_data[0x7FDC:0x7FE0] = [inv & 0xFF, (inv >> 8) & 0xFF, crc & 0xFF, (crc >> 8) & 0xFF] + return bytes(rom_data) From d0a9d0e2d1df641668f4f806b45f9577e69229f6 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Wed, 20 Mar 2024 06:43:13 -0600 Subject: [PATCH 02/74] Pokemon Emerald: Bump required client version (#2963) --- worlds/pokemon_emerald/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index c17fd1bc197c..384bec9f4501 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -87,7 +87,7 @@ class PokemonEmeraldWorld(World): location_name_groups = LOCATION_GROUPS data_version = 2 - required_client_version = (0, 4, 3) + required_client_version = (0, 4, 5) badge_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] hm_shuffle_info: Optional[List[Tuple[PokemonEmeraldLocation, PokemonEmeraldItem]]] From 6f64bb98693556ac2635791381cc9651c365b324 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Wed, 20 Mar 2024 08:46:31 -0400 Subject: [PATCH 03/74] Noita: Remove newline from option description so it doesn't look bad on webhost (#2969) --- worlds/noita/options.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/noita/options.py b/worlds/noita/options.py index 7d987571a589..2c99e9dd2f38 100644 --- a/worlds/noita/options.py +++ b/worlds/noita/options.py @@ -78,8 +78,7 @@ class VictoryCondition(Choice): class ExtraOrbs(Range): - """Add extra orbs to your item pool, to prevent you from needing to wait as long - for the last orb you need for your victory condition. + """Add extra orbs to your item pool, to prevent you from needing to wait as long for the last orb you need for your victory condition. Extra orbs received past your victory condition's amount will be received as hearts instead. Can be turned on for the Greed Ending goal, but will only really make it harder.""" display_name = "Extra Orbs" From 8f7b63a787a0ef05625ae2fad1768251aced0c87 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 20 Mar 2024 05:56:04 -0700 Subject: [PATCH 04/74] SMW: Blocksanity logic fixes (#2988) --- worlds/smw/Regions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/smw/Regions.py b/worlds/smw/Regions.py index 2f8a128a5660..249604987401 100644 --- a/worlds/smw/Regions.py +++ b/worlds/smw/Regions.py @@ -975,7 +975,7 @@ def create_regions(world: World, active_locations): add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_yellow_block_2, lambda state: state.has(ItemName.yellow_switch_palace, player)) add_location_to_region(multiworld, player, active_locations, LocationName.donut_plains_2_region, LocationName.donut_plains_2_vine_block_1, - lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.yoshi_activate, player)))) + lambda state:( ((state.has(ItemName.mario_carry, player) and state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.mario_spin_jump, player))) or (state.has(ItemName.yoshi_activate, player)))) add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_coin_block_1, lambda state: state.has(ItemName.mario_swim, player)) add_location_to_region(multiworld, player, active_locations, LocationName.donut_secret_1_region, LocationName.donut_secret_1_coin_block_2, @@ -1118,7 +1118,7 @@ def create_regions(world: World, active_locations): add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_yellow_block_2, lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.yellow_switch_palace, player))) add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_castle_region, LocationName.chocolate_castle_green_block_1, - lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.yellow_switch_palace, player))) + lambda state: (state.has(ItemName.progressive_powerup, player, 1) and state.has(ItemName.green_switch_palace, player))) add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_powerup_block_1) add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_powerup_block_2) add_location_to_region(multiworld, player, active_locations, LocationName.chocolate_fortress_region, LocationName.chocolate_fortress_coin_block_1) @@ -1468,7 +1468,7 @@ def create_regions(world: World, active_locations): add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_9) add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_4_region, LocationName.forest_of_illusion_4_coin_block_10) add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_green_block_1, - lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.mario_carry, player))) + lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.mario_swim, player))) add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_powerup_block_1, lambda state: state.has(ItemName.mario_swim, player)) add_location_to_region(multiworld, player, active_locations, LocationName.forest_of_illusion_2_region, LocationName.forest_of_illusion_2_invis_coin_block_1, @@ -1762,7 +1762,7 @@ def create_regions(world: World, active_locations): add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_green_block_7, lambda state: (state.has(ItemName.green_switch_palace, player) and state.has(ItemName.yoshi_activate, player) and state.has(ItemName.mario_carry, player))) add_location_to_region(multiworld, player, active_locations, LocationName.star_road_4_region, LocationName.star_road_4_key_block_1, - lambda state:( ((state.has(ItemName.mario_climb, player) and state.has(ItemName.mario_carry, player))) or ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_climb, player))))) + lambda state:( ((state.has(ItemName.mario_carry, player) and state.has(ItemName.yoshi_activate, player))) or ((state.has(ItemName.green_switch_palace, player) and state.has(ItemName.red_switch_palace, player) and state.has(ItemName.mario_carry, player))))) add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_directional_coin_block_1) add_location_to_region(multiworld, player, active_locations, LocationName.star_road_5_region, LocationName.star_road_5_life_block_1, lambda state: state.has(ItemName.p_switch, player)) From fcaaa197a19a3be03965c504ca78dd2c21ce1f84 Mon Sep 17 00:00:00 2001 From: TheLX5 Date: Wed, 20 Mar 2024 05:56:19 -0700 Subject: [PATCH 05/74] SMW: Fixes for Bowser being defeatable on Egg Hunt and CI2 DC room access (#2981) --- worlds/smw/Client.py | 13 ++++++++----- worlds/smw/Rom.py | 37 +++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/worlds/smw/Client.py b/worlds/smw/Client.py index eb9b4ec3d3a9..33a74b3dc80f 100644 --- a/worlds/smw/Client.py +++ b/worlds/smw/Client.py @@ -65,11 +65,12 @@ SMW_BLOCKSANITY_BLOCK_COUNT = 582 -SMW_GOAL_LEVELS = [0x28, 0x31, 0x32] -SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D] -SMW_BAD_TEXT_BOX_LEVELS = [0x00, 0x26, 0x02, 0x4B] -SMW_BOSS_STATES = [0x80, 0xC0, 0xC1] -SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32] +SMW_GOAL_LEVELS = [0x28, 0x31, 0x32] +SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D] +SMW_BAD_TEXT_BOX_LEVELS = [0x00, 0x26, 0x02, 0x4B] +SMW_BOSS_STATES = [0x80, 0xC0, 0xC1] +SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32] +SMW_UNCOLLECTABLE_DRAGON_COINS = [0x24] class SMWSNIClient(SNIClient): @@ -604,6 +605,8 @@ async def game_watcher(self, ctx): if level_data[1] == 2: # Dragon Coins Check + if level_data[0] in SMW_UNCOLLECTABLE_DRAGON_COINS: + continue progress_byte = (level_data[0] // 8) progress_bit = 7 - (level_data[0] % 8) diff --git a/worlds/smw/Rom.py b/worlds/smw/Rom.py index 66226d503685..36078d4622b9 100644 --- a/worlds/smw/Rom.py +++ b/worlds/smw/Rom.py @@ -587,18 +587,17 @@ def handle_yoshi_box(rom): def handle_bowser_damage(rom): - rom.write_bytes(0x1A509, bytearray([0x20, 0x50, 0xBC])) # JSR $03BC50 + rom.write_bytes(0x1A509, bytearray([0x5C, 0x50, 0xBC, 0x03])) # JML $03BC50 BOWSER_BALLS_SUB_ADDR = 0x01BC50 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x00, bytearray([0x08])) # PHP - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x01, bytearray([0xAD, 0x48, 0x0F])) # LDA $F48 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x04, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # CMP $03BFA1 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x08, bytearray([0x90, 0x06])) # BCC +0x06 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0A, bytearray([0x28])) # PLP - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0B, bytearray([0xEE, 0xB8, 0x14])) # INC $14B8 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0E, bytearray([0x80, 0x01])) # BRA +0x01 - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x10, bytearray([0x28])) # PLP - rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x11, bytearray([0x60])) # RTS + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0000, bytearray([0xAF, 0xA0, 0xBF, 0x03])) # bowser_infinite_balls: lda.l goal_setting + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0004, bytearray([0xD0, 0x0C])) # bne .nope + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0006, bytearray([0xAD, 0x48, 0x0F])) # lda $0F48 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0009, bytearray([0xCF, 0xA1, 0xBF, 0x03])) # cmp.l required_bosses_setting + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x000D, bytearray([0x90, 0x03])) # bcc .nope + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x000F, bytearray([0xEE, 0xB8, 0x14])) # inc $14B8 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0012, bytearray([0xAD, 0xB8, 0x14])) # .nope lda $14B8 + rom.write_bytes(BOWSER_BALLS_SUB_ADDR + 0x0015, bytearray([0x5C, 0x0F, 0xA5, 0x03])) # jml $03A50F return @@ -2729,6 +2728,22 @@ def handle_uncompressed_player_gfx(rom): ]) rom.write_bytes(0x87F80, vram_targets) + +def handle_chocolate_island_2(rom): + FIX_CHOCOISLAND2_ADDR = 0x87200 + rom.write_bytes(0x2DB3E, bytearray([0x5C, 0x00, 0xF2, 0x10])) # jml fix_choco_island_2 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0000, bytearray([0xAD, 0x33, 0x1F])) # fix_choco_island_2 lda $1F2F+$04 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0003, bytearray([0x29, 0x08])) # and #$08 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0005, bytearray([0xD0, 0x0D])) # bne .dc_room + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0007, bytearray([0xAD, 0x22, 0x14])) # lda $1422 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000A, bytearray([0xC9, 0x04])) # cmp #$04 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000C, bytearray([0xF0, 0x06])) # beq .dc_room + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x000E, bytearray([0xA2, 0x02])) # .rex_room ldx #$02 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0010, bytearray([0x5C, 0x49, 0xDB, 0x05])) # jml $05DB49 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0014, bytearray([0xA2, 0x00])) # .dc_room ldx #$00 + rom.write_bytes(FIX_CHOCOISLAND2_ADDR + 0x0016, bytearray([0x5C, 0x49, 0xDB, 0x05])) # jml $05DB49 + + def decompress_gfx(compressed_graphics): # This code decompresses graphics in LC_LZ2 format in order to be able to swap player and yoshi's graphics with ease. decompressed_gfx = bytearray([]) @@ -3050,6 +3065,8 @@ def patch_rom(world: World, rom, player, active_level_dict): rom.write_bytes(0x09C13, bytearray([0x7E, 0x7E, 0x7F, 0x7F])) rom.write_byte(0x3F425, 0x32) + handle_chocolate_island_2(rom) + handle_ability_code(rom) handle_yoshi_box(rom) From 183ca35bbaf6c805fdb53396d21d0cba34f9cc5e Mon Sep 17 00:00:00 2001 From: qwint Date: Wed, 20 Mar 2024 08:39:37 -0500 Subject: [PATCH 06/74] CommonClient: Port Casting Bug (#2975) --- CommonClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CommonClient.py b/CommonClient.py index b6f8e43b181b..085a48a4b74b 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -643,13 +643,13 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) ctx.username = server_url.username if server_url.password: ctx.password = server_url.password - port = server_url.port or 38281 def reconnect_hint() -> str: return ", type /connect to reconnect" if ctx.server_address else "" logger.info(f'Connecting to Archipelago server at {address}') try: + port = server_url.port or 38281 # raises ValueError if invalid socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None, ssl=get_ssl_context() if address.startswith("wss://") else None) if ctx.ui is not None: From e9620bea777ff1008a09c24a70bf523c94f22c29 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:56:00 -0400 Subject: [PATCH 07/74] SM64: Goal Logic and Hint Bugfixes (#2886) --- worlds/sm64ex/Regions.py | 57 ++++++++++++++++++++++++++------------- worlds/sm64ex/Rules.py | 15 ++++++----- worlds/sm64ex/__init__.py | 24 ++++++++++++----- 3 files changed, 64 insertions(+), 32 deletions(-) diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index 8c2d32e401bf..a493281ec3f6 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -36,6 +36,11 @@ class SM64Levels(int, Enum): BOWSER_IN_THE_FIRE_SEA = 191 WING_MARIO_OVER_THE_RAINBOW = 311 + +class SM64Region(Region): + subregions: typing.List[Region] = [] + + # sm64paintings is a dict of entrances, format LEVEL | AREA sm64_level_to_paintings: typing.Dict[SM64Levels, str] = { SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield", @@ -81,14 +86,16 @@ def create_regions(world: MultiWorld, player: int): regBoB = create_region("Bob-omb Battlefield", player, world) create_locs(regBoB, "BoB: Big Bob-Omb on the Summit", "BoB: Footrace with Koopa The Quick", "BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy") - create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins") + bob_island = create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins") + regBoB.subregions = [bob_island] if (world.EnableCoinStars[player].value): create_locs(regBoB, "BoB: 100 Coins") regWhomp = create_region("Whomp's Fortress", player, world) create_locs(regWhomp, "WF: Chip Off Whomp's Block", "WF: Shoot into the Wild Blue", "WF: Red Coins on the Floating Isle", "WF: Fall onto the Caged Island", "WF: Blast Away the Wall") - create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy") + wf_tower = create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy") + regWhomp.subregions = [wf_tower] if (world.EnableCoinStars[player].value): create_locs(regWhomp, "WF: 100 Coins") @@ -96,6 +103,7 @@ def create_regions(world: MultiWorld, player: int): create_locs(regJRB, "JRB: Plunder in the Sunken Ship", "JRB: Can the Eel Come Out to Play?", "JRB: Treasure of the Ocean Cave", "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy") jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat") + regJRB.subregions = [jrb_upper] if (world.EnableCoinStars[player].value): create_locs(jrb_upper, "JRB: 100 Coins") @@ -108,7 +116,8 @@ def create_regions(world: MultiWorld, player: int): create_locs(regBBH, "BBH: Go on a Ghost Hunt", "BBH: Ride Big Boo's Merry-Go-Round", "BBH: Secret of the Haunted Books", "BBH: Seek the 8 Red Coins") bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room") - create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion") + bbh_roof = create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion") + regBBH.subregions = [bbh_third_floor, bbh_roof] if (world.EnableCoinStars[player].value): create_locs(regBBH, "BBH: 100 Coins") @@ -130,22 +139,26 @@ def create_regions(world: MultiWorld, player: int): create_locs(regHMC, "HMC: Swimming Beast in the Cavern", "HMC: Metal-Head Mario Can Move!", "HMC: Watch for Rolling Rocks", "HMC: Navigating the Toxic Maze","HMC: 1Up Block Past Rolling Rocks") hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins") - create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit") + hmc_pit_islands = create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit") + regHMC.subregions = [hmc_red_coin_area, hmc_pit_islands] if (world.EnableCoinStars[player].value): create_locs(hmc_red_coin_area, "HMC: 100 Coins") regLLL = create_region("Lethal Lava Land", player, world) create_locs(regLLL, "LLL: Boil the Big Bully", "LLL: Bully the Bullies", "LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling") - create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano") + lll_upper_volcano = create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano") + regLLL.subregions = [lll_upper_volcano] if (world.EnableCoinStars[player].value): create_locs(regLLL, "LLL: 100 Coins") regSSL = create_region("Shifting Sand Land", player, world) - create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid", "SSL: Inside the Ancient Pyramid", + create_locs(regSSL, "SSL: In the Talons of the Big Bird", "SSL: Shining Atop the Pyramid", "SSL: Free Flying for 8 Red Coins", "SSL: Bob-omb Buddy", "SSL: 1Up Block Outside Pyramid", "SSL: 1Up Block Pyramid Left Path", "SSL: 1Up Block Pyramid Back") - create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle") + ssl_upper_pyramid = create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Inside the Ancient Pyramid", + "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle") + regSSL.subregions = [ssl_upper_pyramid] if (world.EnableCoinStars[player].value): create_locs(regSSL, "SSL: 100 Coins") @@ -153,6 +166,7 @@ def create_regions(world: MultiWorld, player: int): create_locs(regDDD, "DDD: Board Bowser's Sub", "DDD: Chests in the Current", "DDD: Through the Jet Stream", "DDD: The Manta Ray's Reward", "DDD: Collect the Caps...") ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins") + regDDD.subregions = [ddd_moving_poles] if (world.EnableCoinStars[player].value): create_locs(ddd_moving_poles, "DDD: 100 Coins") @@ -163,7 +177,8 @@ def create_regions(world: MultiWorld, player: int): create_default_locs(regVCutM, locVCutM_table) regBitFS = create_region("Bowser in the Fire Sea", player, world) - create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys()) + bitfs_upper = create_subregion(regBitFS, "BitFS: Upper", *locBitFS_table.keys()) + regBitFS.subregions = [bitfs_upper] create_region("Second Floor", player, world) @@ -176,7 +191,8 @@ def create_regions(world: MultiWorld, player: int): create_locs(regWDW, "WDW: Express Elevator--Hurry Up!") wdw_top = create_subregion(regWDW, "WDW: Top", "WDW: Shocking Arrow Lifts!", "WDW: Top o' the Town", "WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy") - create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown") + wdw_downtown = create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown") + regWDW.subregions = [wdw_top, wdw_downtown] if (world.EnableCoinStars[player].value): create_locs(wdw_top, "WDW: 100 Coins") @@ -185,17 +201,19 @@ def create_regions(world: MultiWorld, player: int): "TTM: Bob-omb Buddy", "TTM: 1Up Block on Red Mushroom") ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage", "TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge") + regTTM.subregions = [ttm_middle, ttm_top] if (world.EnableCoinStars[player].value): create_locs(ttm_top, "TTM: 100 Coins") create_region("Tiny-Huge Island (Huge)", player, world) create_region("Tiny-Huge Island (Tiny)", player, world) regTHI = create_region("Tiny-Huge Island", player, world) - create_locs(regTHI, "THI: The Tip Top of the Huge Island", "THI: 1Up Block THI Small near Start") - thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick", + create_locs(regTHI, "THI: 1Up Block THI Small near Start") + thi_pipes = create_subregion(regTHI, "THI: Pipes", "THI: The Tip Top of the Huge Island", "THI: Pluck the Piranha Flower", "THI: Rematch with Koopa the Quick", "THI: Five Itty Bitty Secrets", "THI: Wiggler's Red Coins", "THI: Bob-omb Buddy", "THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area") thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm") + regTHI.subregions = [thi_pipes, thi_large_top] if (world.EnableCoinStars[player].value): create_locs(thi_large_top, "THI: 100 Coins") @@ -206,6 +224,7 @@ def create_regions(world: MultiWorld, player: int): ttc_lower = create_subregion(regTTC, "TTC: Lower", "TTC: Roll into the Cage", "TTC: Get a Hand", "TTC: 1Up Block Midway Up") ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums") ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top") + regTTC.subregions = [ttc_lower, ttc_upper, ttc_top] if (world.EnableCoinStars[player].value): create_locs(ttc_top, "TTC: 100 Coins") @@ -213,8 +232,9 @@ def create_regions(world: MultiWorld, player: int): create_locs(regRR, "RR: Swingin' in the Breeze", "RR: Tricky Triangles!", "RR: 1Up Block Top of Red Coin Maze", "RR: 1Up Block Under Fly Guy", "RR: Bob-omb Buddy") rr_maze = create_subregion(regRR, "RR: Maze", "RR: Coins Amassed in a Maze") - create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow") - create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky") + rr_cruiser = create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow") + rr_house = create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky") + regRR.subregions = [rr_maze, rr_cruiser, rr_house] if (world.EnableCoinStars[player].value): create_locs(rr_maze, "RR: 100 Coins") @@ -223,7 +243,8 @@ def create_regions(world: MultiWorld, player: int): regBitS = create_region("Bowser in the Sky", player, world) create_locs(regBitS, "Bowser in the Sky 1Up Block") - create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins") + bits_top = create_subregion(regBitS, "BitS: Top", "Bowser in the Sky Red Coins") + regBitS.subregions = [bits_top] def connect_regions(world: MultiWorld, player: int, source: str, target: str, rule=None): @@ -232,14 +253,14 @@ def connect_regions(world: MultiWorld, player: int, source: str, target: str, ru sourceRegion.connect(targetRegion, rule=rule) -def create_region(name: str, player: int, world: MultiWorld) -> Region: - region = Region(name, player, world) +def create_region(name: str, player: int, world: MultiWorld) -> SM64Region: + region = SM64Region(name, player, world) world.regions.append(region) return region -def create_subregion(source_region: Region, name: str, *locs: str) -> Region: - region = Region(name, source_region.player, source_region.multiworld) +def create_subregion(source_region: Region, name: str, *locs: str) -> SM64Region: + region = SM64Region(name, source_region.player, source_region.multiworld) connection = Entrance(source_region.player, name, source_region) source_region.exits.append(connection) connection.connect(region) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index f2b8e0bcdf2d..cc2b52f0f12f 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -107,9 +107,9 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, star_costs["SecondFloorDoorCost"])) - connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"]) - connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"]) - connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"]) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"], rf.build_rule("LG/TJ/SF/BF/WK")) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"], rf.build_rule("TJ/SF/BF")) + connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"], rf.build_rule("TJ/SF/BF")) connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, star_costs["StarsToFinish"])) # Course Rules @@ -146,7 +146,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move rf.assign_rule("LLL: Upper Volcano", "CL") # Shifting Sand Land rf.assign_rule("SSL: Upper Pyramid", "CL & TJ/BF/SF/LG | MOVELESS") - rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ/SF/BF & TJ+WC | TJ/SF/BF & CAPLESS | MOVELESS") + rf.assign_rule("SSL: Free Flying for 8 Red Coins", "TJ/SF/BF & TJ+WC | TJ/SF/BF & CAPLESS | MOVELESS & CAPLESS") # Dire, Dire Docks rf.assign_rule("DDD: Moving Poles", "CL & {{Bowser in the Fire Sea Key}} | TJ+DV+LG+WK & MOVELESS") rf.assign_rule("DDD: Through the Jet Stream", "MC | CAPLESS") @@ -165,6 +165,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move rf.assign_rule("TTM: Top", "MOVELESS & TJ | LJ/DV & LG/KK | MOVELESS & WK & SF/LG | MOVELESS & KK/DV") rf.assign_rule("TTM: Blast to the Lonely Mushroom", "CANN | CANNLESS & LJ | MOVELESS & CANNLESS") # Tiny-Huge Island + rf.assign_rule("THI: 1Up Block THI Small near Start", "NAR | {THI: Pipes}") rf.assign_rule("THI: Pipes", "NAR | LJ/TJ/DV/LG | MOVELESS & BF/SF/KK") rf.assign_rule("THI: Large Top", "NAR | LJ/TJ/DV | MOVELESS") rf.assign_rule("THI: Wiggler's Red Coins", "WK") @@ -225,11 +226,11 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) if world.CompletionType[player] == "last_bowser_stage": - world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Sky", 'Region', player) + world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) elif world.CompletionType[player] == "all_bowser_stages": world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ - state.can_reach("Bowser in the Fire Sea", 'Region', player) and \ - state.can_reach("Bowser in the Sky", 'Region', player) + state.can_reach("BitFS: Upper", 'Region', player) and \ + state.can_reach("BitS: Top", 'Region', player) class RuleFactory: diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index e54a4b7a9103..e6a6e42c76a0 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -5,8 +5,8 @@ from .Locations import location_table, SM64Location from .Options import sm64_options from .Rules import set_rules -from .Regions import create_regions, sm64_level_to_entrances -from BaseClasses import Item, Tutorial, ItemClassification +from .Regions import create_regions, sm64_level_to_entrances, SM64Levels +from BaseClasses import Item, Tutorial, ItemClassification, Region from ..AutoWorld import World, WebWorld @@ -200,11 +200,21 @@ def generate_output(self, output_directory: str): with open(os.path.join(output_directory, filename), 'w') as f: json.dump(data, f) - def modify_multidata(self, multidata): + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): if self.topology_present: er_hint_data = {} for entrance, destination in self.area_connections.items(): - region = self.multiworld.get_region(sm64_level_to_entrances[destination], self.player) - for location in region.locations: - er_hint_data[location.address] = sm64_level_to_entrances[entrance] - multidata['er_hint_data'][self.player] = er_hint_data + regions = [self.multiworld.get_region(sm64_level_to_entrances[destination], self.player)] + if regions[0].name == "Tiny-Huge Island (Huge)": + # Special rules for Tiny-Huge Island's dual entrances + reverse_area_connections = {destination: entrance for entrance, destination in self.area_connections.items()} + entrance_name = sm64_level_to_entrances[reverse_area_connections[SM64Levels.TINY_HUGE_ISLAND_HUGE]] \ + + ' or ' + sm64_level_to_entrances[reverse_area_connections[SM64Levels.TINY_HUGE_ISLAND_TINY]] + regions[0] = self.multiworld.get_region("Tiny-Huge Island", self.player) + else: + entrance_name = sm64_level_to_entrances[entrance] + regions += regions[0].subregions + for region in regions: + for location in region.locations: + er_hint_data[location.address] = entrance_name + hint_data[self.player] = er_hint_data From 32315776ac0ac1a714eb9d58688c479e2038c658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Wed, 20 Mar 2024 16:57:45 -0400 Subject: [PATCH 08/74] Stardew Valley: Fix extended family legendary fishes being locations with fishsanity set to exclude legendary (#2967) --- worlds/stardew_valley/locations.py | 7 ++++--- worlds/stardew_valley/options.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 3bd1cf21e3f6..103b3bd96081 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -6,7 +6,7 @@ from . import data from .bundles.bundle_room import BundleRoom -from .data.fish_data import legendary_fish, special_fish, get_fish_for_mods +from .data.fish_data import special_fish, get_fish_for_mods from .data.museum_data import all_museum_items from .data.villagers_data import get_villagers_for_mods from .mods.mod_data import ModNames @@ -206,7 +206,8 @@ def extend_fishsanity_locations(randomized_locations: List[LocationData], option if fishsanity == Fishsanity.option_none: return elif fishsanity == Fishsanity.option_legendaries: - randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish) + fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.legendary] + randomized_locations.extend(filter_disabled_locations(options, fish_locations)) elif fishsanity == Fishsanity.option_special: randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) elif fishsanity == Fishsanity.option_randomized: @@ -216,7 +217,7 @@ def extend_fishsanity_locations(randomized_locations: List[LocationData], option fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish] randomized_locations.extend(filter_disabled_locations(options, fish_locations)) elif fishsanity == Fishsanity.option_exclude_legendaries: - fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish not in legendary_fish] + fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if not fish.legendary] randomized_locations.extend(filter_disabled_locations(options, fish_locations)) elif fishsanity == Fishsanity.option_exclude_hard_fish: fish_locations = [location_table[f"{prefix}{fish.name}"] for fish in active_fish if fish.difficulty < 80] diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index c2d239d2749e..634de45285f7 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -356,7 +356,7 @@ class QuestLocations(NamedRange): class Fishsanity(Choice): """Locations for catching a fish the first time? None: There are no locations for catching fish - Legendaries: Each of the 5 legendary fish are checks + Legendaries: Each of the 5 legendary fish are checks, plus the extended family if qi board is turned on Special: A curated selection of strong fish are checks Randomized: A random selection of fish are checks All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal From db02e9d2aabc0f4c1302ac761b3f5547ef00c7c5 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Wed, 20 Mar 2024 15:03:25 -0600 Subject: [PATCH 09/74] Castlevania 64: Implement New Game (#2472) --- README.md | 1 + docs/CODEOWNERS | 3 + inno_setup.iss | 5 + worlds/cv64/__init__.py | 327 +++ worlds/cv64/aesthetics.py | 666 ++++++ worlds/cv64/client.py | 207 ++ worlds/cv64/data/APLogo-LICENSE.txt | 3 + worlds/cv64/data/ap_icons.bin | Bin 0 -> 4666 bytes worlds/cv64/data/ename.py | 71 + worlds/cv64/data/iname.py | 49 + worlds/cv64/data/lname.py | 479 +++++ worlds/cv64/data/patches.py | 2865 +++++++++++++++++++++++++ worlds/cv64/data/rname.py | 63 + worlds/cv64/docs/en_Castlevania 64.md | 148 ++ worlds/cv64/docs/obscure_checks.md | 429 ++++ worlds/cv64/docs/setup_en.md | 63 + worlds/cv64/entrances.py | 149 ++ worlds/cv64/items.py | 214 ++ worlds/cv64/locations.py | 699 ++++++ worlds/cv64/lzkn64.py | 266 +++ worlds/cv64/options.py | 490 +++++ worlds/cv64/regions.py | 517 +++++ worlds/cv64/rom.py | 959 +++++++++ worlds/cv64/rules.py | 103 + worlds/cv64/src/drop_sub_weapon.c | 69 + worlds/cv64/src/print.c | 116 + worlds/cv64/src/print_text_ovl.c | 26 + worlds/cv64/stages.py | 490 +++++ worlds/cv64/test/__init__.py | 6 + worlds/cv64/test/test_access.py | 250 +++ worlds/cv64/text.py | 95 + 31 files changed, 9828 insertions(+) create mode 100644 worlds/cv64/__init__.py create mode 100644 worlds/cv64/aesthetics.py create mode 100644 worlds/cv64/client.py create mode 100644 worlds/cv64/data/APLogo-LICENSE.txt create mode 100644 worlds/cv64/data/ap_icons.bin create mode 100644 worlds/cv64/data/ename.py create mode 100644 worlds/cv64/data/iname.py create mode 100644 worlds/cv64/data/lname.py create mode 100644 worlds/cv64/data/patches.py create mode 100644 worlds/cv64/data/rname.py create mode 100644 worlds/cv64/docs/en_Castlevania 64.md create mode 100644 worlds/cv64/docs/obscure_checks.md create mode 100644 worlds/cv64/docs/setup_en.md create mode 100644 worlds/cv64/entrances.py create mode 100644 worlds/cv64/items.py create mode 100644 worlds/cv64/locations.py create mode 100644 worlds/cv64/lzkn64.py create mode 100644 worlds/cv64/options.py create mode 100644 worlds/cv64/regions.py create mode 100644 worlds/cv64/rom.py create mode 100644 worlds/cv64/rules.py create mode 100644 worlds/cv64/src/drop_sub_weapon.c create mode 100644 worlds/cv64/src/print.c create mode 100644 worlds/cv64/src/print_text_ovl.c create mode 100644 worlds/cv64/stages.py create mode 100644 worlds/cv64/test/__init__.py create mode 100644 worlds/cv64/test/test_access.py create mode 100644 worlds/cv64/text.py diff --git a/README.md b/README.md index 975f0ce75a7b..2c0c164b53c4 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ Currently, the following games are supported: * Kirby's Dream Land 3 * Celeste 64 * Zork Grand Inquisitor +* Castlevania 64 For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 90b1dabb6dc8..9c801f04af04 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -28,6 +28,9 @@ # Bumper Stickers /worlds/bumpstik/ @FelicitusNeko +# Castlevania 64 +/worlds/cv64/ @LiquidCat64 + # Celeste 64 /worlds/celeste64/ @PoryGone diff --git a/inno_setup.iss b/inno_setup.iss index 5a6d60830660..9f4c9d1678ef 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -169,6 +169,11 @@ Root: HKCR; Subkey: "{#MyAppName}pkmnepatch"; ValueData: "Ar Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}pkmnepatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apcv64"; ValueData: "{#MyAppName}cv64patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Archipelago Castlevania 64 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: ""; + Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py new file mode 100644 index 000000000000..ca4697bce8d5 --- /dev/null +++ b/worlds/cv64/__init__.py @@ -0,0 +1,327 @@ +import os +import typing +import settings +import base64 +import logging + +from BaseClasses import Item, Region, MultiWorld, Tutorial, ItemClassification +from .items import CV64Item, filler_item_names, get_item_info, get_item_names_to_ids, get_item_counts +from .locations import CV64Location, get_location_info, verify_locations, get_location_names_to_ids, base_id +from .entrances import verify_entrances, get_warp_entrances +from .options import CV64Options, CharacterStages, DraculasCondition, SubWeaponShuffle +from .stages import get_locations_from_stage, get_normal_stage_exits, vanilla_stage_order, \ + shuffle_stages, generate_warps, get_region_names +from .regions import get_region_info +from .rules import CV64Rules +from .data import iname, rname, ename +from ..AutoWorld import WebWorld, World +from .aesthetics import randomize_lighting, shuffle_sub_weapons, rom_empty_breakables_flags, rom_sub_weapon_flags, \ + randomize_music, get_start_inventory_data, get_location_data, randomize_shop_prices, get_loading_zone_bytes, \ + get_countdown_numbers +from .rom import LocalRom, patch_rom, get_base_rom_path, CV64DeltaPatch +from .client import Castlevania64Client + + +class CV64Settings(settings.Group): + class RomFile(settings.UserFilePath): + """File name of the CV64 US 1.0 rom""" + copy_to = "Castlevania (USA).z64" + description = "CV64 (US 1.0) ROM File" + md5s = [CV64DeltaPatch.hash] + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class CV64Web(WebWorld): + theme = "stone" + + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Archipleago Castlevania 64 randomizer on your computer and connecting it to a " + "multiworld.", + "English", + "setup_en.md", + "setup/en", + ["Liquid Cat"] + )] + + +class CV64World(World): + """ + Castlevania for the Nintendo 64 is the first 3D game in the franchise. As either whip-wielding Belmont descendant + Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you make your + way to Dracula's chamber and stop his rule of terror! + """ + game = "Castlevania 64" + item_name_groups = { + "Bomb": {iname.magical_nitro, iname.mandragora}, + "Ingredient": {iname.magical_nitro, iname.mandragora}, + } + location_name_groups = {stage: set(get_locations_from_stage(stage)) for stage in vanilla_stage_order} + options_dataclass = CV64Options + options: CV64Options + settings: typing.ClassVar[CV64Settings] + topology_present = True + data_version = 1 + + item_name_to_id = get_item_names_to_ids() + location_name_to_id = get_location_names_to_ids() + + active_stage_exits: typing.Dict[str, typing.Dict] + active_stage_list: typing.List[str] + active_warp_list: typing.List[str] + + # Default values to possibly be updated in generate_early + reinhardt_stages: bool = True + carrie_stages: bool = True + branching_stages: bool = False + starting_stage: str = rname.forest_of_silence + total_s1s: int = 7 + s1s_per_warp: int = 1 + total_s2s: int = 0 + required_s2s: int = 0 + drac_condition: int = 0 + + auth: bytearray + + web = CV64Web() + + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld) -> None: + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def generate_early(self) -> None: + # Generate the player's unique authentication + self.auth = bytearray(self.multiworld.random.getrandbits(8) for _ in range(16)) + + self.total_s1s = self.options.total_special1s.value + self.s1s_per_warp = self.options.special1s_per_warp.value + self.drac_condition = self.options.draculas_condition.value + + # If there are more S1s needed to unlock the whole warp menu than there are S1s in total, drop S1s per warp to + # something manageable. + if self.s1s_per_warp * 7 > self.total_s1s: + self.s1s_per_warp = self.total_s1s // 7 + logging.warning(f"[{self.multiworld.player_name[self.player]}] Too many required Special1s " + f"({self.options.special1s_per_warp.value * 7}) for Special1s Per Warp setting: " + f"{self.options.special1s_per_warp.value} with Total Special1s setting: " + f"{self.options.total_special1s.value}. Lowering Special1s Per Warp to: " + f"{self.s1s_per_warp}") + self.options.special1s_per_warp.value = self.s1s_per_warp + + # Set the total and required Special2s to 1 if the drac condition is the Crystal, to the specified YAML numbers + # if it's Specials, or to 0 if it's None or Bosses. The boss totals will be figured out later. + if self.drac_condition == DraculasCondition.option_crystal: + self.total_s2s = 1 + self.required_s2s = 1 + elif self.drac_condition == DraculasCondition.option_specials: + self.total_s2s = self.options.total_special2s.value + self.required_s2s = int(self.options.percent_special2s_required.value / 100 * self.total_s2s) + + # Enable/disable character stages and branching paths accordingly + if self.options.character_stages == CharacterStages.option_reinhardt_only: + self.carrie_stages = False + elif self.options.character_stages == CharacterStages.option_carrie_only: + self.reinhardt_stages = False + elif self.options.character_stages == CharacterStages.option_both: + self.branching_stages = True + + self.active_stage_exits = get_normal_stage_exits(self) + + stage_1_blacklist = [] + + # Prevent Clock Tower from being Stage 1 if more than 4 S1s are needed to warp out of it. + if self.s1s_per_warp > 4 and not self.options.multi_hit_breakables: + stage_1_blacklist.append(rname.clock_tower) + + # Shuffle the stages if the option is on. + if self.options.stage_shuffle: + self.active_stage_exits, self.starting_stage, self.active_stage_list = \ + shuffle_stages(self, stage_1_blacklist) + else: + self.active_stage_list = [stage for stage in vanilla_stage_order if stage in self.active_stage_exits] + + # Create a list of warps from the active stage list. They are in a random order by default and will never + # include the starting stage. + self.active_warp_list = generate_warps(self) + + def create_regions(self) -> None: + # Add the Menu Region. + created_regions = [Region("Menu", self.player, self.multiworld)] + + # Add every stage Region by checking to see if that stage is active. + created_regions.extend([Region(name, self.player, self.multiworld) + for name in get_region_names(self.active_stage_exits)]) + + # Add the Renon's shop Region if shopsanity is on. + if self.options.shopsanity: + created_regions.append(Region(rname.renon, self.player, self.multiworld)) + + # Add the Dracula's chamber (the end) Region. + created_regions.append(Region(rname.ck_drac_chamber, self.player, self.multiworld)) + + # Set up the Regions correctly. + self.multiworld.regions.extend(created_regions) + + # Add the warp Entrances to the Menu Region (the one always at the start of the Region list). + created_regions[0].add_exits(get_warp_entrances(self.active_warp_list)) + + for reg in created_regions: + + # Add the Entrances to all the Regions. + ent_names = get_region_info(reg.name, "entrances") + if ent_names is not None: + reg.add_exits(verify_entrances(self.options, ent_names, self.active_stage_exits)) + + # Add the Locations to all the Regions. + loc_names = get_region_info(reg.name, "locations") + if loc_names is None: + continue + verified_locs, events = verify_locations(self.options, loc_names) + reg.add_locations(verified_locs, CV64Location) + + # Place event Items on all of their associated Locations. + for event_loc, event_item in events.items(): + self.get_location(event_loc).place_locked_item(self.create_item(event_item, "progression")) + # If we're looking at a boss kill trophy, increment the total S2s and, if we're not already at the + # set number of required bosses, the total required number. This way, we can prevent gen failures + # should the player set more bosses required than there are total. + if event_item == iname.trophy: + self.total_s2s += 1 + if self.required_s2s < self.options.bosses_required.value: + self.required_s2s += 1 + + # If Dracula's Condition is Bosses and there are less calculated required S2s than the value specified by the + # player (meaning there weren't enough bosses to reach the player's setting), throw a warning and lower the + # option value. + if self.options.draculas_condition == DraculasCondition.option_bosses and self.required_s2s < \ + self.options.bosses_required.value: + logging.warning(f"[{self.multiworld.player_name[self.player]}] Not enough bosses for Bosses Required " + f"setting: {self.options.bosses_required.value}. Lowering to: {self.required_s2s}") + self.options.bosses_required.value = self.required_s2s + + def create_item(self, name: str, force_classification: typing.Optional[str] = None) -> Item: + if force_classification is not None: + classification = getattr(ItemClassification, force_classification) + else: + classification = getattr(ItemClassification, get_item_info(name, "default classification")) + + code = get_item_info(name, "code") + if code is not None: + code += base_id + + created_item = CV64Item(name, classification, code, self.player) + + return created_item + + def create_items(self) -> None: + item_counts = get_item_counts(self) + + # Set up the items correctly + self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item + in item_counts[classification] for _ in range(item_counts[classification][item])] + + def set_rules(self) -> None: + # Set all the Entrance rules properly. + CV64Rules(self).set_cv64_rules() + + def pre_fill(self) -> None: + # If we need more Special1s to warp out of Sphere 1 than there are locations available, then AP's fill + # algorithm may try placing the Special1s anyway despite placing the stage's single key always being an option. + # To get around this problem in the fill algorithm, the keys will be forced early in these situations to ensure + # the algorithm will pick them over the Special1s. + if self.starting_stage == rname.tower_of_science: + if self.s1s_per_warp > 3: + self.multiworld.local_early_items[self.player][iname.science_key2] = 1 + elif self.starting_stage == rname.clock_tower: + if (self.s1s_per_warp > 2 and not self.options.multi_hit_breakables) or \ + (self.s1s_per_warp > 8 and self.options.multi_hit_breakables): + self.multiworld.local_early_items[self.player][iname.clocktower_key1] = 1 + elif self.starting_stage == rname.castle_wall: + if self.s1s_per_warp > 5 and not self.options.hard_logic and \ + not self.options.multi_hit_breakables: + self.multiworld.local_early_items[self.player][iname.left_tower_key] = 1 + + def generate_output(self, output_directory: str) -> None: + active_locations = self.multiworld.get_locations(self.player) + + # Location data and shop names, descriptions, and colors + offset_data, shop_name_list, shop_colors_list, shop_desc_list = \ + get_location_data(self, active_locations) + # Shop prices + if self.options.shop_prices: + offset_data.update(randomize_shop_prices(self)) + # Map lighting + if self.options.map_lighting: + offset_data.update(randomize_lighting(self)) + # Sub-weapons + if self.options.sub_weapon_shuffle == SubWeaponShuffle.option_own_pool: + offset_data.update(shuffle_sub_weapons(self)) + elif self.options.sub_weapon_shuffle == SubWeaponShuffle.option_anywhere: + offset_data.update(rom_sub_weapon_flags) + # Empty breakables + if self.options.empty_breakables: + offset_data.update(rom_empty_breakables_flags) + # Music + if self.options.background_music: + offset_data.update(randomize_music(self)) + # Loading zones + offset_data.update(get_loading_zone_bytes(self.options, self.starting_stage, self.active_stage_exits)) + # Countdown + if self.options.countdown: + offset_data.update(get_countdown_numbers(self.options, active_locations)) + # Start Inventory + offset_data.update(get_start_inventory_data(self.player, self.options, + self.multiworld.precollected_items[self.player])) + + cv64_rom = LocalRom(get_base_rom_path()) + + rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.z64") + + patch_rom(self, cv64_rom, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations) + + cv64_rom.write_to_file(rompath) + + patch = CV64DeltaPatch(os.path.splitext(rompath)[0] + CV64DeltaPatch.patch_file_ending, player=self.player, + player_name=self.multiworld.player_name[self.player], patched_path=rompath) + patch.write() + os.unlink(rompath) + + def get_filler_item_name(self) -> str: + return self.random.choice(filler_item_names) + + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]): + # Attach each location's stage's position to its hint information if Stage Shuffle is on. + if not self.options.stage_shuffle: + return + + stage_pos_data = {} + for loc in list(self.multiworld.get_locations(self.player)): + stage = get_region_info(loc.parent_region.name, "stage") + if stage is not None and loc.address is not None: + num = str(self.active_stage_exits[stage]["position"]).zfill(2) + path = self.active_stage_exits[stage]["path"] + stage_pos_data[loc.address] = f"Stage {num}" + if path != " ": + stage_pos_data[loc.address] += path + hint_data[self.player] = stage_pos_data + + def modify_multidata(self, multidata: typing.Dict[str, typing.Any]): + # Put the player's unique authentication in connect_names. + multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \ + multidata["connect_names"][self.multiworld.player_name[self.player]] + + def write_spoiler(self, spoiler_handle: typing.TextIO) -> None: + # Write the stage order to the spoiler log + spoiler_handle.write(f"\nCastlevania 64 stage & warp orders for {self.multiworld.player_name[self.player]}:\n") + for stage in self.active_stage_list: + num = str(self.active_stage_exits[stage]["position"]).zfill(2) + path = self.active_stage_exits[stage]["path"] + spoiler_handle.writelines(f"Stage {num}{path}:\t{stage}\n") + + # Write the warp order to the spoiler log + spoiler_handle.writelines(f"\nStart :\t{self.active_stage_list[0]}\n") + for i in range(1, len(self.active_warp_list)): + spoiler_handle.writelines(f"Warp {i}:\t{self.active_warp_list[i]}\n") diff --git a/worlds/cv64/aesthetics.py b/worlds/cv64/aesthetics.py new file mode 100644 index 000000000000..cbf2728c8298 --- /dev/null +++ b/worlds/cv64/aesthetics.py @@ -0,0 +1,666 @@ +import logging + +from BaseClasses import ItemClassification, Location, Item +from .data import iname, rname +from .options import CV64Options, BackgroundMusic, Countdown, IceTrapAppearance, InvisibleItems, CharacterStages +from .stages import vanilla_stage_order, get_stage_info +from .locations import get_location_info, base_id +from .regions import get_region_info +from .items import get_item_info, item_info + +from typing import TYPE_CHECKING, Dict, List, Tuple, Union, Iterable + +if TYPE_CHECKING: + from . import CV64World + +rom_sub_weapon_offsets = { + 0x10C6EB: (0x10, rname.forest_of_silence), # Forest + 0x10C6F3: (0x0F, rname.forest_of_silence), + 0x10C6FB: (0x0E, rname.forest_of_silence), + 0x10C703: (0x0D, rname.forest_of_silence), + + 0x10C81F: (0x0F, rname.castle_wall), # Castle Wall + 0x10C827: (0x10, rname.castle_wall), + 0x10C82F: (0x0E, rname.castle_wall), + 0x7F9A0F: (0x0D, rname.castle_wall), + + 0x83A5D9: (0x0E, rname.villa), # Villa + 0x83A5E5: (0x0D, rname.villa), + 0x83A5F1: (0x0F, rname.villa), + 0xBFC903: (0x10, rname.villa), + 0x10C987: (0x10, rname.villa), + 0x10C98F: (0x0D, rname.villa), + 0x10C997: (0x0F, rname.villa), + 0x10CF73: (0x10, rname.villa), + + 0x10CA57: (0x0D, rname.tunnel), # Tunnel + 0x10CA5F: (0x0E, rname.tunnel), + 0x10CA67: (0x10, rname.tunnel), + 0x10CA6F: (0x0D, rname.tunnel), + 0x10CA77: (0x0F, rname.tunnel), + 0x10CA7F: (0x0E, rname.tunnel), + + 0x10CBC7: (0x0E, rname.castle_center), # Castle Center + 0x10CC0F: (0x0D, rname.castle_center), + 0x10CC5B: (0x0F, rname.castle_center), + + 0x10CD3F: (0x0E, rname.tower_of_execution), # Character towers + 0x10CD65: (0x0D, rname.tower_of_execution), + 0x10CE2B: (0x0E, rname.tower_of_science), + 0x10CE83: (0x10, rname.duel_tower), + + 0x10CF8B: (0x0F, rname.room_of_clocks), # Room of Clocks + 0x10CF93: (0x0D, rname.room_of_clocks), + + 0x99BC5A: (0x0D, rname.clock_tower), # Clock Tower + 0x10CECB: (0x10, rname.clock_tower), + 0x10CED3: (0x0F, rname.clock_tower), + 0x10CEDB: (0x0E, rname.clock_tower), + 0x10CEE3: (0x0D, rname.clock_tower), +} + +rom_sub_weapon_flags = { + 0x10C6EC: 0x0200FF04, # Forest of Silence + 0x10C6FC: 0x0400FF04, + 0x10C6F4: 0x0800FF04, + 0x10C704: 0x4000FF04, + + 0x10C831: 0x08, # Castle Wall + 0x10C829: 0x10, + 0x10C821: 0x20, + 0xBFCA97: 0x04, + + # Villa + 0xBFC926: 0xFF04, + 0xBFC93A: 0x80, + 0xBFC93F: 0x01, + 0xBFC943: 0x40, + 0xBFC947: 0x80, + 0x10C989: 0x10, + 0x10C991: 0x20, + 0x10C999: 0x40, + 0x10CF77: 0x80, + + 0x10CA58: 0x4000FF0E, # Tunnel + 0x10CA6B: 0x80, + 0x10CA60: 0x1000FF05, + 0x10CA70: 0x2000FF05, + 0x10CA78: 0x4000FF05, + 0x10CA80: 0x8000FF05, + + 0x10CBCA: 0x02, # Castle Center + 0x10CC10: 0x80, + 0x10CC5C: 0x40, + + 0x10CE86: 0x01, # Duel Tower + 0x10CD43: 0x02, # Tower of Execution + 0x10CE2E: 0x20, # Tower of Science + + 0x10CF8E: 0x04, # Room of Clocks + 0x10CF96: 0x08, + + 0x10CECE: 0x08, # Clock Tower + 0x10CED6: 0x10, + 0x10CEE6: 0x20, + 0x10CEDE: 0x80, +} + +rom_empty_breakables_flags = { + 0x10C74D: 0x40FF05, # Forest of Silence + 0x10C765: 0x20FF0E, + 0x10C774: 0x0800FF0E, + 0x10C755: 0x80FF05, + 0x10C784: 0x0100FF0E, + 0x10C73C: 0x0200FF0E, + + 0x10C8D0: 0x0400FF0E, # Villa foyer + + 0x10CF9F: 0x08, # Room of Clocks flags + 0x10CFA7: 0x01, + 0xBFCB6F: 0x04, # Room of Clocks candle property IDs + 0xBFCB73: 0x05, +} + +rom_axe_cross_lower_values = { + 0x6: [0x7C7F97, 0x07], # Forest + 0x8: [0x7C7FA6, 0xF9], + + 0x30: [0x83A60A, 0x71], # Villa hallway + 0x27: [0x83A617, 0x26], + 0x2C: [0x83A624, 0x6E], + + 0x16C: [0x850FE6, 0x07], # Villa maze + + 0x10A: [0x8C44D3, 0x08], # CC factory floor + 0x109: [0x8C44E1, 0x08], + + 0x74: [0x8DF77C, 0x07], # CC invention area + 0x60: [0x90FD37, 0x43], + 0x55: [0xBFCC2B, 0x43], + 0x65: [0x90FBA1, 0x51], + 0x64: [0x90FBAD, 0x50], + 0x61: [0x90FE56, 0x43] +} + +rom_looping_music_fade_ins = { + 0x10: None, + 0x11: None, + 0x12: None, + 0x13: None, + 0x14: None, + 0x15: None, + 0x16: 0x17, + 0x18: 0x19, + 0x1A: 0x1B, + 0x21: 0x75, + 0x27: None, + 0x2E: 0x23, + 0x39: None, + 0x45: 0x63, + 0x56: None, + 0x57: 0x58, + 0x59: None, + 0x5A: None, + 0x5B: 0x5C, + 0x5D: None, + 0x5E: None, + 0x5F: None, + 0x60: 0x61, + 0x62: None, + 0x64: None, + 0x65: None, + 0x66: None, + 0x68: None, + 0x69: None, + 0x6D: 0x78, + 0x6E: None, + 0x6F: None, + 0x73: None, + 0x74: None, + 0x77: None, + 0x79: None +} + +music_sfx_ids = [0x1C, 0x4B, 0x4C, 0x4D, 0x4E, 0x55, 0x6C, 0x76] + +renon_item_dialogue = { + 0x02: "More Sub-weapon uses!\n" + "Just what you need!", + 0x03: "Galamoth told me it's\n" + "a heart in other times.", + 0x04: "Who needs Warp Rooms\n" + "when you have these?", + 0x05: "I was told to safeguard\n" + "this, but I dunno why.", + 0x06: "Fresh off a Behemoth!\n" + "Those cows are weird.", + 0x07: "Preserved with special\n" + " wall-based methods.", + 0x08: "Don't tell Geneva\n" + "about this...", + 0x09: "If this existed in 1094,\n" + "that whip wouldn't...", + 0x0A: "For when some lizard\n" + "brain spits on your ego.", + 0x0C: "It'd be a shame if you\n" + "lost it immediately...", + 0x10C: "No consequences should\n" + "you perish with this!", + 0x0D: "Arthur was far better\n" + "with it than you!", + 0x0E: "Night Creatures handle\n" + "with care!", + 0x0F: "Some may call it a\n" + "\"Banshee Boomerang.\"", + 0x10: "No weapon triangle\n" + "advantages with this.", + 0x12: "It looks sus? Trust me," + "my wares are genuine.", + 0x15: "This non-volatile kind\n" + "is safe to handle.", + 0x16: "If you can soul-wield,\n" + "they have a good one!", + 0x17: "Calls the morning sun\n" + "to vanquish the night.", + 0x18: "1 on-demand horrible\n" + "night. Devils love it!", + 0x1A: "Want to study here?\n" + "It will cost you.", + 0x1B: "\"Let them eat cake!\"\n" + "Said no princess ever.", + 0x1C: "Why do I suspect this\n" + "was a toilet room?", + 0x1D: "When you see Coller,\n" + "tell him I said hi!", + 0x1E: "Atomic number is 29\n" + "and weight is 63.546.", + 0x1F: "One torture per pay!\n" + "Who will it be?", + 0x20: "Being here feels like\n" + "time is slowing down.", + 0x21: "Only one thing beind\n" + "this. Do you dare?", + 0x22: "The key 2 Science!\n" + "Both halves of it!", + 0x23: "This warehouse can\n" + "be yours for a fee.", + 0x24: "Long road ahead if you\n" + "don't have the others.", + 0x25: "Will you get the curse\n" + "of eternal burning?", + 0x26: "What's beyond time?\n" + "Find out your", + 0x27: "Want to take out a\n" + "loan? By all means!", + 0x28: "The bag is green,\n" + "so it must be lucky!", + 0x29: "(Does this fool realize?)\n" + "Oh, sorry.", + "prog": "They will absolutely\n" + "need it in time!", + "useful": "Now, this would be\n" + "useful to send...", + "common": "Every last little bit\n" + "helps, right?", + "trap": "I'll teach this fool\n" + " a lesson for a price!", + "dlc coin": "1 coin out of... wha!?\n" + "You imp, why I oughta!" +} + + +def randomize_lighting(world: "CV64World") -> Dict[int, int]: + """Generates randomized data for the map lighting table.""" + randomized_lighting = {} + for entry in range(67): + for sub_entry in range(19): + if sub_entry not in [3, 7, 11, 15] and entry != 4: + # The fourth entry in the lighting table affects the lighting on some item pickups; skip it + randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = \ + world.random.randint(0, 255) + return randomized_lighting + + +def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]: + """Shuffles the sub-weapons amongst themselves.""" + sub_weapon_dict = {offset: rom_sub_weapon_offsets[offset][0] for offset in rom_sub_weapon_offsets if + rom_sub_weapon_offsets[offset][1] in world.active_stage_exits} + + # Remove the one 3HB sub-weapon in Tower of Execution if 3HBs are not shuffled. + if not world.options.multi_hit_breakables and 0x10CD65 in sub_weapon_dict: + del (sub_weapon_dict[0x10CD65]) + + sub_bytes = list(sub_weapon_dict.values()) + world.random.shuffle(sub_bytes) + return dict(zip(sub_weapon_dict, sub_bytes)) + + +def randomize_music(world: "CV64World") -> Dict[int, int]: + """Generates randomized or disabled data for all the music in the game.""" + music_array = bytearray(0x7A) + for number in music_sfx_ids: + music_array[number] = number + if world.options.background_music == BackgroundMusic.option_randomized: + looping_songs = [] + non_looping_songs = [] + fade_in_songs = {} + # Create shuffle-able lists of all the looping, non-looping, and fade-in track IDs + for i in range(0x10, len(music_array)): + if i not in rom_looping_music_fade_ins.keys() and i not in rom_looping_music_fade_ins.values() and \ + i != 0x72: # Credits song is blacklisted + non_looping_songs.append(i) + elif i in rom_looping_music_fade_ins.keys(): + looping_songs.append(i) + elif i in rom_looping_music_fade_ins.values(): + fade_in_songs[i] = i + # Shuffle the looping songs + rando_looping_songs = looping_songs.copy() + world.random.shuffle(rando_looping_songs) + looping_songs = dict(zip(looping_songs, rando_looping_songs)) + # Shuffle the non-looping songs + rando_non_looping_songs = non_looping_songs.copy() + world.random.shuffle(rando_non_looping_songs) + non_looping_songs = dict(zip(non_looping_songs, rando_non_looping_songs)) + non_looping_songs[0x72] = 0x72 + # Figure out the new fade-in songs if applicable + for vanilla_song in looping_songs: + if rom_looping_music_fade_ins[vanilla_song]: + if rom_looping_music_fade_ins[looping_songs[vanilla_song]]: + fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = rom_looping_music_fade_ins[ + looping_songs[vanilla_song]] + else: + fade_in_songs[rom_looping_music_fade_ins[vanilla_song]] = looping_songs[vanilla_song] + # Build the new music array + for i in range(0x10, len(music_array)): + if i in looping_songs.keys(): + music_array[i] = looping_songs[i] + elif i in non_looping_songs.keys(): + music_array[i] = non_looping_songs[i] + else: + music_array[i] = fade_in_songs[i] + del (music_array[0x00: 0x10]) + + # Convert the music array into a data dict + music_offsets = {} + for i in range(len(music_array)): + music_offsets[0xBFCD30 + i] = music_array[i] + + return music_offsets + + +def randomize_shop_prices(world: "CV64World") -> Dict[int, int]: + """Randomize the shop prices based on the minimum and maximum values chosen. + The minimum price will adjust if it's higher than the max.""" + min_price = world.options.minimum_gold_price.value + max_price = world.options.maximum_gold_price.value + + if min_price > max_price: + min_price = world.random.randint(0, max_price) + logging.warning(f"[{world.multiworld.player_name[world.player]}] The Minimum Gold Price " + f"({world.options.minimum_gold_price.value * 100}) is higher than the " + f"Maximum Gold Price ({max_price * 100}). Lowering the minimum to: {min_price * 100}") + world.options.minimum_gold_price.value = min_price + + shop_price_list = [world.random.randint(min_price * 100, max_price * 100) for _ in range(7)] + + # Convert the price list into a data dict. Which offset it starts from depends on how many bytes it takes up. + price_dict = {} + for i in range(len(shop_price_list)): + if shop_price_list[i] <= 0xFF: + price_dict[0x103D6E + (i*12)] = 0 + price_dict[0x103D6F + (i*12)] = shop_price_list[i] + elif shop_price_list[i] <= 0xFFFF: + price_dict[0x103D6E + (i*12)] = shop_price_list[i] + else: + price_dict[0x103D6D + (i*12)] = shop_price_list[i] + + return price_dict + + +def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, int]: + """Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should + increase a number. + + First, check the location's info to see if it has a countdown number override. + If not, then figure it out based on the parent region's stage's position in the vanilla stage order. + If the parent region is not part of any stage (as is the case for Renon's shop), skip the location entirely.""" + countdown_list = [0 for _ in range(15)] + for loc in active_locations: + if loc.address is not None and (options.countdown == Countdown.option_all_locations or + (options.countdown == Countdown.option_majors + and loc.item.advancement)): + + countdown_number = get_location_info(loc.name, "countdown") + + if countdown_number is None: + stage = get_region_info(loc.parent_region.name, "stage") + if stage is not None: + countdown_number = vanilla_stage_order.index(stage) + + if countdown_number is not None: + countdown_list[countdown_number] += 1 + + # Convert the Countdown list into a data dict + countdown_dict = {} + for i in range(len(countdown_list)): + countdown_dict[0xBFD818 + i] = countdown_list[i] + + return countdown_dict + + +def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \ + -> Tuple[Dict[int, int], List[str], List[bytearray], List[List[Union[int, str, None]]]]: + """Gets ALL the item data to go into the ROM. Item data consists of two bytes: the first dictates the appearance of + the item, the second determines what the item actually is when picked up. All items from other worlds will be AP + items that do nothing when picked up other than set their flag, and their appearance will depend on whether it's + another CV64 player's item and, if so, what item it is in their game. Ice Traps can assume the form of any item that + is progression, non-progression, or either depending on the player's settings. + + Appearance does not matter if it's one of the two NPC-given items (from either Vincent or Heinrich Meyer). For + Renon's shop items, a list containing the shop item names, descriptions, and colors will be returned alongside the + regular data.""" + + # Figure out the list of possible Ice Trap appearances to use based on the settings, first and foremost. + if world.options.ice_trap_appearance == IceTrapAppearance.option_major_only: + allowed_classifications = ["progression", "progression skip balancing"] + elif world.options.ice_trap_appearance == IceTrapAppearance.option_junk_only: + allowed_classifications = ["filler", "useful"] + else: + allowed_classifications = ["progression", "progression skip balancing", "filler", "useful"] + + trap_appearances = [] + for item in item_info: + if item_info[item]["default classification"] in allowed_classifications and item != "Ice Trap" and \ + get_item_info(item, "code") is not None: + trap_appearances.append(item) + + shop_name_list = [] + shop_desc_list = [] + shop_colors_list = [] + + location_bytes = {} + + for loc in active_locations: + # If the Location is an event, skip it. + if loc.address is None: + continue + + loc_type = get_location_info(loc.name, "type") + + # Figure out the item ID bytes to put in each Location here. Write the item itself if either it's the player's + # very own, or it belongs to an Item Link that the player is a part of. + if loc.item.player == world.player or (loc.item.player in world.multiworld.groups and + world.player in world.multiworld.groups[loc.item.player]['players']): + if loc_type not in ["npc", "shop"] and get_item_info(loc.item.name, "pickup actor id") is not None: + location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "pickup actor id") + else: + location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") + else: + # Make the item the unused Wooden Stake - our multiworld item. + location_bytes[get_location_info(loc.name, "offset")] = 0x11 + + # Figure out the item's appearance. If it's a CV64 player's item, change the multiworld item's model to + # match what it is. Otherwise, change it to an Archipelago progress or not progress icon. The model "change" + # has to be applied to even local items because this is how the game knows to count it on the Countdown. + if loc.item.game == "Castlevania 64": + location_bytes[get_location_info(loc.name, "offset") - 1] = get_item_info(loc.item.name, "code") + elif loc.item.advancement: + location_bytes[get_location_info(loc.name, "offset") - 1] = 0x11 # Wooden Stakes are majors + else: + location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12 # Roses are minors + + # If it's a PermaUp, change the item's model to a big PowerUp no matter what. + if loc.item.game == "Castlevania 64" and loc.item.code == 0x10C + base_id: + location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B + + # If it's an Ice Trap, change its model to one of the appearances we determined before. + # Unless it's an NPC item, in which case use the Ice Trap's regular ID so that it won't decrement the majors + # Countdown due to how I set up the NPC items to work. + if loc.item.game == "Castlevania 64" and loc.item.code == 0x12 + base_id: + if loc_type == "npc": + location_bytes[get_location_info(loc.name, "offset") - 1] = 0x12 + else: + location_bytes[get_location_info(loc.name, "offset") - 1] = \ + get_item_info(world.random.choice(trap_appearances), "code") + # If we chose a PermaUp as our trap appearance, change it to its actual in-game ID of 0x0B. + if location_bytes[get_location_info(loc.name, "offset") - 1] == 0x10C: + location_bytes[get_location_info(loc.name, "offset") - 1] = 0x0B + + # Apply the invisibility variable depending on the "invisible items" setting. + if (world.options.invisible_items == InvisibleItems.option_vanilla and loc_type == "inv") or \ + (world.options.invisible_items == InvisibleItems.option_hide_all and loc_type not in ["npc", "shop"]): + location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80 + elif world.options.invisible_items == InvisibleItems.option_chance and loc_type not in ["npc", "shop"]: + invisible = world.random.randint(0, 1) + if invisible: + location_bytes[get_location_info(loc.name, "offset") - 1] += 0x80 + + # If it's an Axe or Cross in a higher freestanding location, lower it into grab range. + # KCEK made these spawn 3.2 units higher for some reason. + if loc.address & 0xFFF in rom_axe_cross_lower_values and loc.item.code & 0xFF in [0x0F, 0x10]: + location_bytes[rom_axe_cross_lower_values[loc.address & 0xFFF][0]] = \ + rom_axe_cross_lower_values[loc.address & 0xFFF][1] + + # Figure out the list of shop names, descriptions, and text colors here. + if loc.parent_region.name != rname.renon: + continue + + shop_name = loc.item.name + if len(shop_name) > 18: + shop_name = shop_name[0:18] + shop_name_list.append(shop_name) + + if loc.item.player == world.player: + shop_desc_list.append([get_item_info(loc.item.name, "code"), None]) + elif loc.item.game == "Castlevania 64": + shop_desc_list.append([get_item_info(loc.item.name, "code"), + world.multiworld.get_player_name(loc.item.player)]) + else: + if loc.item.game == "DLCQuest" and loc.item.name in ["DLC Quest: Coin Bundle", + "Live Freemium or Die: Coin Bundle"]: + if getattr(world.multiworld.worlds[loc.item.player].options, "coinbundlequantity") == 1: + shop_desc_list.append(["dlc coin", world.multiworld.get_player_name(loc.item.player)]) + shop_colors_list.append(get_item_text_color(loc)) + continue + + if loc.item.advancement: + shop_desc_list.append(["prog", world.multiworld.get_player_name(loc.item.player)]) + elif loc.item.classification == ItemClassification.useful: + shop_desc_list.append(["useful", world.multiworld.get_player_name(loc.item.player)]) + elif loc.item.classification == ItemClassification.trap: + shop_desc_list.append(["trap", world.multiworld.get_player_name(loc.item.player)]) + else: + shop_desc_list.append(["common", world.multiworld.get_player_name(loc.item.player)]) + + shop_colors_list.append(get_item_text_color(loc)) + + return location_bytes, shop_name_list, shop_colors_list, shop_desc_list + + +def get_loading_zone_bytes(options: CV64Options, starting_stage: str, + active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, int]: + """Figure out all the bytes for loading zones and map transitions based on which stages are where in the exit data. + The same data was used earlier in figuring out the logic. Map transitions consist of two major components: which map + to send the player to, and which spot within the map to spawn the player at.""" + + # Write the byte for the starting stage to send the player to after the intro narration. + loading_zone_bytes = {0xB73308: get_stage_info(starting_stage, "start map id")} + + for stage in active_stage_exits: + + # Start loading zones + # If the start zone is the start of the line, have it simply refresh the map. + if active_stage_exits[stage]["prev"] == "Menu": + loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = 0xFF + loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = 0x00 + elif active_stage_exits[stage]["prev"]: + loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = \ + get_stage_info(active_stage_exits[stage]["prev"], "end map id") + loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = \ + get_stage_info(active_stage_exits[stage]["prev"], "end spawn id") + + # Change CC's end-spawn ID to put you at Carrie's exit if appropriate + if active_stage_exits[stage]["prev"] == rname.castle_center: + if options.character_stages == CharacterStages.option_carrie_only or \ + active_stage_exits[rname.castle_center]["alt"] == stage: + loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] += 1 + + # End loading zones + if active_stage_exits[stage]["next"]: + loading_zone_bytes[get_stage_info(stage, "endzone map offset")] = \ + get_stage_info(active_stage_exits[stage]["next"], "start map id") + loading_zone_bytes[get_stage_info(stage, "endzone spawn offset")] = \ + get_stage_info(active_stage_exits[stage]["next"], "start spawn id") + + # Alternate end loading zones + if active_stage_exits[stage]["alt"]: + loading_zone_bytes[get_stage_info(stage, "altzone map offset")] = \ + get_stage_info(active_stage_exits[stage]["alt"], "start map id") + loading_zone_bytes[get_stage_info(stage, "altzone spawn offset")] = \ + get_stage_info(active_stage_exits[stage]["alt"], "start spawn id") + + return loading_zone_bytes + + +def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, int]: + """Calculate and return the starting inventory values. Not every Item goes into the menu inventory, so everything + has to be handled appropriately.""" + start_inventory_data = {0xBFD867: 0, # Jewels + 0xBFD87B: 0, # PowerUps + 0xBFD883: 0, # Sub-weapon + 0xBFD88B: 0} # Ice Traps + + inventory_items_array = [0 for _ in range(35)] + total_money = 0 + + items_max = 10 + + # Raise the items max if Increase Item Limit is enabled. + if options.increase_item_limit: + items_max = 99 + + for item in precollected_items: + if item.player != player: + continue + + inventory_offset = get_item_info(item.name, "inventory offset") + sub_equip_id = get_item_info(item.name, "sub equip id") + # Starting inventory items + if inventory_offset is not None: + inventory_items_array[inventory_offset] += 1 + if inventory_items_array[inventory_offset] > items_max and "Special" not in item.name: + inventory_items_array[inventory_offset] = items_max + if item.name == iname.permaup: + if inventory_items_array[inventory_offset] > 2: + inventory_items_array[inventory_offset] = 2 + # Starting sub-weapon + elif sub_equip_id is not None: + start_inventory_data[0xBFD883] = sub_equip_id + # Starting PowerUps + elif item.name == iname.powerup: + start_inventory_data[0xBFD87B] += 1 + if start_inventory_data[0xBFD87B] > 2: + start_inventory_data[0xBFD87B] = 2 + # Starting Gold + elif "GOLD" in item.name: + total_money += int(item.name[0:4]) + if total_money > 99999: + total_money = 99999 + # Starting Jewels + elif "jewel" in item.name: + if "L" in item.name: + start_inventory_data[0xBFD867] += 10 + else: + start_inventory_data[0xBFD867] += 5 + if start_inventory_data[0xBFD867] > 99: + start_inventory_data[0xBFD867] = 99 + # Starting Ice Traps + else: + start_inventory_data[0xBFD88B] += 1 + if start_inventory_data[0xBFD88B] > 0xFF: + start_inventory_data[0xBFD88B] = 0xFF + + # Convert the inventory items into data. + for i in range(len(inventory_items_array)): + start_inventory_data[0xBFE518 + i] = inventory_items_array[i] + + # Convert the starting money into data. Which offset it starts from depends on how many bytes it takes up. + if total_money <= 0xFF: + start_inventory_data[0xBFE517] = total_money + elif total_money <= 0xFFFF: + start_inventory_data[0xBFE516] = total_money + else: + start_inventory_data[0xBFE515] = total_money + + return start_inventory_data + + +def get_item_text_color(loc: Location) -> bytearray: + if loc.item.advancement: + return bytearray([0xA2, 0x0C]) + elif loc.item.classification == ItemClassification.useful: + return bytearray([0xA2, 0x0A]) + elif loc.item.classification == ItemClassification.trap: + return bytearray([0xA2, 0x0B]) + else: + return bytearray([0xA2, 0x02]) diff --git a/worlds/cv64/client.py b/worlds/cv64/client.py new file mode 100644 index 000000000000..ff9c79f578be --- /dev/null +++ b/worlds/cv64/client.py @@ -0,0 +1,207 @@ +from typing import TYPE_CHECKING, Set +from .locations import base_id +from .text import cv64_text_wrap, cv64_string_to_bytearray + +from NetUtils import ClientStatus +import worlds._bizhawk as bizhawk +import base64 +from worlds._bizhawk.client import BizHawkClient + +if TYPE_CHECKING: + from worlds._bizhawk.context import BizHawkClientContext + + +class Castlevania64Client(BizHawkClient): + game = "Castlevania 64" + system = "N64" + patch_suffix = ".apcv64" + self_induced_death = False + received_deathlinks = 0 + death_causes = [] + currently_shopping = False + local_checked_locations: Set[int] + + def __init__(self) -> None: + super().__init__() + self.local_checked_locations = set() + + async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: + from CommonClient import logger + + try: + # Check ROM name/patch version + game_names = await bizhawk.read(ctx.bizhawk_ctx, [(0x20, 0x14, "ROM"), (0xBFBFD0, 12, "ROM")]) + if game_names[0].decode("ascii") != "CASTLEVANIA ": + return False + if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00': + logger.info("ERROR: You appear to be running an unpatched version of Castlevania 64. " + "You need to generate a patch file and use it to create a patched ROM.") + return False + if game_names[1].decode("ascii") != "ARCHIPELAGO1": + logger.info("ERROR: The patch file used to create this ROM is not compatible with " + "this client. Double check your client version against the version being " + "used by the generator.") + return False + except UnicodeDecodeError: + return False + except bizhawk.RequestFailedError: + return False # Should verify on the next pass + + ctx.game = self.game + ctx.items_handling = 0b001 + ctx.want_slot_data = False + ctx.watcher_timeout = 0.125 + return True + + async def set_auth(self, ctx: "BizHawkClientContext") -> None: + auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(0xBFBFE0, 16, "ROM")]))[0] + ctx.auth = base64.b64encode(auth_raw).decode("utf-8") + + def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: + if cmd != "Bounced": + return + if "tags" not in args: + return + if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name: + self.received_deathlinks += 1 + if "cause" in args["data"]: + cause = args["data"]["cause"] + if len(cause) > 88: + cause = cause[0x00:0x89] + else: + cause = f"{args['data']['source']} killed you!" + self.death_causes.append(cause) + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + + try: + read_state = await bizhawk.read(ctx.bizhawk_ctx, [(0x342084, 4, "RDRAM"), + (0x389BDE, 6, "RDRAM"), + (0x389BE4, 224, "RDRAM"), + (0x389EFB, 1, "RDRAM"), + (0x389EEF, 1, "RDRAM"), + (0xBFBFDE, 2, "ROM")]) + + game_state = int.from_bytes(read_state[0], "big") + save_struct = read_state[2] + written_deathlinks = int.from_bytes(bytearray(read_state[1][4:6]), "big") + deathlink_induced_death = int.from_bytes(bytearray(read_state[1][0:1]), "big") + cutscene_value = int.from_bytes(read_state[3], "big") + current_menu = int.from_bytes(read_state[4], "big") + num_received_items = int.from_bytes(bytearray(save_struct[0xDA:0xDC]), "big") + rom_flags = int.from_bytes(read_state[5], "big") + + # Make sure we are in the Gameplay or Credits states before detecting sent locations and/or DeathLinks. + # If we are in any other state, such as the Game Over state, set self_induced_death to false, so we can once + # again send a DeathLink once we are back in the Gameplay state. + if game_state not in [0x00000002, 0x0000000B]: + self.self_induced_death = False + return + + # Enable DeathLink if the bit for it is set in our ROM flags. + if "DeathLink" not in ctx.tags and rom_flags & 0x0100: + await ctx.update_death_link(True) + + # Scout the Renon shop locations if the shopsanity flag is written in the ROM. + if rom_flags & 0x0001 and ctx.locations_info == {}: + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": [base_id + i for i in range(0x1C8, 0x1CF)], + "create_as_hint": 0 + }]) + + # Send a DeathLink if we died on our own independently of receiving another one. + if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \ + deathlink_induced_death: + self.self_induced_death = True + if save_struct[0xA4] & 0x08: + # Special death message for dying while having the Vamp status. + await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!") + else: + await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!") + + # Write any DeathLinks received along with the corresponding death cause starting with the oldest. + # To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one. + if self.received_deathlinks and not self.self_induced_death and not written_deathlinks: + death_text, num_lines = cv64_text_wrap(self.death_causes[0], 96) + await bizhawk.write(ctx.bizhawk_ctx, [(0x389BE3, [0x01], "RDRAM"), + (0x389BDF, [0x11], "RDRAM"), + (0x18BF98, bytearray([0xA2, 0x0B]) + + cv64_string_to_bytearray(death_text, False), "RDRAM"), + (0x18C097, [num_lines], "RDRAM")]) + self.received_deathlinks -= 1 + del self.death_causes[0] + else: + # If the game hasn't received all items yet, the received item struct doesn't contain an item, the + # current number of received items still matches what we read before, and there are no open text boxes, + # then fill it with the next item and write the "item from player" text in its buffer. The game will + # increment the number of received items on its own. + if num_received_items < len(ctx.items_received): + next_item = ctx.items_received[num_received_items] + if next_item.flags & 0b001: + text_color = bytearray([0xA2, 0x0C]) + elif next_item.flags & 0b010: + text_color = bytearray([0xA2, 0x0A]) + elif next_item.flags & 0b100: + text_color = bytearray([0xA2, 0x0B]) + else: + text_color = bytearray([0xA2, 0x02]) + received_text, num_lines = cv64_text_wrap(f"{ctx.item_names[next_item.item]}\n" + f"from {ctx.player_names[next_item.player]}", 96) + await bizhawk.guarded_write(ctx.bizhawk_ctx, + [(0x389BE1, [next_item.item & 0xFF], "RDRAM"), + (0x18C0A8, text_color + cv64_string_to_bytearray(received_text, False), + "RDRAM"), + (0x18C1A7, [num_lines], "RDRAM")], + [(0x389BE1, [0x00], "RDRAM"), # Remote item reward buffer + (0x389CBE, save_struct[0xDA:0xDC], "RDRAM"), # Received items + (0x342891, [0x02], "RDRAM")]) # Textbox state + + flag_bytes = bytearray(save_struct[0x00:0x44]) + bytearray(save_struct[0x90:0x9F]) + locs_to_send = set() + + # Check for set location flags. + for byte_i, byte in enumerate(flag_bytes): + for i in range(8): + and_value = 0x80 >> i + if byte & and_value != 0: + flag_id = byte_i * 8 + i + + location_id = flag_id + base_id + if location_id in ctx.server_locations: + locs_to_send.add(location_id) + + # Send locations if there are any to send. + if locs_to_send != self.local_checked_locations: + self.local_checked_locations = locs_to_send + + if locs_to_send is not None: + await ctx.send_msgs([{ + "cmd": "LocationChecks", + "locations": list(locs_to_send) + }]) + + # Check the menu value to see if we are in Renon's shop, and set currently_shopping to True if we are. + if current_menu == 0xA: + self.currently_shopping = True + + # If we are currently shopping, and the current menu value is 0 (meaning we just left the shop), hint the + # un-bought shop locations that have progression. + if current_menu == 0 and self.currently_shopping: + await ctx.send_msgs([{ + "cmd": "LocationScouts", + "locations": [loc for loc, n_item in ctx.locations_info.items() if n_item.flags & 0b001], + "create_as_hint": 2 + }]) + self.currently_shopping = False + + # Send game clear if we're in either any ending cutscene or the credits state. + if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B): + await ctx.send_msgs([{ + "cmd": "StatusUpdate", + "status": ClientStatus.CLIENT_GOAL + }]) + + except bizhawk.RequestFailedError: + # Exit handler and return to main loop to reconnect. + pass diff --git a/worlds/cv64/data/APLogo-LICENSE.txt b/worlds/cv64/data/APLogo-LICENSE.txt new file mode 100644 index 000000000000..69d1e3ecd137 --- /dev/null +++ b/worlds/cv64/data/APLogo-LICENSE.txt @@ -0,0 +1,3 @@ +The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson is licensed under Attribution-NonCommercial 4.0 International. To view a copy of this license, visit http://creativecommons.org/licenses/by-nc/4.0/ + +Logo modified by Liquid Cat to fit artstyle and uses within the mod. diff --git a/worlds/cv64/data/ap_icons.bin b/worlds/cv64/data/ap_icons.bin new file mode 100644 index 0000000000000000000000000000000000000000..0a4129ac409ceb3a7126a8f04fe793d27bb0d2bc GIT binary patch literal 4666 zcmeH}ze`(D6vt1}@M2rrS`jg+CS8O=|AA5{k#5DgW1&>U(p7XQ9i*#&fRlrW&<+lT z?sh12u*AWkbr8j1+d)Xv;7~9*J>PqBa$kNupP-$*2XgLv_kPa#-t%6P)1M872m5Gf zxGxyZ5oJdy$lVNxazT(B$`U2BKS{>{_+b9)o_Y93N$-tBh5$isF9AGn%sSJ%8UxBPnk$v{ z%z8%eF_*{7<6N;-+p|@j+@9RFJ@aGpV_S{y6Eo;yzL*DTW;@M;p3Y2XK$>X|@a?oH zkn#Gd8s?q!{8%(^G<-DAm9TbxujdE1dPj|UtLn8ctu3wj^Sk<2?A-9E9=OdFZ-`*$ zhPUcjedUOa7&vV^jX1ckF~GJyA4I`D9je>swwbMWvZ}(g)aP2YAI%)iY_8PjqB$P} z`Yk8C@^NXLq&^qsF^``eI&e}S18n^4b`CH$?B4l%>r+Z}EkOfB2SlU&BTv5X{Qm2{ zpF1Lfq!IZhP0a!=P>F6i5hc3e`kIr$kt=+Qxh8BtuNIEQr?0+!{-YtaDn4DPkmWfZ zkPWi_2kYriZ%Q;Maqdd%GtnpfM372KpcDlGB`FmojdMC==UyZ3#W|}v{8_Vrx$Il) zKihnC9Z@qKmwgNBaM`znzjc>=3px+^d32`#;QD=v-?vcW|1*Aar>55 z-gh4Ivd8LM!n~*Pmbl-IQ~Q=M-}zhQVz`fPeT&xJ*Uw#tU(n_p|M=bd4xb%Sgzq_e z_AR03*Ate7aL-}=OQUab5u$QyT$G`E-x94ntjgE3rPdMW4%2bx))$;l-!l8)fAuZ@ E08)QCxc~qF literal 0 HcmV?d00001 diff --git a/worlds/cv64/data/ename.py b/worlds/cv64/data/ename.py new file mode 100644 index 000000000000..26cd7151de89 --- /dev/null +++ b/worlds/cv64/data/ename.py @@ -0,0 +1,71 @@ +forest_dbridge_gate = "Descending bridge gate" +forest_werewolf_gate = "Werewolf gate" +forest_end = "Dracula's drawbridge" + +cw_portcullis_c = "Central portcullis" +cw_lt_skip = "Do Left Tower Skip" +cw_lt_door = "Left Tower door" +cw_end = "End portcullis" + +villa_dog_gates = "Front dog gates" +villa_snipe_dogs = "Orb snipe the dogs" +villa_to_storeroom = "To Storeroom door" +villa_to_archives = "To Archives door" +villa_renon = "Villa contract" +villa_to_maze = "To maze gate" +villa_from_storeroom = "From Storeroom door" +villa_from_maze = "From maze gate" +villa_servant_door = "Servants' door" +villa_copper_door = "Copper door" +villa_copper_skip = "Get Copper Skip" +villa_bridge_door = "From bridge door" +villa_end_r = "Villa Reinhardt (daytime) exit" +villa_end_c = "Villa Carrie (nighttime) exit" + +tunnel_start_renon = "Tunnel start contract" +tunnel_gondolas = "Gondola ride" +tunnel_end_renon = "Tunnel end contract" +tunnel_end = "End Tunnel door" + +uw_final_waterfall = "Final waterfall" +uw_waterfall_skip = "Do Waterfall Skip" +uw_renon = "Underground Waterway contract" +uw_end = "End Waterway door" + +cc_tc_door = "Torture Chamber door" +cc_renon = "Castle Center contract" +cc_lower_wall = "Lower sealed cracked wall" +cc_upper_wall = "Upper cracked wall" +cc_elevator = "Activate crystal and ride elevator" +cc_exit_r = "Castle Center Reinhardt (Medusa Head) exit" +cc_exit_c = "Castle Center Carrie (Ghost) exit" + +dt_start = "Duel Tower start passage" +dt_end = "Duel Tower end passage" + +toe_start = "Tower of Execution start passage" +toe_gate = "Execution gate" +toe_gate_skip = "Just jump past the gate from above, bro!" +toe_end = "Tower of Execution end staircase" + +tosci_start = "Tower of Science start passage" +tosci_key1_door = "Science Door 1" +tosci_to_key2_door = "To Science Door 2" +tosci_from_key2_door = "From Science Door 2" +tosci_key3_door = "Science Door 3" +tosci_end = "Tower of Science end passage" + +tosor_start = "Tower of Sorcery start passage" +tosor_end = "Tower of Sorcery end passage" + +roc_gate = "Defeat boss gate" + +ct_to_door1 = "To Clocktower Door 1" +ct_from_door1 = "From Clocktower Door 1" +ct_to_door2 = "To Clocktower Door 2" +ct_from_door2 = "From Clocktower Door 2" +ct_renon = "Clock Tower contract" +ct_door_3 = "Clocktower Door 3" + +ck_slope_jump = "Slope Jump to boss tower" +ck_drac_door = "Dracula's door" diff --git a/worlds/cv64/data/iname.py b/worlds/cv64/data/iname.py new file mode 100644 index 000000000000..9b9e225ca5c7 --- /dev/null +++ b/worlds/cv64/data/iname.py @@ -0,0 +1,49 @@ +# Items +white_jewel = "White jewel" +special_one = "Special1" +special_two = "Special2" +red_jewel_s = "Red jewel(S)" +red_jewel_l = "Red jewel(L)" +roast_chicken = "Roast chicken" +roast_beef = "Roast beef" +healing_kit = "Healing kit" +purifying = "Purifying" +cure_ampoule = "Cure ampoule" +pot_pourri = "pot-pourri" +powerup = "PowerUp" +permaup = "PermaUp" +holy_water = "Holy water" +cross = "Cross" +axe = "Axe" +knife = "Knife" +wooden_stake = "Wooden stake" +rose = "Rose" +ice_trap = "Ice Trap" +the_contract = "The contract" +engagement_ring = "engagement ring" +magical_nitro = "Magical Nitro" +mandragora = "Mandragora" +sun_card = "Sun card" +moon_card = "Moon card" +incandescent_gaze = "Incandescent gaze" +five_hundred_gold = "500 GOLD" +three_hundred_gold = "300 GOLD" +one_hundred_gold = "100 GOLD" +archives_key = "Archives Key" +left_tower_key = "Left Tower Key" +storeroom_key = "Storeroom Key" +garden_key = "Garden Key" +copper_key = "Copper Key" +chamber_key = "Chamber Key" +execution_key = "Execution Key" +science_key1 = "Science Key1" +science_key2 = "Science Key2" +science_key3 = "Science Key3" +clocktower_key1 = "Clocktower Key1" +clocktower_key2 = "Clocktower Key2" +clocktower_key3 = "Clocktower Key3" + +trophy = "Trophy" +crystal = "Crystal" + +victory = "The Count Downed" diff --git a/worlds/cv64/data/lname.py b/worlds/cv64/data/lname.py new file mode 100644 index 000000000000..09db86b38083 --- /dev/null +++ b/worlds/cv64/data/lname.py @@ -0,0 +1,479 @@ +# Forest of Silence main locations +forest_pillars_right = "Forest of Silence: Grab practice pillars - Right" +forest_pillars_top = "Forest of Silence: Grab practice pillars - Top" +forest_king_skeleton = "Forest of Silence: King Skeleton's bridge" +forest_lgaz_in = "Forest of Silence: Moon gazebo inside" +forest_lgaz_top = "Forest of Silence: Moon gazebo roof" +forest_hgaz_in = "Forest of Silence: Sun gazebo inside" +forest_hgaz_top = "Forest of Silence: Sun gazebo roof" +forest_weretiger_sw = "Forest of Silence: Were-tiger switch" +forest_weretiger_gate = "Forest of Silence: Dirge maiden gate" +forest_dirge_tomb_u = "Forest of Silence: Dirge maiden crypt - Upper" +forest_dirge_plaque = "Forest of Silence: Dirge maiden pedestal plaque" +forest_corpse_save = "Forest of Silence: Tri-corpse save junction" +forest_dbridge_wall = "Forest of Silence: Descending bridge wall side" +forest_dbridge_sw = "Forest of Silence: Descending bridge switch side" +forest_dbridge_gate_r = "Forest of Silence: Tri-corpse gate - Right" +forest_dbridge_tomb_uf = "Forest of Silence: Three-crypt plaza main path crypt - Upper-front" +forest_bface_tomb_lf = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-front" +forest_bface_tomb_u = "Forest of Silence: Three-crypt plaza back-facing crypt - Upper" +forest_ibridge = "Forest of Silence: Invisible bridge platform" +forest_werewolf_tomb_r = "Forest of Silence: Werewolf crypt - Right" +forest_werewolf_plaque = "Forest of Silence: Werewolf statue plaque" +forest_werewolf_tree = "Forest of Silence: Werewolf path near tree" +forest_final_sw = "Forest of Silence: Three-crypt plaza switch" + +# Forest of Silence empty breakables +forest_dirge_tomb_l = "Forest of Silence: Dirge maiden crypt - Lower" +forest_dbridge_tomb_l = "Forest of Silence: Three-crypt plaza main path crypt - Lower" +forest_dbridge_tomb_ur = "Forest of Silence: Three-crypt plaza main path crypt - Upper-rear" +forest_bface_tomb_lr = "Forest of Silence: Three-crypt plaza back-facing crypt - Lower-rear" +forest_werewolf_tomb_lf = "Forest of Silence: Werewolf crypt - Left-front" +forest_werewolf_tomb_lr = "Forest of Silence: Werewolf crypt - Left-rear" + +# Forest of Silence 3-hit breakables +forest_dirge_rock1 = "Forest of Silence: Dirge maiden rock - Item 1" +forest_dirge_rock2 = "Forest of Silence: Dirge maiden rock - Item 2" +forest_dirge_rock3 = "Forest of Silence: Dirge maiden rock - Item 3" +forest_dirge_rock4 = "Forest of Silence: Dirge maiden rock - Item 4" +forest_dirge_rock5 = "Forest of Silence: Dirge maiden rock - Item 5" +forest_bridge_rock1 = "Forest of Silence: Bat archway rock - Item 1" +forest_bridge_rock2 = "Forest of Silence: Bat archway rock - Item 2" +forest_bridge_rock3 = "Forest of Silence: Bat archway rock - Item 3" +forest_bridge_rock4 = "Forest of Silence: Bat archway rock - Item 4" + +# Forest of Silence sub-weapons +forest_pillars_left = "Forest of Silence: Grab practice pillars - Left" +forest_dirge_ped = "Forest of Silence: Dirge maiden pedestal" +forest_dbridge_gate_l = "Forest of Silence: Tri-corpse gate - Left" +forest_werewolf_island = "Forest of Silence: Werewolf path island switch platforms" + + +# Castle Wall main locations +cw_ground_middle = "Castle Wall: Ground gatehouse - Middle" +cw_rrampart = "Castle Wall: Central rampart near right tower" +cw_lrampart = "Castle Wall: Central rampart near left tower" +cw_dragon_sw = "Castle Wall: White Dragons switch door" +cw_drac_sw = "Castle Wall: Dracula cutscene switch door" +cw_shelf_visible = "Castle Wall: Sandbag shelf - Visible" +cw_shelf_sandbags = "Castle Wall: Sandbag shelf - Invisible" + +# Castle Wall towers main locations +cwr_bottom = "Castle Wall: Above bottom right tower door" +cwl_bottom = "Castle Wall: Above bottom left tower door" +cwl_bridge = "Castle Wall: Left tower child ledge" + +# Castle Wall 3-hit breakables +cw_save_slab1 = "Castle Wall: Central rampart savepoint slab - Item 1" +cw_save_slab2 = "Castle Wall: Central rampart savepoint slab - Item 2" +cw_save_slab3 = "Castle Wall: Central rampart savepoint slab - Item 3" +cw_save_slab4 = "Castle Wall: Central rampart savepoint slab - Item 4" +cw_save_slab5 = "Castle Wall: Central rampart savepoint slab - Item 5" +cw_drac_slab1 = "Castle Wall: Dracula cutscene switch slab - Item 1" +cw_drac_slab2 = "Castle Wall: Dracula cutscene switch slab - Item 2" +cw_drac_slab3 = "Castle Wall: Dracula cutscene switch slab - Item 3" +cw_drac_slab4 = "Castle Wall: Dracula cutscene switch slab - Item 4" +cw_drac_slab5 = "Castle Wall: Dracula cutscene switch slab - Item 5" + +# Castle Wall sub-weapons +cw_ground_left = "Castle Wall: Ground gatehouse - Left" +cw_ground_right = "Castle Wall: Ground gatehouse - Right" +cw_shelf_torch = "Castle Wall: Sandbag shelf floor torch" +cw_pillar = "Castle Wall: Central rampart broken pillar" + + +# Villa front yard main locations +villafy_outer_gate_l = "Villa: Outer front gate - Left" +villafy_outer_gate_r = "Villa: Outer front gate - Right" +villafy_inner_gate = "Villa: Inner front gate dog food" +villafy_dog_platform = "Villa: Outer front gate platform" +villafy_gate_marker = "Villa: Front yard cross grave near gates" +villafy_villa_marker = "Villa: Front yard cross grave near porch" +villafy_tombstone = "Villa: Front yard visitor's tombstone" +villafy_fountain_fl = "Villa: Midnight fountain - Front-left" +villafy_fountain_fr = "Villa: Midnight fountain - Front-right" +villafy_fountain_ml = "Villa: Midnight fountain - Middle-left" +villafy_fountain_mr = "Villa: Midnight fountain - Middle-right" +villafy_fountain_rl = "Villa: Midnight fountain - Rear-left" +villafy_fountain_rr = "Villa: Midnight fountain - Rear-right" + +# Villa foyer main locations +villafo_sofa = "Villa: Foyer sofa" +villafo_pot_r = "Villa: Foyer upper-right pot" +villafo_pot_l = "Villa: Foyer upper-left pot" +villafo_rear_r = "Villa: Foyer lower level - Rear-right" +villafo_rear_l = "Villa: Foyer lower level - Rear-left" +villafo_mid_l = "Villa: Foyer lower level - Middle-left" +villafo_front_r = "Villa: Foyer lower level - Front-right" +villafo_front_l = "Villa: Foyer lower level - Front-left" +villafo_serv_ent = "Villa: Servants' entrance" + +# Villa empty breakables +villafo_mid_r = "Villa: Foyer lower level - Middle-right" + +# Villa 3-hit breakables +villafo_chandelier1 = "Villa: Foyer chandelier - Item 1" +villafo_chandelier2 = "Villa: Foyer chandelier - Item 2" +villafo_chandelier3 = "Villa: Foyer chandelier - Item 3" +villafo_chandelier4 = "Villa: Foyer chandelier - Item 4" +villafo_chandelier5 = "Villa: Foyer chandelier - Item 5" + +# Villa living area main locations +villala_hallway_stairs = "Villa: Rose garden staircase bottom" +villala_bedroom_chairs = "Villa: Bedroom near chairs" +villala_bedroom_bed = "Villa: Bedroom near bed" +villala_vincent = "Villa: Vincent" +villala_slivingroom_table = "Villa: Mary's room table" +villala_storeroom_l = "Villa: Storeroom - Left" +villala_storeroom_r = "Villa: Storeroom - Right" +villala_storeroom_s = "Villa: Storeroom statue" +villala_diningroom_roses = "Villa: Dining room rose vase" +villala_archives_table = "Villa: Archives table" +villala_archives_rear = "Villa: Archives rear corner" +villala_llivingroom_lion = "Villa: Living room lion head" +villala_llivingroom_pot_r = "Villa: Living room - Right pot" +villala_llivingroom_pot_l = "Villa: Living room - Left pot" +villala_llivingroom_light = "Villa: Living room ceiling light" +villala_llivingroom_painting = "Villa: Living room clawed painting" +villala_exit_knight = "Villa: Maze garden exit knight" + +# Villa maze main locations +villam_malus_torch = "Villa: Front maze garden - Malus start torch" +villam_malus_bush = "Villa: Front maze garden - Malus's hiding bush" +villam_frankieturf_r = "Villa: Front maze garden - Frankie's right dead-end" +villam_frankieturf_l = "Villa: Front maze garden - Frankie's left dead-end" +villam_frankieturf_ru = "Villa: Front maze garden - Frankie's right dead-end urn" +villam_fgarden_f = "Villa: Rear maze garden - Iron Thorn Fenced area - Front" +villam_fgarden_mf = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-front" +villam_fgarden_mr = "Villa: Rear maze garden - Iron Thorn Fenced area - Mid-rear" +villam_fgarden_r = "Villa: Rear maze garden - Iron Thorn Fenced area - Rear" +villam_rplatform_de = "Villa: Rear maze garden - Viewing platform dead-end" +villam_exit_de = "Villa: Rear maze garden - Past-exit dead-end" +villam_serv_path = "Villa: Servants' path small alcove" +villam_crypt_ent = "Villa: Crypt entrance" +villam_crypt_upstream = "Villa: Crypt bridge upstream" + +# Villa crypt main locations +villac_ent_l = "Villa: Crypt - Left from entrance" +villac_ent_r = "Villa: Crypt - Right from entrance" +villac_wall_l = "Villa: Crypt - Left wall" +villac_wall_r = "Villa: Crypt - Right wall" +villac_coffin_r = "Villa: Crypt - Right of coffin" + +# Villa sub-weapons +villala_hallway_l = "Villa: Hallway near rose garden stairs - Left" +villala_hallway_r = "Villa: Hallway near rose garden stairs - Right" +villala_slivingroom_mirror = "Villa: Mary's room corner" +villala_archives_entrance = "Villa: Archives near entrance" +villam_fplatform = "Villa: Front maze garden - Viewing platform" +villam_rplatform = "Villa: Rear maze garden - Viewing platform" +villac_coffin_l = "Villa: Crypt - Left of coffin" + + +# Tunnel main locations +tunnel_landing = "Tunnel: Landing point" +tunnel_landing_rc = "Tunnel: Landing point rock crusher" +tunnel_stone_alcove_l = "Tunnel: Stepping stone alcove - Left" +tunnel_twin_arrows = "Tunnel: Twin arrow signs" +tunnel_lonesome_bucket = "Tunnel: Near lonesome bucket" +tunnel_lbucket_quag = "Tunnel: Lonesome bucket poison pit" +tunnel_lbucket_albert = "Tunnel: Lonesome bucket-Albert junction" +tunnel_albert_camp = "Tunnel: Albert's campsite" +tunnel_albert_quag = "Tunnel: Albert's poison pit" +tunnel_gondola_rc_sdoor_r = "Tunnel: Gondola rock crusher sun door - Right" +tunnel_gondola_rc_sdoor_m = "Tunnel: Gondola rock crusher sun door - Middle" +tunnel_gondola_rc = "Tunnel: Gondola rock crusher" +tunnel_rgondola_station = "Tunnel: Red gondola station" +tunnel_gondola_transfer = "Tunnel: Gondola transfer point" +tunnel_corpse_bucket_quag = "Tunnel: Corpse bucket poison pit" +tunnel_corpse_bucket_mdoor_r = "Tunnel: Corpse bucket moon door - Right" +tunnel_shovel_quag_start = "Tunnel: Shovel poison pit start" +tunnel_exit_quag_start = "Tunnel: Exit door poison pit start" +tunnel_shovel_quag_end = "Tunnel: Shovel poison pit end" +tunnel_exit_quag_end = "Tunnel: Exit door poison pit end" +tunnel_shovel = "Tunnel: Shovel" +tunnel_shovel_save = "Tunnel: Shovel zone save junction" +tunnel_shovel_mdoor_l = "Tunnel: Shovel zone moon door - Left" +tunnel_shovel_sdoor_l = "Tunnel: Shovel zone sun door - Left" +tunnel_shovel_sdoor_m = "Tunnel: Shovel zone sun door - Middle" + +# Tunnel 3-hit breakables +tunnel_arrows_rock1 = "Tunnel: Twin arrow signs rock - Item 1" +tunnel_arrows_rock2 = "Tunnel: Twin arrow signs rock - Item 2" +tunnel_arrows_rock3 = "Tunnel: Twin arrow signs rock - Item 3" +tunnel_arrows_rock4 = "Tunnel: Twin arrow signs rock - Item 4" +tunnel_arrows_rock5 = "Tunnel: Twin arrow signs rock - Item 5" +tunnel_bucket_quag_rock1 = "Tunnel: Lonesome bucket poison pit rock - Item 1" +tunnel_bucket_quag_rock2 = "Tunnel: Lonesome bucket poison pit rock - Item 2" +tunnel_bucket_quag_rock3 = "Tunnel: Lonesome bucket poison pit rock - Item 3" + +# Tunnel sub-weapons +tunnel_stone_alcove_r = "Tunnel: Stepping stone alcove - Right" +tunnel_lbucket_mdoor_l = "Tunnel: Lonesome bucket moon door" +tunnel_gondola_rc_sdoor_l = "Tunnel: Gondola rock crusher sun door - Left" +tunnel_corpse_bucket_mdoor_l = "Tunnel: Corpse bucket moon door - Left" +tunnel_shovel_mdoor_r = "Tunnel: Shovel zone moon door - Right" +tunnel_shovel_sdoor_r = "Tunnel: Shovel zone sun door - Right" + + +# Underground Waterway main locations +uw_near_ent = "Underground Waterway: Near entrance corridor" +uw_across_ent = "Underground Waterway: Across from entrance" +uw_poison_parkour = "Underground Waterway: Across poison parkour ledges" +uw_waterfall_alcove = "Underground Waterway: Waterfall alcove ledge" +uw_carrie1 = "Underground Waterway: Carrie crawlspace corridor - First left" +uw_carrie2 = "Underground Waterway: Carrie crawlspace corridor - Second left" +uw_bricks_save = "Underground Waterway: Brick platforms save corridor" +uw_above_skel_ledge = "Underground Waterway: Above skeleton crusher ledge" + +# Underground Waterway 3-hit breakables +uw_first_ledge1 = "Underground Waterway: First poison parkour ledge - Item 1" +uw_first_ledge2 = "Underground Waterway: First poison parkour ledge - Item 2" +uw_first_ledge3 = "Underground Waterway: First poison parkour ledge - Item 3" +uw_first_ledge4 = "Underground Waterway: First poison parkour ledge - Item 4" +uw_first_ledge5 = "Underground Waterway: First poison parkour ledge - Item 5" +uw_first_ledge6 = "Underground Waterway: First poison parkour ledge - Item 6" +uw_in_skel_ledge1 = "Underground Waterway: Inside skeleton crusher ledge - Item 1" +uw_in_skel_ledge2 = "Underground Waterway: Inside skeleton crusher ledge - Item 2" +uw_in_skel_ledge3 = "Underground Waterway: Inside skeleton crusher ledge - Item 3" + + +# Castle Center basement main locations +ccb_skel_hallway_ent = "Castle Center: Entrance hallway" +ccb_skel_hallway_jun = "Castle Center: Basement hallway junction" +ccb_skel_hallway_tc = "Castle Center: Torture chamber hallway" +ccb_behemoth_l_ff = "Castle Center: Behemoth arena - Left far-front torch" +ccb_behemoth_l_mf = "Castle Center: Behemoth arena - Left mid-front torch" +ccb_behemoth_l_mr = "Castle Center: Behemoth arena - Left mid-rear torch" +ccb_behemoth_l_fr = "Castle Center: Behemoth arena - Left far-rear torch" +ccb_behemoth_r_ff = "Castle Center: Behemoth arena - Right far-front torch" +ccb_behemoth_r_mf = "Castle Center: Behemoth arena - Right mid-front torch" +ccb_behemoth_r_mr = "Castle Center: Behemoth arena - Right mid-rear torch" +ccb_behemoth_r_fr = "Castle Center: Behemoth arena - Right far-rear torch" +ccb_mandrag_shelf_l = "Castle Center: Mandragora shelf - Left" +ccb_mandrag_shelf_r = "Castle Center: Mandragora shelf - Right" +ccb_torture_rack = "Castle Center: Torture chamber instrument rack" +ccb_torture_rafters = "Castle Center: Torture chamber rafters" + +# Castle Center elevator room main locations +ccelv_near_machine = "Castle Center: Near elevator room machine" +ccelv_atop_machine = "Castle Center: Atop elevator room machine" +ccelv_pipes = "Castle Center: Elevator pipe device" +ccelv_staircase = "Castle Center: Elevator room staircase" + +# Castle Center factory floor main locations +ccff_redcarpet_knight = "Castle Center: Red carpet hall knight" +ccff_gears_side = "Castle Center: Gear room side" +ccff_gears_mid = "Castle Center: Gear room center" +ccff_gears_corner = "Castle Center: Gear room corner" +ccff_lizard_knight = "Castle Center: Lizard locker knight" +ccff_lizard_pit = "Castle Center: Lizard locker room near pit" +ccff_lizard_corner = "Castle Center: Lizard locker room corner" + +# Castle Center lizard lab main locations +ccll_brokenstairs_floor = "Castle Center: Broken staircase floor" +ccll_brokenstairs_knight = "Castle Center: Broken staircase knight" +ccll_brokenstairs_save = "Castle Center: Above broken staircase savepoint" +ccll_glassknight_l = "Castle Center: Stained Glass Knight room - Left" +ccll_glassknight_r = "Castle Center: Stained Glass Knight room - Right" +ccll_butlers_door = "Castle Center: Butler bros. room near door" +ccll_butlers_side = "Castle Center: Butler bros. room inner" +ccll_cwhall_butlerflames_past = "Castle Center: Past butler room flamethrowers" +ccll_cwhall_flamethrower = "Castle Center: Inside cracked wall hallway flamethrower" +ccll_cwhall_cwflames = "Castle Center: Past upper cracked wall flamethrowers" +ccll_cwhall_wall = "Castle Center: Inside upper cracked wall" +ccll_heinrich = "Castle Center: Heinrich Meyer" + +# Castle Center library main locations +ccl_bookcase = "Castle Center: Library bookshelf" + +# Castle Center invention area main locations +ccia_nitro_crates = "Castle Center: Nitro room crates" +ccia_nitro_shelf_h = "Castle Center: Magical Nitro shelf - Heinrich side" +ccia_nitro_shelf_i = "Castle Center: Magical Nitro shelf - Invention side" +ccia_nitrohall_torch = "Castle Center: Past nitro room flamethrowers" +ccia_nitrohall_flamethrower = "Castle Center: Inside nitro hallway flamethrower" +ccia_inventions_crusher = "Castle Center: Invention room spike crusher door" +ccia_inventions_maids = "Castle Center: Invention room maid sisters door" +ccia_inventions_round = "Castle Center: Invention room round machine" +ccia_inventions_famicart = "Castle Center: Invention room giant Famicart" +ccia_inventions_zeppelin = "Castle Center: Invention room zeppelin" +ccia_maids_outer = "Castle Center: Maid sisters room outer table" +ccia_maids_inner = "Castle Center: Maid sisters room inner table" +ccia_maids_vase = "Castle Center: Maid sisters room vase" +ccia_stairs_knight = "Castle Center: Hell Knight landing corner knight" + +# Castle Center sub-weapons +ccb_skel_hallway_ba = "Castle Center: Behemoth arena hallway" +ccelv_switch = "Castle Center: Near elevator switch" +ccff_lizard_near_knight = "Castle Center: Near lizard locker knight" + +# Castle Center lizard lockers +ccff_lizard_locker_nfr = "Castle Center: Far-right near-side lizard locker" +ccff_lizard_locker_nmr = "Castle Center: Mid-right near-side lizard locker" +ccff_lizard_locker_nml = "Castle Center: Mid-left near-side lizard locker" +ccff_lizard_locker_nfl = "Castle Center: Far-left near-side lizard locker" +ccff_lizard_locker_fl = "Castle Center: Left far-side lizard locker" +ccff_lizard_locker_fr = "Castle Center: Right far-side lizard locker" + +# Castle Center 3-hit breakables +ccb_behemoth_crate1 = "Castle Center: Behemoth arena crate - Item 1" +ccb_behemoth_crate2 = "Castle Center: Behemoth arena crate - Item 2" +ccb_behemoth_crate3 = "Castle Center: Behemoth arena crate - Item 3" +ccb_behemoth_crate4 = "Castle Center: Behemoth arena crate - Item 4" +ccb_behemoth_crate5 = "Castle Center: Behemoth arena crate - Item 5" +ccelv_stand1 = "Castle Center: Elevator room unoccupied statue stand - Item 1" +ccelv_stand2 = "Castle Center: Elevator room unoccupied statue stand - Item 2" +ccelv_stand3 = "Castle Center: Elevator room unoccupied statue stand - Item 3" +ccff_lizard_slab1 = "Castle Center: Lizard locker room slab - Item 1" +ccff_lizard_slab2 = "Castle Center: Lizard locker room slab - Item 2" +ccff_lizard_slab3 = "Castle Center: Lizard locker room slab - Item 3" +ccff_lizard_slab4 = "Castle Center: Lizard locker room slab - Item 4" + + +# Duel Tower main locations +dt_stones_start = "Duel Tower: Stepping stone path start" +dt_werebull_arena = "Duel Tower: Above Were-bull arena" +dt_ibridge_l = "Duel Tower: Invisible bridge balcony - Left" +dt_ibridge_r = "Duel Tower: Invisible bridge balcony - Right" + +# Duel Tower sub-weapons +dt_stones_end = "Duel Tower: Stepping stone path end" + + +# Tower of Execution main locations +toe_midsavespikes_r = "Tower of Execution: Past mid-savepoint spikes - Right" +toe_midsavespikes_l = "Tower of Execution: Past mid-savepoint spikes - Left" +toe_elec_grate = "Tower of Execution: Electric grate ledge" +toe_ibridge = "Tower of Execution: Invisible bridge ledge" +toe_top = "Tower of Execution: Guillotine tower top level" +toe_keygate_l = "Tower of Execution: Key gate alcove - Left" + +# Tower of Execution 3-hit breakables +toe_ledge1 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 1" +toe_ledge2 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 2" +toe_ledge3 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 3" +toe_ledge4 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 4" +toe_ledge5 = "Tower of Execution: Pre-mid-savepoint platforms ledge - Item 5" + +# Tower of Execution sub-weapons +toe_keygate_r = "Tower of Execution: Key gate alcove - Right" + + +# Tower of Science main locations +tosci_elevator = "Tower of Science: Elevator hallway" +tosci_plain_sr = "Tower of Science: Plain sight side room" +tosci_stairs_sr = "Tower of Science: Staircase side room" +tosci_three_door_hall = "Tower of Science: Pick-a-door hallway locked middle room" +tosci_ibridge_t = "Tower of Science: Invisible bridge platform torch" +tosci_conveyor_sr = "Tower of Science: Spiky conveyor side room" +tosci_exit = "Tower of Science: Exit hallway" +tosci_key3_r = "Tower of Science: Locked Key3 room - Right" +tosci_key3_l = "Tower of Science: Locked Key3 room - Left" + +# Tower of Science 3-hit breakables +tosci_ibridge_b1 = "Tower of Science: Invisible bridge platform crate - Item 1" +tosci_ibridge_b2 = "Tower of Science: Invisible bridge platform crate - Item 2" +tosci_ibridge_b3 = "Tower of Science: Invisible bridge platform crate - Item 3" +tosci_ibridge_b4 = "Tower of Science: Invisible bridge platform crate - Item 4" +tosci_ibridge_b5 = "Tower of Science: Invisible bridge platform crate - Item 5" +tosci_ibridge_b6 = "Tower of Science: Invisible bridge platform crate - Item 6" + +# Tower of Science sub-weapons +tosci_key3_m = "Tower of Science: Locked Key3 room - Middle" + + +# Tower of Sorcery main locations +tosor_stained_tower = "Tower of Sorcery: Stained glass tower" +tosor_savepoint = "Tower of Sorcery: Mid-savepoint platform" +tosor_trickshot = "Tower of Sorcery: Trick shot from mid-savepoint platform" +tosor_yellow_bubble = "Tower of Sorcery: Above yellow bubble" +tosor_blue_platforms = "Tower of Sorcery: Above tiny blue platforms start" +tosor_side_isle = "Tower of Sorcery: Lone red platform side island" +tosor_ibridge = "Tower of Sorcery: Invisible bridge platform" + +# Room of Clocks main locations +roc_ent_l = "Room of Clocks: Left from entrance hallway" +roc_cont_r = "Room of Clocks: Right of Contract" +roc_ent_r = "Room of Clocks: Right from entrance hallway" + +# Room of Clocks sub-weapons +roc_elev_l = "Room of Clocks: Left of elevator hallway" +roc_elev_r = "Room of Clocks: Right of elevator hallway" + +# Room of Clocks empty breakables +roc_cont_l = "Room of Clocks: Left of Contract" +roc_exit = "Room of Clocks: Left of exit" + +# Clock Tower main locations +ct_gearclimb_corner = "Clock Tower: Gear climb room corner" +ct_gearclimb_side = "Clock Tower: Gear climb room side" +ct_bp_chasm_fl = "Clock Tower: Bone Pillar chasm room - Front-left" +ct_bp_chasm_fr = "Clock Tower: Bone Pillar chasm room - Front-right" +ct_bp_chasm_k = "Clock Tower: Bone Pillar chasm room key alcove" +ct_finalroom_platform = "Clock Tower: Final room key ledge" + +# Clock Tower 3-hit breakables +ct_gearclimb_battery_slab1 = "Clock Tower: Gear climb room beneath battery slab - Item 1" +ct_gearclimb_battery_slab2 = "Clock Tower: Gear climb room beneath battery slab - Item 2" +ct_gearclimb_battery_slab3 = "Clock Tower: Gear climb room beneath battery slab - Item 3" +ct_gearclimb_door_slab1 = "Clock Tower: Gear climb room beneath door slab - Item 1" +ct_gearclimb_door_slab2 = "Clock Tower: Gear climb room beneath door slab - Item 2" +ct_gearclimb_door_slab3 = "Clock Tower: Gear climb room beneath door slab - Item 3" +ct_finalroom_door_slab1 = "Clock Tower: Final room entrance slab - Item 1" +ct_finalroom_door_slab2 = "Clock Tower: Final room entrance slab - Item 2" +ct_finalroom_renon_slab1 = "Clock Tower: Renon's final offers slab - Item 1" +ct_finalroom_renon_slab2 = "Clock Tower: Renon's final offers slab - Item 2" +ct_finalroom_renon_slab3 = "Clock Tower: Renon's final offers slab - Item 3" +ct_finalroom_renon_slab4 = "Clock Tower: Renon's final offers slab - Item 4" +ct_finalroom_renon_slab5 = "Clock Tower: Renon's final offers slab - Item 5" +ct_finalroom_renon_slab6 = "Clock Tower: Renon's final offers slab - Item 6" +ct_finalroom_renon_slab7 = "Clock Tower: Renon's final offers slab - Item 7" +ct_finalroom_renon_slab8 = "Clock Tower: Renon's final offers slab - Item 8" + +# Clock Tower sub-weapons +ct_bp_chasm_rl = "Clock Tower: Bone Pillar chasm room - Rear-left" +ct_finalroom_fr = "Clock Tower: Final room floor - front-right" +ct_finalroom_fl = "Clock Tower: Final room floor - front-left" +ct_finalroom_rr = "Clock Tower: Final room floor - rear-right" +ct_finalroom_rl = "Clock Tower: Final room floor - rear-left" + + +# Castle Keep main locations +ck_flame_l = "Castle Keep: Left Dracula door flame" +ck_flame_r = "Castle Keep: Right Dracula door flame" +ck_behind_drac = "Castle Keep: Behind Dracula's chamber" +ck_cube = "Castle Keep: Dracula's floating cube" + + +# Renon's shop locations +renon1 = "Renon's shop: Roast Chicken purchase" +renon2 = "Renon's shop: Roast Beef purchase" +renon3 = "Renon's shop: Healing Kit purchase" +renon4 = "Renon's shop: Purifying purchase" +renon5 = "Renon's shop: Cure Ampoule purchase" +renon6 = "Renon's shop: Sun Card purchase" +renon7 = "Renon's shop: Moon Card purchase" + + +# Events +forest_boss_one = "Forest of Silence: King Skeleton 1" +forest_boss_two = "Forest of Silence: Were-tiger" +forest_boss_three = "Forest of Silence: King Skeleton 2" +cw_boss = "Castle Wall: Bone Dragons" +villa_boss_one = "Villa: J. A. Oldrey" +villa_boss_two = "Villa: Undead Maiden" +uw_boss = "Underground Waterway: Lizard-man trio" +cc_boss_one = "Castle Center: Behemoth" +cc_boss_two = "Castle Center: Rosa/Camilla" +dt_boss_one = "Duel Tower: Were-jaguar" +dt_boss_two = "Duel Tower: Werewolf" +dt_boss_three = "Duel Tower: Were-bull" +dt_boss_four = "Duel Tower: Were-tiger" +roc_boss = "Room of Clocks: Death/Actrise" +ck_boss_one = "Castle Keep: Renon" +ck_boss_two = "Castle Keep: Vincent" + +cc_behind_the_seal = "Castle Center: Behind the seal" + +the_end = "Dracula" diff --git a/worlds/cv64/data/patches.py b/worlds/cv64/data/patches.py new file mode 100644 index 000000000000..4c4670363831 --- /dev/null +++ b/worlds/cv64/data/patches.py @@ -0,0 +1,2865 @@ +normal_door_hook = [ + 0x00862021, # ADDU A0, A0, A2 + 0x80849C60, # LB A0, 0x9C60 (A0) + 0x0C0FF174, # JAL 0x803FC5D0 + 0x308900FF # ANDI T1, A0, 0x00FF +] + +normal_door_code = [ + 0x00024080, # SLL T0, V0, 2 + 0x3C048039, # LUI A0, 0x8039 + 0x00882021, # ADDU A0, A0, T0 + 0x8C849BE4, # LW A0, 0x9BE4 (A0) + 0x8C6A0008, # LW T2, 0x0008 (V1) + 0x008A5824, # AND T3, A0, T2 + 0x11600003, # BEQZ T3, [forward 0x03] + 0x00000000, # NOP + 0x24020003, # ADDIU V0, R0, 0x0003 + 0x27FF006C, # ADDIU RA, RA, 0x006C + 0x03E00008 # JR RA +] + +ct_door_hook = [ + 0x0C0FF182, # JAL 0x803FC608 + 0x00000000, # NOP + 0x315900FF # ANDI T9, T2, 0x00FF +] + +ct_door_code = [ + 0x3C0A8039, # LUI T2, 0x8039 + 0x8D429BF8, # LW V0, 0x9BF8 (T2) + 0x01465021, # ADDU T2, T2, A2 + 0x814A9C60, # LB T2, 0x9C60 (T2) + 0x00495824, # AND T3, V0, T1 + 0x55600001, # BNEZL T3, [forward 0x01] + 0x27FF0010, # ADDIU RA, RA, 0x0010 + 0x03E00008 # JR RA +] + +stage_select_overwrite = [ + # Replacement for the "wipe world state" function when using the warp menu. Now it's the "Special1 jewel checker" + # to see how many destinations can be selected on it with the current count. + 0x8FA60018, # LW A2, 0x0018 (SP) + 0xA0606437, # SB R0, 0x6437 (V1) + 0x10000029, # B [forward 0x29] + 0x00000000, # NOP + 0x3C0A8039, # LUI T2, 0x8039 + 0x254A9C4B, # ADDIU T2, T2, 0x9C4B + 0x814B0000, # LB T3, 0x0000 (T2) + 0x240C000A, # ADDIU T4, R0, 0x000A + 0x016C001B, # DIVU T3, T4 + 0x00003012, # MFLO A2 + 0x24C60001, # ADDIU A2, A2, 0x0001 + 0x28CA0009, # SLTI T2, A2, 0x0009 + 0x51400001, # BEQZL T2, 0x8012AC7C + 0x24060008, # ADDIU A2, R0, 0x0008 + 0x3C0A800D, # LUI T2, 0x800D + 0x914A5E20, # LBU T2, 0x5E20 (T2) + 0x314A0040, # ANDI T2, T2, 0x0040 + 0x11400003, # BEQZ T2, [forward 0x03] + 0x240BFFFE, # ADDIU T3, R0, 0xFFFE + 0x3C0C8034, # LUI T4, 0x8034 + 0xAD8B2084, # SW T3, 0x2084 (T4) + 0x03200008, # JR T9 + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP +] + +custom_code_loader = [ + # On boot, when the company logos show up, this will trigger and load most of the custom ASM data in this module + # off from ROM offsets 0xBFC000-0xBFFFFF and into the 803FC000-803FFFFF range in RAM. + 0x3C080C10, # LUI T0, 0x0C10 + 0x2508F1C0, # ADDIU T0, T0, 0xF1C0 + 0x3C098000, # LUI T1, 0x8000 + 0xAD282438, # SW T0, 0x2438 (T1) + 0x3C088040, # LUI T0, 0x8040 + 0x9108C000, # ADDIU T0, 0xC000 (T0) + 0x15000007, # BNEZ T0, [forward 0x07] + 0x3C0400C0, # LUI A0, 0x00C0 + 0x2484C000, # ADDIU A0, A0, 0xC000 + 0x3C058040, # LUI A1, 0x8040 + 0x24A5C000, # ADDIU A1, A1, 0xC000 + 0x24064000, # ADDIU A2, R0, 0x4000 + 0x08005DFB, # J 0x800177EC + 0x00000000, # NOP + 0x03E00008 # JR RA +] + +remote_item_giver = [ + # The essential multiworld function. Every frame wherein the player is in control and not looking at a text box, + # this thing will check some bytes in RAM to see if an item or DeathLink has been received and trigger the right + # functions accordingly to either reward items or kill the player. + + # Primary checks + 0x3C088034, # LUI T0, 0x8034 + 0x9509244A, # LHU T1, 0x244A (T0) + 0x3C088039, # LUI T0, 0x8039 + 0x910A9EFB, # LBU T2, 0x9EFF (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x910A9EFF, # LBU T2, 0x9EFF (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x910A9CCF, # LBU T2, 0x9CCF (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x910A9EEF, # LBU T2, 0x9EEF (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x910A9CD3, # LBU T2, 0x9CD3 (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x3C088038, # LUI T0, 0x8038 + 0x910A7ADD, # LBU T2, 0x7ADD (T0) + 0x012A4821, # ADDU T1, T1, T2 + 0x3C0B8039, # LUI T3, 0x8039 + 0x916A9BE0, # LBU T2, 0x9BE0 (T3) + 0x012A4821, # ADDU T1, T1, T2 + 0x11200006, # BEQZ T1, [forward 0x06] + 0x00000000, # NOP + 0x11400002, # BEQZ T2, [forward 0x02] + 0x254AFFFF, # ADDIU T2, T2, 0xFFFF + 0xA16A9BE0, # SB T2, 0x9BE0 (T3) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Item-specific checks + 0x3C088034, # LUI T0, 0x8034 + 0x91082891, # LBU T0, 0x2891 (T0) + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x15090012, # BNE T0, T1, [forward 0x12] + 0x00000000, # NOP + 0x256B9BDF, # ADDIU T3, T3, 0x9BDF + 0x91640000, # LBU A0, 0x0000 (T3) + 0x14800003, # BNEZ A0, [forward 0x03] + 0x00000000, # NOP + 0x10000005, # B [forward 0x05] + 0x256B0002, # ADDIU T3, T3, 0x0002 + 0x2409000F, # ADDIU T1, R0, 0x000F + 0xA1690001, # SB T1, 0x0001 (T3) + 0x080FF8DD, # J 0x803FE374 + 0xA1600000, # SB R0, 0x0000 (T3) + 0x91640000, # LBU A0, 0x0000 (T3) + 0x14800002, # BNEZ A0, [forward 0x02] + 0x00000000, # NOP + 0x10000003, # B [forward 0x03] + 0x2409000F, # ADDIU T1, R0, 0x000F + 0x080FF864, # J 0x803FE190 + 0xA169FFFF, # SB T1, 0xFFFF (T3) + # DeathLink-specific checks + 0x3C0B8039, # LUI T3, 0x8039 + 0x256B9BE1, # ADDIU T3, T3, 0x9BE1 + 0x91640002, # LBU A0, 0x0002 (T3) + 0x14800002, # BNEZ A0, [forward 0x02] + 0x916900A7, # LBU T1, 0x00A7 (T3) + 0x080FF9C0, # J 0x803FE700 + 0x312A0080, # ANDI T2, T1, 0x0080 + 0x11400002, # BEQZ T2, [forward 0x02] + 0x00000000, # NOP + 0x03E00008, # JR RA + 0x35290080, # ORI T1, T1, 0x0080 + 0xA16900A7, # SB T1, 0x00A7 (T3) + 0x2484FFFF, # ADDIU A0, A0, 0xFFFF + 0x24080001, # ADDIU T0, R0, 0x0001 + 0x03E00008, # JR RA + 0xA168FFFD, # SB T0, 0xFFFD (T3) +] + +deathlink_nitro_edition = [ + # Alternative to the end of the above DeathLink-specific checks that kills the player with the Nitro explosion + # instead of the normal death. + 0x91690043, # LBU T1, 0x0043 (T3) + 0x080FF9C0, # J 0x803FE700 + 0x3C088034, # LUI T0, 0x8034 + 0x91082BFE, # LBU T0, 0x2BFE (T0) + 0x11000002, # BEQZ T0, [forward 0x02] + 0x00000000, # NOP + 0x03E00008, # JR RA + 0x35290080, # ORI T1, T1, 0x0080 + 0xA1690043, # SB T1, 0x0043 (T3) + 0x2484FFFF, # ADDIU A0, A0, 0xFFFF + 0x24080001, # ADDIU T0, R0, 0x0001 + 0x03E00008, # JR RA + 0xA168FFFD, # SB T0, 0xFFFD (T3) +] + +nitro_fall_killer = [ + # Custom code to force the instant fall death if at a high enough falling speed after getting killed by the Nitro + # explosion, since the game doesn't run the checks for the fall death after getting hit by said explosion and could + # result in a softlock when getting blown into an abyss. + 0x3C0C8035, # LUI T4, 0x8035 + 0x918807E2, # LBU T0, 0x07E2 (T4) + 0x2409000C, # ADDIU T1, R0, 0x000C + 0x15090006, # BNE T0, T1, [forward 0x06] + 0x3C098035, # LUI T1, 0x8035 + 0x91290810, # LBU T1, 0x0810 (T1) + 0x240A00C1, # ADDIU T2, R0, 0x00C1 + 0x152A0002, # BNE T1, T2, [forward 0x02] + 0x240B0001, # ADDIU T3, R0, 0x0001 + 0xA18B07E2, # SB T3, 0x07E2 (T4) + 0x03E00008 # JR RA +] + +deathlink_counter_decrementer = [ + # Decrements the DeathLink counter if it's above zero upon loading a previous state. Checking this number will be + # how the client will tell if a player's cause of death was something in-game or a DeathLink (and send a DeathLink + # to the server if it was the former). Also resets the remote item values to 00 so the player's received items don't + # get mucked up in-game. + 0x3C088039, # LUI T0, 0x8039 + 0x91099BE3, # LBU T1, 0x9BE3 (T0) + 0x11200002, # BEQZ T1, 0x803FC154 + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1099BE3, # SB T1, 0x9BE3 + 0x240900FF, # ADDIU T1, R0, 0x00FF + 0xA1099BE0, # SB T1, 0x9BE0 (T0) + 0xA1009BDF, # SB R0, 0x9BDF (T0) + 0xA1009BE1, # SB R0, 0x9BE1 (T0) + 0x91099BDE, # LBU T1, 0x9BDE (T0) + 0x55200001, # BNEZL T1, [forward 0x01] + 0x24090000, # ADDIU T1, R0, 0x0000 + 0xA1099BDE, # SB T1, 0x9BDE (T0) + 0x91099C24, # LBU T1, 0x9C24 (T0) + 0x312A0080, # ANDI T2, T1, 0x0080 + 0x55400001, # BNEZL T2, [forward 0x01] + 0x3129007F, # ANDI T1, T1, 0x007F + 0x03E00008, # JR RA + 0xA1099C24 # SB T1, 0x9C24 (T0) +] + +death_flag_unsetter = [ + # Un-sets the Death status bitflag when overwriting the "Restart this stage" state and sets health to full if it's + # empty. This is to ensure DeathLinked players won't get trapped in a perpetual death loop for eternity should they + # receive one right before transitioning to a different stage. + 0x3C048039, # LUI A0, 0x8039 + 0x90889C88, # LBU T0, 0x9C88 (A0) + 0x31090080, # ANDI T1, T0, 0x0080 + 0x01094023, # SUBU T0, T0, T1 + 0x908A9C3F, # LBU T2, 0x9C3F (A0) + 0x24090064, # ADDIU T1, R0, 0x0064 + 0x51400001, # BEQZL T2, [forward 0x01] + 0xA0899C3F, # SB T1, 0x9C3F (A0) + 0x08006DAE, # J 0x8001B6B8 + 0xA0889C88 # SB T0, 0x9C88 (A0) +] + +warp_menu_opener = [ + # Enables opening the Stage Select menu by pausing while holding Z + R when not in a boss fight, the castle + # crumbling sequence following Fake Dracula, or Renon's arena (in the few seconds after his health bar vanishes). + 0x3C08800D, # LUI T0, 0x800D + 0x85095E20, # LH T1, 0x5E20 (T0) + 0x24083010, # ADDIU T0, R0, 0x3010 + 0x15090011, # BNE T0, T1, [forward 0x11] + 0x3C088035, # LUI T0, 0x8035 + 0x9108F7D8, # LBU T0, 0xF7D8 (T0) + 0x24090020, # ADDIU T1, R0, 0x0020 + 0x1109000D, # BEQ T0, T1, [forward 0x0D] + 0x3C088039, # LUI T0, 0x8039 + 0x91099BFA, # LBU T1, 0x9BFA (T0) + 0x31290001, # ANDI T1, T1, 0x0001 + 0x15200009, # BNEZ T1, [forward 0x09] + 0x8D099EE0, # LW T1, 0x9EE0 + 0x3C0A001B, # LUI T2, 0x001B + 0x254A0003, # ADDIU T2, T2, 0x0003 + 0x112A0005, # BEQ T1, T2, [forward 0x05] + 0x3C098034, # LUI T1, 0x8034 + 0xA1009BE1, # SB R0, 0x9BE1 (T0) + 0x2408FFFC, # ADDIU T0, R0, 0xFFFC + 0x0804DA70, # J 0x80136960 + 0xAD282084, # SW T0, 0x2084 (T1) + 0x0804DA70, # J 0x80136960 + 0xA44E6436 # SH T6, 0x6436 (V0) +] + +give_subweapon_stopper = [ + # Extension to "give subweapon" function to not change the player's weapon if the received item is a Stake or Rose. + # Can also increment the Ice Trap counter if getting a Rose or jump to prev_subweapon_dropper if applicable. + 0x24090011, # ADDIU T1, R0, 0x0011 + 0x11240009, # BEQ T1, A0, [forward 0x09] + 0x24090012, # ADDIU T1, R0, 0x0012 + 0x11240003, # BEQ T1, A0, [forward 0x03] + 0x9465618A, # LHU A1, 0x618A (V1) + 0xA46D618A, # SH T5, 0x618A (V1) + 0x0804F0BF, # J 0x8013C2FC + 0x3C098039, # LUI T1, 0x8039 + 0x912A9BE2, # LBU T2, 0x9BE2 (T1) + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0xA12A9BE2, # SB T2, 0x9BE2 (T1) + 0x0804F0BF, # J 0x8013C2FC +] + +give_powerup_stopper = [ + # Extension to "give PowerUp" function to not increase the player's PowerUp count beyond 2 + 0x240D0002, # ADDIU T5, R0, 0x0002 + 0x556D0001, # BNEL T3, T5, [forward 0x01] + 0xA46C6234, # SH T4, 0x6234 (V1) + 0x0804F0BF # J 0x8013C2FC +] + +npc_item_hack = [ + # Hack to make NPC items show item textboxes when received (and decrease the Countdown if applicable). + 0x3C098039, # LUI T1, 0x8039 + 0x001F5602, # SRL T2, RA, 24 + 0x240B0080, # ADDIU T3, R0, 0x0080 + 0x114B001F, # BEQ T2, T3, [forward 0x1F] + 0x240A001A, # ADDIU T2, R0, 0x001A + 0x27BD0020, # ADDIU SP, SP, 0x20 + 0x15440004, # BNE T2, A0, [forward 0x04] + 0x240B0029, # ADDIU T3, R0, 0x0029 + 0x34199464, # ORI T9, R0, 0x9464 + 0x10000004, # B [forward 0x04] + 0x240C0002, # ADDIU T4, R0, 0x0002 + 0x3419DA64, # ORI T9, R0, 0xDA64 + 0x240B0002, # ADDIU T3, R0, 0x0002 + 0x240C000E, # ADDIU T4, R0, 0x000E + 0x012C7021, # ADDU T6, T1, T4 + 0x316C00FF, # ANDI T4, T3, 0x00FF + 0x000B5A02, # SRL T3, T3, 8 + 0x91CA9CA4, # LBU T2, 0x9CA4 (T6) + 0x3C0D8040, # LUI T5, 0x8040 + 0x256FFFFF, # ADDIU T7, T3, 0xFFFF + 0x01AF6821, # ADDU T5, T5, T7 + 0x91B8D71C, # LBU T8, 0xD71C (T5) + 0x29EF0019, # SLTI T7, T7, 0x0019 + 0x51E00001, # BEQZL T7, [forward 0x01] + 0x91B8D71F, # LBU T8, 0xD71F (T5) + 0x13000002, # BEQZ T8, [forward 0x02] + 0x254AFFFF, # ADDIU T2, T2, 0xFFFF + 0xA1CA9CA4, # SB T2, 0x9CA4 (T6) + 0xA12C9BDF, # SB T4, 0x9BDF (T1) + 0x3C0400BB, # LUI A0, 0x00BB + 0x00992025, # OR A0, A0, T9 + 0x3C058019, # LUI A1, 0x8019 + 0x24A5BF98, # ADDIU A1, A1, 0xBF98 + 0x08005DFB, # J 0x800177EC + 0x24060100, # ADDIU A2, R0, 0x0100 + 0x0804EFFD, # J 0x8013BFF4 + 0xAFBF0014 # SW RA, 0x0014 (SP) +] + +overlay_modifiers = [ + # Whenever a compressed overlay gets decompressed and mapped in the 0F or 0E domains, this thing will check the + # number ID in the T0 register to tell which one it is and overwrite some instructions in it on-the-fly accordingly + # to said number before it runs. Confirmed to NOT be a foolproof solution on console and Simple64; the instructions + # may not be properly overwritten on the first execution of the overlay. + + # Prevent being able to throw Nitro into the Hazardous Waste Disposals + 0x3C0A2402, # LUI T2, 0x2402 + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x24090023, # ADDIU T1, R0, 0x0023 + 0x15090003, # BNE T0, T1, [forward 0x03] + 0x00000000, # NOP + 0x03200008, # JR T9 + 0xAF2A01D4, # SW T2, 0x01D4 (T9) + # Make it so nothing can be taken from the Nitro or Mandragora shelves through the textboxes + 0x24090022, # ADDIU T1, R0, 0x0022 + 0x11090002, # BEQ T0, T1, [forward 0x02] + 0x24090021, # ADDIU T1, R0, 0x0021 + 0x15090003, # BNE T0, T1, [forward 0x03] + 0x254AFFFF, # ADDIU T2, T2, 0xFFFF + 0x03200008, # JR T9 + 0xAF2A0194, # SW T2, 0x0194 (T9) + # Fix to allow placing both bomb components at a cracked wall at once while having multiple copies of each, and + # prevent placing them at the downstairs crack altogether until the seal is removed. Also enables placing both in + # one interaction. + 0x24090024, # ADDIU T1, R0, 0x0024 + 0x15090012, # BNE T0, T1, [forward 0x12] + 0x240A0040, # ADDIU T2, R0, 0x0040 + 0x240BC338, # ADDIU T3, R0, 0xC338 + 0x240CC3D4, # ADDIU T4, R0, 0xC3D4 + 0x240DC38C, # ADDIU T5, R0, 0xC38C + 0xA32A030F, # SB T2, 0x030F (T9) + 0xA72B0312, # SH T3, 0x0312 (T9) + 0xA32A033F, # SB T2, 0x033F (T9) + 0xA72B0342, # SH T3, 0x0342 (T9) + 0xA32A03E3, # SB T2, 0x03E3 (T9) + 0xA72C03E6, # SH T4, 0x03E6 (T9) + 0xA32A039F, # SB T2, 0x039F (T9) + 0xA72D03A2, # SH T5, 0x03A2 (T9) + 0xA32A03CB, # SB T2, 0x03CB (T9) + 0xA72D03CE, # SH T5, 0x03CE (T9) + 0xA32A05CF, # SB T2, 0x05CF (T9) + 0x240EE074, # ADDIU T6, R0, 0xE074 + 0xA72E05D2, # SH T6, 0x05D2 (T9) + 0x03200008, # JR T9 + # Disable the costume and Hard Mode flag checks so that pressing Up on the Player Select screen will always allow + # the characters' alternate costumes to be used as well as Hard Mode being selectable without creating save data. + 0x2409012E, # ADDIU T1, R0, 0x012E + 0x1509000A, # BNE T0, T1, [forward 0x0A] + 0x3C0A3C0B, # LUI T2, 0x3C0B + 0x254A8000, # ADDIU T2, T2, 0x8000 + 0x240B240E, # ADDIU T3, R0, 0x240E + 0x240C240F, # ADDIU T4, R0, 0x240F + 0x240D0024, # ADDIU T5, R0, 0x0024 + 0xAF2A0C78, # SW T2, 0x0C78 (T9) + 0xA72B0CA0, # SH T3, 0x0CA0 (T9) + 0xA72C0CDC, # SH T4, 0x0CDC (T9) + 0xA32D0168, # SB T5, 0x0024 (T9) + 0x03200008, # JR T9 + # Overwrite instructions in the Forest end cutscene script to store a spawn position ID instead of a cutscene ID. + 0x2409002E, # ADDIU T1, R0, 0x002E + 0x15090005, # BNE T0, T1, [forward 0x05] + 0x3C0AA058, # LUI T2, 0xA058 + 0x254A642B, # ADDIU T2, T2, 0x642B + 0xAF2A0D88, # SW T2, 0x0D88 (T9) + 0xAF200D98, # SW R0, 0x0D98 (T9) + 0x03200008, # JR T9 + # Disable the rapid flashing effect in the CC planetarium cutscene to ensure it won't trigger seizures. + 0x2409003E, # ADDIU T1, R0, 0x003E + 0x1509000C, # BNE T0, T1, [forward 0x0C] + 0x00000000, # NOP + 0xAF200C5C, # SW R0, 0x0C5C + 0xAF200CD0, # SW R0, 0x0CD0 + 0xAF200C64, # SW R0, 0x0C64 + 0xAF200C74, # SW R0, 0x0C74 + 0xAF200C80, # SW R0, 0x0C80 + 0xAF200C88, # SW R0, 0x0C88 + 0xAF200C90, # SW R0, 0x0C90 + 0xAF200C9C, # SW R0, 0x0C9C + 0xAF200CB4, # SW R0, 0x0CB4 + 0xAF200CC8, # SW R0, 0x0CC8 + 0x03200008, # JR T9 + 0x24090134, # ADDIU T1, R0, 0x0134 + 0x15090005, # BNE T0, T1, [forward 0x05] + 0x340B8040, # ORI T3, R0, 0x8040 + 0x340CDD20, # ORI T4, R0, 0xDD20 + 0xA72B1D1E, # SH T3, 0x1D1E (T9) + 0xA72C1D22, # SH T4, 0x1D22 (T9) + 0x03200008, # JR T9 + # Make the Ice Trap model check branch properly + 0x24090125, # ADDIU T1, R0, 0x0125 + 0x15090003, # BNE T0, T1, [forward 0x03] + 0x3C0B3C19, # LUI T3, 0x3C19 + 0x356B803F, # ORI T3, T3, 0x803F + 0xAF2B04D0, # SW T3, 0x04D0 (T9) + 0x03200008 # JR T9 +] + +double_component_checker = [ + # When checking to see if a bomb component can be placed at a cracked wall, this will run if the code lands at the + # "no need to set 2" outcome to see if the other can be set. + + # Mandragora checker + 0x10400007, # BEQZ V0, [forward 0x07] + 0x3C0A8039, # LUI T2, 0x8039 + 0x31098000, # ANDI T1, T0, 0x8000 + 0x15200008, # BNEZ T1, [forward 0x08] + 0x91499C5D, # LBU T1, 0x9C5D (T2) + 0x11200006, # BEQZ T1, 0x80183938 + 0x00000000, # NOP + 0x10000007, # B [forward 0x07] + 0x31E90100, # ANDI T1, T7, 0x0100 + 0x15200002, # BNEZ T1, [forward 0x02] + 0x91499C5D, # LBU T1, 0x9C5D (T2) + 0x15200003, # BNEZ T1, [forward 0x03] + 0x3C198000, # LUI T9, 0x8000 + 0x27391590, # ADDIU T9, T9, 0x1590 + 0x03200008, # JR T9 + 0x24090001, # ADDIU T1, R0, 0x0001 + 0xA4E9004C, # SH T1, 0x004C (A3) + 0x3C190E00, # LUI T9, 0x0E00 + 0x273903E0, # ADDIU T9, T9, 0x03E0 + 0x03200008, # JR T9 + 0x00000000, # NOP + # Nitro checker + 0x10400007, # BEQZ V0, [forward 0x07] + 0x3C0A8039, # LUI T2, 0x8039 + 0x31694000, # ANDI T1, T3, 0x4000 + 0x15200008, # BNEZ T1, [forward 0x08] + 0x91499C5C, # LBU T1, 0x9C5C + 0x11200006, # BEQZ T1, [forward 0x06] + 0x00000000, # NOP + 0x1000FFF4, # B [backward 0x0B] + 0x914F9C18, # LBU T7, 0x9C18 (T2) + 0x31E90002, # ANDI T1, T7, 0x0002 + 0x1520FFEC, # BNEZ T1, [backward 0x13] + 0x91499C5C, # LBU T1, 0x9C5C (T2) + 0x1520FFEF, # BNEZ T1, [backward 0x15] + 0x00000000, # NOP + 0x1000FFE8, # B [backward 0x17] + 0x00000000, # NOP +] + +downstairs_seal_checker = [ + # This will run specifically for the downstairs crack to see if the seal has been removed before then deciding to + # let the player set the bomb components or not. An anti-dick measure, since there is a limited number of each + # component per world. + 0x14400004, # BNEZ V0, [forward 0x04] + 0x3C0A8039, # LUI T2, 0x8039 + 0x914A9C18, # LBU T2, 0x9C18 (T2) + 0x314A0001, # ANDI T2, T2, 0x0001 + 0x11400003, # BEQZ T2, [forward 0x03] + 0x3C198000, # LUI T9, 0x8000 + 0x27391448, # ADDIU T9, T9, 0x1448 + 0x03200008, # JR T9 + 0x3C190E00, # LUI T9, 0x0E00 + 0x273902B4, # ADDIU T9, T9, 0x02B4 + 0x03200008, # JR T9 + 0x00000000, # NOP +] + +map_data_modifiers = [ + # Overwrites the map data table on-the-fly after it loads and before the game reads it to load objects. Good for + # changing anything that is part of a compression chain in the ROM data, including some freestanding item IDs. + # Also jumps to the function that overwrites the "Restart this stage" data if entering through the back of a level. + + 0x08006DAA, # J 0x8001B6A8 + 0x00000000, # NOP + # Demo checker (if we're in a title demo, don't do any of this) + 0x3C028034, # LUI V0, 0x8034 + 0x9449244A, # LHU T1, 0x244A (V0) + 0x11200002, # BEQZ T1, [forward 0x02] + # Zero checker (if there are zeroes in the word at 0x8034244A, where the entity list address is stored, don't do + # any of this either) + 0x8C422B00, # LW V0, 0x2B00 (V0) + 0x03E00008, # JR RA + 0x00000000, # NOP + 0x14400002, # BNEZ V0, [forward 0x02] + 0x00000000, # NOP + 0x03E00008, # JR RA + 0x3C088039, # LUI T0, 0x8039 + 0x91199EE3, # LBU T9, 0x9EE3 (T0) + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + # Forest of Silence (replaces 1 invisible chicken) + 0x15000006, # BNEZ T0, [forward 0x06] + 0x340A0001, # ORI T2, R0, 0x0001 <- Werewolf plaque + 0xA44A01C8, # SH T2, 0x01C8 (V0) + 0x24090001, # ADDIU T1, R0, 0x0001 + 0x1139FFED, # BEQ T1, T9, [backward 0x12] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Villa front yard (replaces 1 moneybag and 2 beefs) + 0x24090003, # ADDIU T1, R0, 0x0003 + 0x15090008, # BNE T0, T1, [forward 0x08] + 0x340A0001, # ORI T2, R0, 0x0001 <- Fountain FL + 0x340B0001, # ORI T3, R0, 0x0001 <- Fountain RL + 0x340C001F, # ORI T4, R0, 0x0001 <- Dog food gate + 0xA44A0058, # SH T2, 0x0058 (V0) + 0xA44B0038, # SH T3, 0x0038 (V0) + 0xA44C0068, # SH T4, 0x0068 (V0) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Villa living area (Replaces 1 chicken, 1 knife, and 3 invisible Purifyings and assigns flags to the sub-weapons) + 0x24090005, # ADDIU T1, R0, 0x0005 + 0x15090025, # BNE T0, T1, [forward 0x25] + 0x340B0010, # ORI T3, R0, 0x0001 <- Hallway axe + 0xA44B00B8, # SH T3, 0x00B8 (V0) + 0x340A0001, # ORI T2, R0, 0x0001 <- Storeroom R + 0x340B0010, # ORI T3, R0, 0x0001 <- Hallway knife + 0x340C0001, # ORI T4, R0, 0x0001 <- Living Room painting + 0x340D0001, # ORI T5, R0, 0x0001 <- Dining Room vase + 0x340E0001, # ORI T6, R0, 0x0001 <- Archives table + 0xA44A0078, # SH T2, 0x0078 (V0) + 0xA44B00C8, # SH T3, 0x00C8 (V0) + 0xA44C0108, # SH T4, 0x0108 (V0) + 0xA44D0128, # SH T5, 0x0128 (V0) + 0xA44E0138, # SH T6, 0x0138 (V0) + 0x340A0000, # ORI T2, R0, 0x0000 <- Sub-weapons left flag half + 0xA44A009C, # SH T2, 0x009C (V0) + 0xA44A00AC, # SH T2, 0x00AC (V0) + 0xA44A00BC, # SH T2, 0x00BC (V0) + 0xA44A00CC, # SH T2, 0x00CC (V0) + 0x340A0000, # ORI T2, R0, 0x0000 <- Sub-weapons right flag halves + 0x240B0000, # ADDIU T3, R0, 0x0000 + 0x240C0000, # ADDIU T4, R0, 0x0000 + 0x240D0000, # ADDIU T5, R0, 0x0000 + 0xA44A00CA, # SH T2, 0x00CA (V0) + 0xA44B00BA, # SH T3, 0x00BA (V0) + 0xA44C009A, # SH T4, 0x009A (V0) + 0xA44D00AA, # SH T5, 0x00AA (V0) + 0x340A0001, # ORI T2, R0, 0x0001 <- Near bed + 0x340B0010, # ORI T3, R0, 0x0001 <- Storeroom L + 0x340C0001, # ORI T4, R0, 0x0001 <- Storeroom statue + 0x340D0001, # ORI T5, R0, 0x0001 <- Exit knight + 0x340E0001, # ORI T6, R0, 0x0001 <- Sitting room table + 0xA44A0048, # SH T2, 0x0078 (V0) + 0xA44B0088, # SH T3, 0x00C8 (V0) + 0xA44C00D8, # SH T4, 0x0108 (V0) + 0xA44D00F8, # SH T5, 0x0128 (V0) + 0xA44E0118, # SH T6, 0x0138 (V0) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Tunnel (replaces 1 invisible Cure Ampoule) + 0x24090007, # ADDIU T1, R0, 0x0007 + 0x1509000A, # BNE T0, T1, [forward 0x0A] + 0x340A0001, # ORI T2, R0, 0x0001 <- Twin arrow signs + 0xA44A0268, # SH T2, 0x0268 (V0) + 0x340A0001, # ORI T2, R0, 0x0001 <- Bucket + 0xA44A0258, # SH T2, 0x0258 (V0) + 0x240B0005, # ADDIU T3, R0, 0x0005 + 0xA04B0150, # SB T3, 0x0150 (V0) + 0x24090011, # ADDIU T1, R0, 0x0011 + 0x1139FFB0, # BEQ T1, T9, [backward 0x50] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Castle Center factory floor (replaces 1 moneybag, 1 jewel, and gives every lizard man coffin item a unique flag) + 0x2409000B, # ADDIU T1, R0, 0x000B + 0x15090016, # BNE T0, T1, [forward 0x16] + 0x340A001A, # ORI T2, R0, 0x001A <- Lizard coffin nearside mid-right + 0x340B0003, # ORI T3, R0, 0x0003 <- Lizard coffin nearside mid-left + 0xA44A00C8, # SH T2, 0x00C8 (V0) + 0xA44B00D8, # SH T3, 0x00D8 (V0) + 0x240A1000, # ADDIU T2, R0, 0x1000 + 0x240B2000, # ADDIU T3, R0, 0x2000 + 0x240C0400, # ADDIU T4, R0, 0x0400 + 0x240D0800, # ADDIU T5, R0, 0x0800 + 0x240E0200, # ADDIU T6, R0, 0x0200 + 0x240F0100, # ADDIU T7, R0, 0x0100 + 0xA44A009A, # SH T2, 0x009A (V0) + 0xA44B00AA, # SH T3, 0x00AA (V0) + 0xA44C00CA, # SH T4, 0x00CA (V0) + 0xA44D00BA, # SH T5, 0x00BA (V0) + 0xA44E00DA, # SH T6, 0x00DA (V0) + 0xA44F00EA, # SH T7, 0x00EA (V0) + 0x340A0017, # ORI T2, R0, 0x0017 <- Lizard coffin nearside mid-right + 0x340B000C, # ORI T3, R0, 0x000C <- Lizard coffin nearside mid-left + 0xA44A00A8, # SH T2, 0x00C8 (V0) + 0xA44B00E8, # SH T3, 0x00D8 (V0) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Duel Tower (replaces a flame on top of a rotating lion pillar with a White Jewel on the invisible bridge ledge) + 0x24090013, # ADDIU T1, R0, 0x0013 + 0x1509000F, # BNE T0, T1, [forward 0x0F] + 0x3C0A00B9, # LUI T2, 0x00BB + 0x254A012B, # ADDIU T2, T2, 0x012B + 0x3C0BFE2A, # LUI T3, 0xFE2A + 0x256B0027, # ADDIU T3, T3, 0x0027 + 0x3C0C0001, # LUI T4, 0x0001 + 0x3C0D0022, # LUI T5, 0x0022 + 0x25AD0100, # ADDIU T5, T5, 0x0100 + 0xAC4A0A80, # SW T2, 0x0AE0 (V0) + 0xAC4B0A84, # SW T3, 0x0AE4 (V0) + 0xAC4C0A88, # SW T4, 0x0AE8 (V0) + 0xAC4D0A8C, # SW T5, 0x0AEC (V0) + 0x24090001, # ADDIU T1, R0, 0x0001 + 0x1139FF87, # BEQ T1, T9, [backward 0x77] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Castle Keep outside (replaces 1 invisible Healing Kit and gives both invisible Healing Kits pickup flags) + 0x24090014, # ADDIU T1, R0, 0x0014 + 0x1509000A, # BNE T0, T1, [forward 0x0A] + 0x340A0001, # ORI T2, R0, 0x0001 <- Right flame + 0xA44A0058, # SH T2, 0x0058 (V0) + 0x240A0001, # ADDIU T2, R0, 0x0001 + 0x240B0002, # ADDIU T3, R0, 0x0002 + 0xA44A004A, # SH T2, 0x004A (V0) + 0xA44B005A, # SH T3, 0x005A (V0) + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x1139FF7B, # BEQ T0, T1, [backward 0x74] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Castle Wall main area (sets a flag for the freestanding Holy Water if applicable and the "beginning of stage" + # state if entered from the rear) + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x15090006, # BNE T0, T1, [forward 0x06] + 0x24090000, # ADDIU T1, R0, 0x0000 + 0xA049009B, # SB T1, 0x009B (V0) + 0x24090010, # ADDIU T1, R0, 0x0010 + 0x1139FF73, # BEQ T1, T9, [backward 0x8D] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Villa vampire crypt (sets the "beginning of stage" state if entered from the rear, as well as the "can warp here" + # flag if arriving for the first time) + 0x2409001A, # ADDIU T1, R0, 0x001A + 0x15090008, # BNE T0, T1, [forward 0x08] + 0x3C0A8039, # LUI T2, 0x8039 + 0x914B9C1C, # LBU T3, 0x9C1C (T2) + 0x356B0001, # ORI T3, T3, 0x0001 + 0xA14B9C1C, # SB T3, 0x9C1C (T2) + 0x24090003, # ADDIU T1, R0, 0x0003 + 0x1139FF69, # BEQ T1, T9, [backward 0x98] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Underground Waterway (sets the "beginning of stage" state if entered from the rear) + 0x24090008, # ADDIU T1, R0, 0x0008 + 0x15090004, # BNE T0, T1, [forward 0x04] + 0x24090001, # ADDIU T1, R0, 0x0001 + 0x1139FF63, # BEQ T1, T9, [backward 0x9F] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Castle Center elevator top (sets the "beginning of stage" state if entered from either rear, as well as the "can + # warp here" flag if arriving for the first time) + 0x2409000F, # ADDIU T1, R0, 0x000F + 0x1509000A, # BNE T0, T1, [forward 0x0A] + 0x3C0A8039, # LUI T2, 0x8039 + 0x914B9C1C, # LBU T3, 0x9C1C (T2) + 0x356B0002, # ORI T3, T3, 0x0002 + 0xA14B9C1C, # SB T3, 0x9C1C (T2) + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x1139FF59, # BEQ T1, T9, [backward 0xAA] + 0x24090003, # ADDIU T1, R0, 0x0003 + 0x1139FF57, # BEQ T1, T9, [backward 0xAC] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Tower of Execution (sets the "beginning of stage" state if entered from the rear) + 0x24090010, # ADDIU T1, R0, 0x0010 + 0x15090004, # BNE T0, T1, [forward 0x10] + 0x24090012, # ADDIU T1, R0, 0x0012 + 0x1139FF51, # BEQ T1, T9, [backward 0xAF] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Tower of Sorcery (sets the "beginning of stage" state if entered from the rear) + 0x24090011, # ADDIU T1, R0, 0x0011 + 0x15090004, # BNE T0, T1, [forward 0x04] + 0x24090013, # ADDIU T1, R0, 0x0013 + 0x1139FF4B, # BEQ T1, T9, [backward 0xBA] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Tower of Science (sets the "beginning of stage" state if entered from the rear) + 0x24090012, # ADDIU T1, R0, 0x0012 + 0x15090004, # BNE T0, T1, [forward 0x04] + 0x24090004, # ADDIU T1, R0, 0x0004 + 0x1139FF45, # BEQ T1, T9, [backward 0xC1] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Room of Clocks (changes 2 candle settings if applicable and sets the "begging of stage" state if spawning at end) + 0x2409001B, # ADDIU T1, R0, 0x001B + 0x15090008, # BNE T0, T1, [forward 0x08] + 0x24090006, # ADDIU T1, R0, 0x0006 + 0x240A0006, # ADDIU T2, R0, 0x0006 + 0xA0490059, # SB T1, 0x0059 (V0) + 0xA04A0069, # SB T2, 0x0069 (V0) + 0x24090014, # ADDIU T1, R0, 0x0014 + 0x1139FF3B, # BEQ T1, T9, [backward 0xCC] + 0x00000000, # NOP + 0x03E00008, # JR RA + # Castle Center basement (changes 2 non-pickup-able Mandragoras into 2 real items and moves the torture shelf item + # forward slightly if it's turned visible) + 0x24090009, # ADDIU T1, R0, 0x0009 + 0x15090011, # BNE T0, T1, [forward 0x11] + 0x3409FFFC, # ORI T1, R0, 0xFFFC + 0xA44907C0, # SH T1, 0x07C0 (V0) + 0xA44907D0, # SH T1, 0x07D0 (V0) + 0x240A0027, # ADDIU T2, R0, 0x0027 + 0xA44A07C6, # SH T2, 0x07C6 (V0) + 0xA44A07D6, # SH T2, 0x07D6 (V0) + 0x340B0001, # ORI T3, R0, 0x0001 <- Right Mandragora + 0x340C0001, # ORI T4, R0, 0x0001 <- Left Mandragora + 0xA44B07C8, # SH T3, 0x07C8 (V0) + 0xA44C07D8, # SH T4, 0x07D8 (V0) + 0x240D00F5, # ADDIU T5, R0, 0x00F5 + 0xA04D06D1, # SB T5, 0x06D1 (V0) + 0x24090040, # ADDIU T1, R0, 0x0040 + 0x240A0080, # ADDIU T2, R0, 0x0080 + 0xA04907CA, # SB T1, 0x07CA (V0) + 0xA04A07DA, # SB T2, 0x07DA (V0) + 0x03E00008, # JR RA + # Castle Center nitro area (changes 2 non-pickup-able Nitros into 2 real items) + 0x2409000E, # ADDIU T1, R0, 0x000E + 0x15090015, # BNE T0, T1, [forward 0x15] + 0x240900C0, # ADDIU T1, R0, 0x00C0 + 0x240A00CE, # ADDIU T2, R0, 0x00CE + 0xA0490471, # SB T1, 0x0471 (V0) + 0xA04A04A1, # SB T2, 0x04A1 (V0) + 0x24090027, # ADDIU T1, R0, 0x0027 + 0xA4490476, # SH T1, 0x0476 (V0) + 0xA44904A6, # SH T1, 0x04A6 (V0) + 0x340A0001, # ORI T2, R0, 0x0001 <- Invention-side shelf + 0x340B0001, # ORI T3, R0, 0x0001 <- Heinrich-side shelf + 0xA44A0478, # SH T2, 0x0478 (V0) + 0xA44B04A8, # SH T3, 0x04A8 (V0) + 0x24090080, # ADDIU T1, R0, 0x0080 + 0xA049047A, # SB T1, 0x047A (V0) + 0xA440047C, # SH R0, 0x047C (V0) + 0x240A0400, # ADDIU T2, R0, 0x0400 + 0x340BFF05, # ORI T3, R0, 0xFF05 + 0xA44A04AA, # SH T2, 0x04AA (V0) + 0xA44B04AC, # SH T3, 0x04AC (V0) + 0x24090046, # ADDIU T1, R0, 0x0046 + 0xA04904A3, # SB T1, 0x04A3 (V0) + 0x03E00008, # JR RA + # Fan meeting room (sets "beginning of stage" flag) + 0x24090019, # ADDIU T1, R0, 0x0019 + 0x1109FF0D, # BEQ T1, T9, [backward 0xFB] + 0x00000000, # NOP + 0x03E00008, # JR RA +] + +renon_cutscene_checker = [ + # Prevents Renon's departure/pre-fight cutscene from playing if the player is either in the escape sequence or both + # did not spend the required 30K to fight him and lacks the required Special2s to fight Dracula. + 0x15810002, # BNE T4, AT, [forward 0x02] + 0x00000000, # NOP + 0x08049EB3, # J 0x80127ACC + 0x24090016, # ADDIU T1, R0, 0x0016 + 0x11C90002, # BEQ T6, T1, [forward 0x02] + 0x00000000, # NOP + 0x08049ECA, # J 0x80127B28 + 0x24190000, # ADDIU T9, R0, 0x0000 + 0x8C696208, # LW T1, 0x6208 (V1) + 0x292A7531, # SLTI T2, T1, 0x7531 + 0x51400001, # BEQZL T2, [forward 0x01] + 0x24190001, # ADDIU T9, R0, 0x0001 + 0x3C0B8013, # LUI T3, 0x8013 + 0x916BAC9F, # LBU T3, 0xAC9F (T3) + 0x906C6194, # LBU T4, 0x6194 (V1) + 0x018B502A, # SLT T2, T4, T3 + 0x51400001, # BEQZL T2, [forward 0x01] + 0x24190001, # ADDIU T9, R0, 0x0001 + 0x90696142, # LBU T1, 0x6142 (V1) + 0x31290002, # ANDI T1, T1, 0x0002 + 0x55200001, # BNEZL T1, [forward 0x01] + 0x24190000, # ADDIU T9, R0, 0x0000 + 0x17200003, # BNEZ T9, [forward 0x03] + 0x00000000, # NOP + 0x08049ECC, # J 0x80127B30 + 0x00000000, # NOP + 0x08049ECA # J 0x80127B28 +] + +renon_cutscene_checker_jr = [ + # Like renon_cutscene_checker, but without the checks for the Special2 and spent money counters. Inserted instead if + # the player chooses to guarantee or disable the Renon fight on their YAML. + 0x15810002, # BNE T4, AT, [forward 0x02] + 0x00000000, # NOP + 0x08049EB3, # J 0x80127ACC + 0x24090016, # ADDIU T1, R0, 0x0016 + 0x11C90002, # BEQ T6, T1, [forward 0x02] + 0x00000000, # NOP + 0x08049ECA, # J 0x80127B28 + 0x24190001, # ADDIU T9, R0, 0x0001 + 0x90696142, # LBU T1, 0x6142 (V1) + 0x31290002, # ANDI T1, T1, 0x0002 + 0x55200001, # BNEZL T1, [forward 0x01] + 0x24190000, # ADDIU T9, R0, 0x0000 + 0x17200003, # BNEZ T9, [forward 0x03] + 0x00000000, # NOP + 0x08049ECC, # J 0x80127B30 + 0x00000000, # NOP + 0x08049ECA # J 0x80127B28 +] + +ck_door_music_player = [ + # Plays Castle Keep's song if you spawn in front of Dracula's door (teleporting via the warp menu) and haven't + # started the escape sequence yet. + 0x17010002, # BNE T8, AT, [forward 0x02] + 0x00000000, # NOP + 0x08063DF9, # J 0x8018F7E4 + 0x240A0000, # ADDIU T2, R0, 0x0000 + 0x3C088039, # LUI T0, 0x8039 + 0x91089BFA, # LBU T0, 0x9BFA (T0) + 0x31080002, # ANDI T0, T0, 0x0002 + 0x51090001, # BEQL T0, T1, [forward 0x01] + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x24080003, # ADDIU T0, R0, 0x0003 + 0x51180001, # BEQL T0, T8, [forward 0x01] + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x240B0002, # ADDIU T3, R0, 0x0002 + 0x114B0002, # BEQ T2, T3, [forward 0x02] + 0x00000000, # NOP + 0x08063DFD, # J 0x8018F7F4 + 0x00000000, # NOP + 0x08063DF9 # J 0x8018F7E4 +] + +dracula_door_text_redirector = [ + # Switches the standard pointer to the map text with one to a custom message for Dracula's chamber door if the + # current scene is Castle Keep exterior (Scene 0x14). + 0x3C088039, # LUI T0, 0x8039 + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + 0x24090014, # ADDIU T1, R0, 0x0014 + 0x15090006, # BNE T0, T1, [forward 0x06] + 0x3C088014, # LUI T0, 0x8014 + 0x2508B9F4, # ADDIU T0, T0, 0xB9F4 + 0x151F0003, # BNE T0, RA, [forward 0x03] + 0x00000000, # NOP + 0x3C028040, # LUI V0, 0x8040 + 0x2442CC48, # ADDIU V0, V0, 0xCC48 + 0x03E00008 # JR RA +] + +coffin_time_checker = [ + # When entering the Villa coffin, this will check to see whether it's day or night and send you to either the Tunnel + # or Underground Waterway level slot accordingly regardless of which character you are + 0x28490006, # SLTI T1, V0, 0x0006 + 0x15200005, # BNEZ T1, [forward 0x05] + 0x28490012, # SLTI T1, V0, 0x0012 + 0x11200003, # BEQZ T1, [forward 0x03] + 0x00000000, # NOP + 0x08055AEB, # J 0x80156BAC + 0x00000000, # NOP + 0x08055AED # J 0x80156BB4 +] + +werebull_flag_unsetter = [ + # This will un-set Were-bull's defeat flag in Duel Tower after beating him so that the check above his arena can + # still be acquired later, if it hasn't been acquired already. This is the only check in the entire game that can be + # permanently missed even with the ability to return to levels. + 0x3C0E0400, # LUI T6, 0x0400 + 0x15CF0006, # BNE T6, T7, [forward 0x06] + 0x00187402, # SRL T6, T8, 16 + 0x31CE2000, # ANDI T6, T6, 0x2000 + 0x15C00003, # BNEZ T6, [forward 0x03] + 0x3C0E0020, # LUI T6, 0x0020 + 0x014E5025, # OR T2, T2, T6 + 0xAC4A613C, # SW T2, 0x613C (V0) + 0x03200008 # JR T9 +] + +werebull_flag_unsetter_special2_electric_boogaloo = [ + # Like werebull_flag_unsetter, but with the added feature of awarding a Special2 after determining the player isn't + # trying to beat Were-bull twice! This will be inserted over the former if the goal is set to boss hunt. + 0x3C0E0400, # LUI T6, 0x0400 + 0x15CF0008, # BNE T6, T7, [forward 0x06] + 0x00187402, # SRL T6, T8, 16 + 0x31CE2000, # ANDI T6, T6, 0x2000 + 0x15C00005, # BNEZ T6, [forward 0x05] + 0x3C0E0020, # LUI T6, 0x0020 + 0x014EC024, # AND T8, T2, T6 + 0x014E5025, # OR T2, T2, T6 + 0xAC4A613C, # SW T2, 0x613C (V0) + 0x17000003, # BNEZ T8, [forward 0x03] + 0x3C188039, # LUI T8, 0x8039 + 0x240E0005, # ADDIU T6, R0, 0x0005 + 0xA30E9BDF, # SB T6, 0x9BDF (T8) + 0x03200008 # JR T9 +] + +werebull_flag_pickup_setter = [ + # Checks to see if an item being picked up is the one on top of Were-bull's arena. If it is, then it'll check to see + # if our makeshift "Were-bull defeated once" flag and, if it is, set Were-bull's arena flag proper, so it'll + # permanently stay down. + 0x3C088038, # LUI T0, 0x8038 + 0x25083AC8, # ADDIU T0, T0, 0x3AC8 + 0x15020007, # BNE T0, V0, [forward 0x07] + 0x3C082000, # LUI T0, 0x2000 + 0x15040005, # BNE T0, A0, [forward 0x05] + 0x9449612C, # LHU T1, 0x612C (V0) + 0x31290020, # ANDI T1, T1, 0x0020 + 0x11200002, # BEQZ T1, [forward 0x02] + 0x3C0A0400, # LUI T2, 0x0400 + 0x014D6825, # OR T5, T2, T5 + 0xAC4D612C, # SW T5, 0x612C (V0) + 0x03E00008 # JR RA +] + +boss_special2_giver = [ + # Enables the rewarding of Special2s upon the vanishing of a boss's health bar when defeating it. + + # Also sets a flag in the case of the Castle Wall White Dragons' health bar going away. Their defeat flag in vanilla + # is tied to hitting the lever after killing them, so this alternate flag is used to track them for the "All Bosses" + # goal in the event someone kills them and then warps out opting to not be a Konami pachinko champ. + 0x3C118035, # LUI S1, 0x8035 + 0x962DF834, # LHU T5, 0xF834 (S1) + 0x240E3F73, # ADDIU T6, R0, 0x3F73 + 0x15AE0012, # BNE T5, T6, [forward 0x12] + 0x3C118039, # LUI S1, 0x8039 + 0x922D9EE1, # LBU T5, 0x9EE1 (S1) + 0x240E0013, # ADDIU T6, R0, 0x0013 + 0x11AE000E, # BEQ T5, T6, [forward 0x0E] + 0x922F9BFA, # LBU T7, 0x9BFA (S1) + 0x31EF0001, # ANDI T7, T7, 0x0001 + 0x15E0000B, # BNEZ T7, [forward 0x0B] + 0x240E0002, # ADDIU T6, R0, 0x0002 + 0x15AE0006, # BNE T5, T6, [forward 0x06] + 0x00000000, # NOP + 0x862F9BF4, # LH T7, 0x9BF4 (S1) + 0x31ED0080, # ANDI T5, T7, 0x0080 + 0x15A00005, # BNEZ T5, [forward 0x05] + 0x35EF0080, # ORI T7, T7, 0x0080 + 0xA62F9BF4, # SH T7, 0x9BF4 (S1) + 0x240D0005, # ADDIU T5, R0, 0x0005 + 0xA22D9BDF, # SB T5, 0x9BDF (S1) + 0xA22D9BE0, # SB T5, 0x9BE0 (S1) + 0x03E00008 # JR RA +] + +boss_goal_checker = [ + # Checks each boss flag to see if every boss with a health meter has been defeated and puts 0x0004 in V0 to + # disallow opening Dracula's door if not all have been. + 0x3C0A8039, # LUI T2, 0x8039 + 0x954B9BF4, # LHU T3, 0x9BF4 (T2) + 0x316D0BA0, # ANDI T5, T3, 0x0BA0 + 0x914B9BFB, # LBU T3, 0x9BFB (T2) + 0x000B6182, # SRL T4, T3, 6 + 0x11800010, # BEQZ T4, [forward 0x10] + 0x240C00C0, # ADDIU T4, R0, 0x00C0 + 0x01AC6821, # ADDU T5, T5, T4 + 0x914B9BFD, # LBU T3, 0x9BFD (T2) + 0x316C0020, # ANDI T4, T3, 0x0020 + 0x01AC6821, # ADDU T5, T5, T4 + 0x914B9BFE, # LBU T3, 0x9BFE (T2) + 0x316C0010, # ANDI T4, T3, 0x0010 + 0x01AC6821, # ADDU T5, T5, T4 + 0x914B9C18, # LBU T3, 0x9C18 (T2) + 0x316C0010, # ANDI T4, T3, 0x0010 + 0x01AC6821, # ADDU T5, T5, T4 + 0x914B9C1B, # LBU T3, 0x9C1B (T2) + 0x000B6102, # SRL T4, T3, 4 + 0x11800005, # BEQZ T4, [forward 0x05] + 0x240C0050, # ADDIU T4, R0, 0x0050 + 0x01AC6821, # ADDU T5, T5, T4 + 0x240E0CF0, # ADDIU T6, R0, 0x0CF0 + 0x55CD0001, # BNEL T6, T5, [forward 0x01] + 0x24020004, # ADDIU V0, R0, 0x0004 + 0x03E00008 # JR RA +] + +special_goal_checker = [ + # Checks the Special2 counter to see if the specified threshold has been reached and puts 0x0001 in V0 to disallow + # opening Dracula's door if it hasn't been. + 0x3C0A8039, # LUI T2, 0x8039 + 0x914B9C4C, # LBU T3, 0x9C4C (T2) + 0x296A001E, # SLTI T2, T3, 0x001E + 0x55400001, # BNEZL T2, 0x8012AC8C + 0x24020001, # ADDIU V0, R0, 0x0001 + 0x03E00008 # JR RA +] + +warp_menu_rewrite = [ + # Rewrite to the warp menu code to ensure each option can have its own scene ID, spawn ID, and fade color. + # Start Warp + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x1000001F, # B [forward 0x1F] + 0x3C0F8000, # LUI T7, 0x8000 + # Warp 1 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x1000001B, # B [forward 0x1B] + 0x3C0F8040, # LUI T7, 0x8040 + # Warp 2 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x10000017, # B [forward 0x17] + 0x3C0F8080, # LUI T7, 0x8080 + # Warp 3 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x10000013, # B [forward 0x13] + 0x3C0F0080, # LUI T7, 0x0080 + # Warp 4 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x3C0F0080, # LUI T7, 0x0080 + 0x1000000E, # B [forward 0x0E] + 0x25EF8000, # ADDIU T7, T7, 0x8000 + # Warp 5 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x1000000A, # B [forward 0x0A] + 0x340F8000, # ORI T7, R0, 0x8000 + # Warp 6 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x3C0F8000, # LUI T7, 0x8000 + 0x10000005, # B [forward 0x05] + 0x35EF8000, # ORI T7, T7, 0x8000 + # Warp 7 + 0x3C0E0000, # LUI T6, 0x0000 + 0x25CE0000, # ADDIU T6, T6, 0x0000 + 0x3C0F8040, # LUI T7, 0x8040 + 0x35EF8000, # ORI T7, T7, 0x8000 + # Warp Crypt + 0x3C18800D, # LUI T8, 0x800D + 0x97185E20, # LHU T8, 0x5E20 (T8) + 0x24192000, # ADDIU T9, R0, 0x2000 + 0x17190009, # BNE T8, T9, [forward 0x09] + 0x3C088039, # LUI T0, 0x8039 + 0x91089C1C, # LBU T0, 0x9C1C (T0) + 0x31080001, # ANDI T0, T0, 0x0001 + 0x1100000F, # BEQZ T0, [forward 0x0F] + 0x00000000, # NOP + 0x3C0E001A, # LUI T6, 0x001A + 0x25CE0003, # ADDIU T6, T6, 0x0003 + 0x1000000B, # B [forward 0x0B] + 0x240F0000, # ADDIU T7, R0, 0x0000 + # Warp Elevator + 0x24190010, # ADDIU T9, R0, 0x0010 + 0x17190008, # BNE T8, T9, [forward 0x08] + 0x91089C1C, # LBU T0, 0x9C1C (T0) + 0x31080002, # ANDI T0, T0, 0x0002 + 0x11000005, # BEQZ T0, [forward 0x05] + 0x00000000, # NOP + 0x3C0E000F, # LUI T6, 0x000F + 0x25CE0001, # ADDIU T6, T6, 0x0001 + 0x3C0F8080, # LUI T7, 0x8080 + 0x35EF8000, # ORI T7, T7, 0x8000 + # All + 0xAC6E6428, # SW T6, 0x6428 (V1) + 0xAC6F642C, # SW T7, 0x642C (V1) + 0x2402001E, # ADDIU V0, R0, 0x001E + 0xA4626430, # SH V0, 0x6430 (V1) + 0xA4626432, # SH V0, 0x6432 (V1) +] + +warp_pointer_table = [ + # Changed pointer table addresses to go with the warp menu rewrite + 0x8012AD74, + 0x8012AD84, + 0x8012AD94, + 0x8012ADA4, + 0x8012ADB4, + 0x8012ADC8, + 0x8012ADD8, + 0x8012ADEC, +] + +spawn_coordinates_extension = [ + # Checks if the 0x10 bit is set in the spawn ID and references the below list of custom spawn coordinates if it is. + 0x316A0010, # ANDI T2, T3, 0x0010 + 0x11400003, # BEQZ T2, [forward 0x03] + 0x8CD90008, # LW T9, 0x0008 (A2) + 0x3C198040, # LUI T9, 0x8040 + 0x2739C2CC, # ADDIU T9, T9, 0xC2CC + 0x08054A83, # J 0x80152A0C + 0x00000000, # NOP + 0x00000000, # NOP + + # Castle Wall end: 10 + # player camera focus point + # x = 0xFFFF 0xFFFF 0xFFFF + # y = 0x0003 0x0012 0x000D + # z = 0xFFF3 0xEDFF 0xFFF3 + # r = 0xC000 + 0x0000FFFF, + 0x0003FFF3, + 0xC000FFFF, + 0x0012FFED, + 0xFFFF000D, + 0xFFF30000, + + # Tunnel end: 11 + # player camera focus point + # x = 0x0088 0x0087 0x0088 + # y = 0x01D6 0x01F1 0x01E5 + # z = 0xF803 0xF7D2 0xF803 + # r = 0xC000 + 0x008801D6, + 0xF803C000, + 0x008701F1, + 0xF7D20088, + 0x01E5F803, + + # Tower of Execution end: 12 + # player camera focus point + # x = 0x00AC 0x00EC 0x00AC + # y = 0x0154 0x0183 0x0160 + # z = 0xFE8F 0xFE8F 0xFE8F + # r = 0x8000 + 0x000000AC, + 0x0154FE8F, + 0x800000EC, + 0x0183FE8F, + 0x00AC0160, + 0xFE8F0000, + + # Tower of Sorcery end: 13 + # player camera focus point + # x = 0xFEB0 0xFE60 0xFEB0 + # y = 0x0348 0x036D 0x0358 + # z = 0xFEFB 0xFEFB 0xFEFB + # r = 0x0000 + 0xFEB00348, + 0xFEFB0000, + 0xFE60036D, + 0xFEFBFEB0, + 0x0358FEFB, + + # Room of Clocks end: 14 + # player camera focus point + # x = 0x01B1 0x01BE 0x01B1 + # y = 0x0006 0x001B 0x0015 + # z = 0xFFCD 0xFFCD 0xFFCD + # r = 0x8000 + 0x000001B1, + 0x0006FFCD, + 0x800001BE, + 0x001BFFCD, + 0x01B10015, + 0xFFCD0000, + + # Duel Tower savepoint: 15 + # player camera focus point + # x = 0x00B9 0x00B9 0x00B9 + # y = 0x012B 0x0150 0x0138 + # z = 0xFE20 0xFE92 0xFE20 + # r = 0xC000 + 0x00B9012B, + 0xFE20C000, + 0x00B90150, + 0xFE9200B9, + 0x0138FE20 +] + +waterway_end_coordinates = [ + # Underground Waterway end: 01 + # player camera focus point + # x = 0x0397 0x03A1 0x0397 + # y = 0xFFC4 0xFFDC 0xFFD3 + # z = 0xFDB9 0xFDB8 0xFDB9 + # r = 0x8000 + 0x00000397, + 0xFFC4FDB9, + 0x800003A1, + 0xFFDCFDB8, + 0x0397FFD3, + 0xFDB90000 +] + +continue_cursor_start_checker = [ + # This is used to improve the Game Over screen's "Continue" menu by starting the cursor on whichever checkpoint + # is most recent instead of always on "Previously saved". If a menu has a cursor start value of 0xFF in its text + # data, this will read the byte at 0x80389BC0 to determine which option to start the cursor on. + 0x8208001C, # LB T0, 0x001C(S0) + 0x05010003, # BGEZ T0, [forward 0x03] + 0x3C098039, # LUI T1, 0x8039 + 0x81289BC0, # LB T0, 0x9BC0 (T1) + 0xA208001C, # SB T0, 0x001C (S0) + 0x03E00008 # JR RA +] + +savepoint_cursor_updater = [ + # Sets the value at 0x80389BC0 to 0x00 after saving to let the Game Over screen's "Continue" menu know to start the + # cursor on "Previously saved" as well as updates the entrance variable for B warping. It then jumps to + # deathlink_counter_decrementer in the event we're loading a save from the Game Over screen. + 0x3C088039, # LUI T0, 0x8039 + 0x91099C95, # LBU T1, 0x9C95 (T0) + 0x000948C0, # SLL T1, T1, 3 + 0x3C0A8018, # LUI T2, 0x8018 + 0x01495021, # ADDU T2, T2, T1 + 0x914B17CF, # LBU T3, 0x17CF (T2) + 0xA10B9EE3, # SB T3, 0x9EE3 (T0) + 0xA1009BC0, # SB R0, 0x9BC0 (T0) + 0x080FF8F0 # J 0x803FE3C0 +] + +stage_start_cursor_updater = [ + # Sets the value at 0x80389BC0 to 0x01 after entering a stage to let the Game Over screen's "Continue" menu know to + # start the cursor on "Restart this stage". + 0x3C088039, # LUI T0, 0x8039 + 0x24090001, # ADDIU T1, R0, 0x0001 + 0xA1099BC0, # SB T1, 0x9BC0 (T0) + 0x03E00008 # JR RA +] + +elevator_flag_checker = [ + # Prevents the top elevator in Castle Center from activating if the bottom elevator switch is not turned on. + 0x3C088039, # LUI T0, 0x8039 + 0x91089C07, # LBU T0, 0x9C07 (T0) + 0x31080002, # ANDI T0, T0, 0x0002 + 0x15000002, # BNEZ T0, [forward 0x02] + 0x848E004C, # LH T6, 0x004C (A0) + 0x240E0000, # ADDIU T6, R0, 0x0000 + 0x03E00008 # JR RA +] + +crystal_special2_giver = [ + # Gives a Special2 upon activating the big crystal in CC basement. + 0x3C098039, # LUI T1, 0x8039 + 0x24190005, # ADDIU T9, R0, 0x0005 + 0xA1399BDF, # SB T9, 0x9BDF (T1) + 0x03E00008, # JR RA + 0x3C198000 # LUI T9, 0x8000 +] + +boss_save_stopper = [ + # Prevents usage of a White Jewel if in a boss fight. Important for the lizard-man trio in Waterway as escaping + # their fight by saving/reloading can render a Special2 permanently missable. + 0x24080001, # ADDIU T0, R0, 0x0001 + 0x15030005, # BNE T0, V1, [forward 0x05] + 0x3C088035, # LUI T0, 0x8035 + 0x9108F7D8, # LBU T0, 0xF7D8 (T0) + 0x24090020, # ADDIU T1, R0, 0x0020 + 0x51090001, # BEQL T0, T1, [forward 0x01] + 0x24020000, # ADDIU V0, R0, 0x0000 + 0x03E00008 # JR RA +] + +music_modifier = [ + # Uses the ID of a song about to be played to pull a switcheroo by grabbing a new ID from a custom table to play + # instead. A hacky way to circumvent song IDs in the compressed overlays' "play song" function calls, but it works! + 0xAFBF001C, # SW RA, 0x001C (SP) + 0x0C004A6B, # JAL 0x800129AC + 0x44800000, # MTC1 R0, F0 + 0x10400003, # BEQZ V0, [forward 0x03] + 0x3C088040, # LUI T0, 0x8040 + 0x01044821, # ADDU T1, T0, A0 + 0x9124CD20, # LBU A0, 0xCD20 (T1) + 0x08004E64 # J 0x80013990 +] + +music_comparer_modifier = [ + # The same as music_modifier, but for the function that compares the "song to play" ID with the one that's currently + # playing. This will ensure the randomized music doesn't reset when going through a loading zone in Villa or CC. + 0x3C088040, # LUI T0, 0x8040 + 0x01044821, # ADDU T1, T0, A0 + 0x9124CD20, # LBU A0, 0xCD20 (T1) + 0x08004A60, # J 0x80012980 +] + +item_customizer = [ + # Allows changing an item's appearance settings and visibility independent of what it actually is as well as setting + # its bitflag literally anywhere in the save file by changing things in the item actor's data as it's being created + # for the below three functions to then utilize. + 0x03205825, # OR T3, T9, R0 + 0x000B5A02, # SRL T3, T3, 8 + 0x316C0080, # ANDI T4, T3, 0x0080 + 0xA0CC0041, # SB T4, 0x0041 (A2) + 0x016C5823, # SUBU T3, T3, T4 + 0xA0CB0040, # SB T3, 0x0040 (A2) + 0x333900FF, # ANDI T9, T9, 0x00FF + 0xA4D90038, # SH T9, 0x0038 (A2) + 0x8CCD0058, # LW T5, 0x0058 (A2) + 0x31ACFF00, # ANDI T4, T5, 0xFF00 + 0x340EFF00, # ORI T6, R0, 0xFF00 + 0x158E000A, # BNE T4, T6, [forward 0x0A] + 0x31AC00FF, # ANDI T4, T5, 0x00FF + 0x240E0002, # ADDIU T6, R0, 0x0002 + 0x018E001B, # DIVU T4, T6 + 0x00006010, # MFHI T4 + 0x000D5C02, # SRL T3, T5, 16 + 0x51800001, # BEQZL T4, [forward 0x01] + 0x000B5C00, # SLL T3, T3, 16 + 0x00006012, # MFLO T4 + 0xA0CC0055, # SB T4, 0x0055 (A2) + 0xACCB0058, # SW T3, 0x0058 (A2) + 0x080494E5, # J 0x80125394 + 0x032A0019 # MULTU T9, T2 +] + +item_appearance_switcher = [ + # Determines an item's model appearance by checking to see if a different item appearance ID was written in a + # specific spot in the actor's data; if one wasn't, then the appearance value will be grabbed from the item's entry + # in the item property table like normal instead. + 0x92080040, # LBU T0, 0x0040 (S0) + 0x55000001, # BNEZL T0, T1, [forward 0x01] + 0x01002025, # OR A0, T0, R0 + 0x03E00008, # JR RA + 0xAFA70024 # SW A3, 0x0024 (SP) +] + +item_model_visibility_switcher = [ + # If 80 is written one byte ahead of the appearance switch value in the item's actor data, parse 0C00 to the + # function that checks if an item should be invisible or not. Otherwise, grab that setting from the item property + # table like normal. + 0x920B0041, # LBU T3, 0x0041 (S0) + 0x316E0080, # ANDI T6, T3, 0x0080 + 0x11C00003, # BEQZ T6, [forward 0x03] + 0x240D0C00, # ADDIU T5, R0, 0x0C00 + 0x03E00008, # JR RA + 0x00000000, # NOP + 0x03E00008, # JR RA + 0x958D0004 # LHU T5, 0x0004 (T4) +] + +item_shine_visibility_switcher = [ + # Same as the above, but for item shines instead of the model. + 0x920B0041, # LBU T3, 0x0041 (S0) + 0x31690080, # ANDI T1, T3, 0x0080 + 0x11200003, # BEQZ T1, [forward 0x03] + 0x00000000, # NOP + 0x03E00008, # JR RA + 0x240C0C00, # ADDIU T4, R0, 0x0C00 + 0x03E00008, # JR RA + 0x958CA908 # LHU T4, 0xA908 (T4) +] + +three_hit_item_flags_setter = [ + # As the function to create items from the 3HB item lists iterates through said item lists, this will pass unique + # flag values to each item when calling the "create item instance" function by right-shifting said flag by a number + # of bits depending on which item in the list it is. Unlike the vanilla game which always puts flags of 0x00000000 + # on each of these. + 0x8DC80008, # LW T0, 0x0008 (T6) + 0x240A0000, # ADDIU T2, R0, 0x0000 + 0x00084C02, # SRL T1, T0, 16 + 0x3108FFFF, # ANDI T0, T0, 0xFFFF + 0x00094842, # SRL T1, T1, 1 + 0x15200003, # BNEZ T1, [forward 0x03] + 0x00000000, # NOP + 0x34098000, # ORI T1, R0, 0x8000 + 0x25080001, # ADDIU T0, T0, 0x0001 + 0x0154582A, # SLT T3, T2, S4 + 0x1560FFF9, # BNEZ T3, [backward 0x07] + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x00094C00, # SLL T1, T1, 16 + 0x01094025, # OR T0, T0, T1 + 0x0805971E, # J 0x80165C78 + 0xAFA80010 # SW T0, 0x0010 (SP) +] + +chandelier_item_flags_setter = [ + # Same as the above, but for the unique function made specifically and ONLY for the Villa foyer chandelier's item + # list. KCEK, why the heck did you have to do this!? + 0x8F280014, # LW T0, 0x0014 (T9) + 0x240A0000, # ADDIU T2, R0, 0x0000 + 0x00084C02, # SRL T1, T0, 16 + 0x3108FFFF, # ANDI T0, T0, 0xFFFF + 0x00094842, # SRL T1, T1, 1 + 0x15200003, # BNEZ T1, [forward 0x03] + 0x00000000, # NOP + 0x34098000, # ORI T1, R0, 0x8000 + 0x25080001, # ADDIU T0, T0, 0x0001 + 0x0155582A, # SLT T3, T2, S5 + 0x1560FFF9, # BNEZ T3, [backward 0x07] + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x00094C00, # SLL T1, T1, 16 + 0x01094025, # OR T0, T0, T1 + 0x0805971E, # J 0x80165C78 + 0xAFA80010 # SW T0, 0x0010 (SP) +] + +prev_subweapon_spawn_checker = [ + # When picking up a sub-weapon this will check to see if it's different from the one the player already had (if they + # did have one) and jump to prev_subweapon_dropper, which will spawn a subweapon actor of what they had before + # directly behind them. + 0x322F3031, # Previous sub-weapon bytes + 0x10A00009, # BEQZ A1, [forward 0x09] + 0x00000000, # NOP + 0x10AD0007, # BEQ A1, T5, [forward 0x07] + 0x3C088040, # LUI T0, 0x8040 + 0x01054021, # ADDU T0, T0, A1 + 0x0C0FF418, # JAL 0x803FD060 + 0x9104CFC3, # LBU A0, 0xCFC3 (T0) + 0x2484FF9C, # ADDIU A0, A0, 0xFF9C + 0x3C088039, # LUI T0, 0x8039 + 0xAD049BD4, # SW A0, 0x9BD4 (T0) + 0x0804F0BF, # J 0x8013C2FC + 0x24020001 # ADDIU V0, R0, 0x0001 +] + +prev_subweapon_fall_checker = [ + # Checks to see if a pointer to a previous sub-weapon drop actor spawned by prev_subweapon_dropper is in 80389BD4 + # and calls the function in prev_subweapon_dropper to lower the weapon closer to the ground on the next frame if a + # pointer exists and its actor ID is 0x0027. Once it hits the ground or despawns, the connection to the actor will + # be severed by 0-ing out the pointer. + 0x3C088039, # LUI T0, 0x8039 + 0x8D049BD4, # LW A0, 0x9BD4 (T0) + 0x10800008, # BEQZ A0, [forward 0x08] + 0x00000000, # NOP + 0x84890000, # LH T1, 0x0000 (A0) + 0x240A0027, # ADDIU T2, R0, 0x0027 + 0x152A0004, # BNE T1, T2, [forward 0x04] + 0x00000000, # NOP + 0x0C0FF452, # JAL 0x803FD148 + 0x00000000, # NOP + 0x50400001, # BEQZL V0, [forward 0x01] + 0xAD009BD4, # SW R0, 0x9BD4 (T0) + 0x080FF40F # J 0x803FD03C +] + +prev_subweapon_dropper = [ + # Spawns a pickup actor of the sub-weapon the player had before picking up a new one behind them at their current + # position like in other CVs. This will enable them to pick it back up again if they still want it. + # Courtesy of Moisés; see derp.c in the src folder for the C source code. + 0x27BDFFC8, + 0xAFBF001C, + 0xAFA40038, + 0xAFB00018, + 0x0C0006B4, + 0x2404016C, + 0x00402025, + 0x0C000660, + 0x24050027, + 0x1040002B, + 0x00408025, + 0x3C048035, + 0x848409DE, + 0x00042023, + 0x0C0230D4, + 0x3084FFFF, + 0x44822000, + 0x3C018040, + 0xC428D370, + 0x468021A0, + 0x3C048035, + 0x848409DE, + 0x00042023, + 0x46083282, + 0x3084FFFF, + 0x0C01FFAC, + 0xE7AA0024, + 0x44828000, + 0x3C018040, + 0xC424D374, + 0x468084A0, + 0x27A40024, + 0x00802825, + 0x3C064100, + 0x46049182, + 0x0C004562, + 0xE7A6002C, + 0x3C058035, + 0x24A509D0, + 0x26040064, + 0x0C004530, + 0x27A60024, + 0x3C018035, + 0xC42809D4, + 0x3C0140A0, + 0x44815000, + 0x00000000, + 0x460A4400, + 0xE6100068, + 0xC6120068, + 0xE6120034, + 0x8FAE0038, + 0xA60E0038, + 0x8FBF001C, + 0x8FB00018, + 0x27BD0038, + 0x03E00008, + 0x00000000, + 0x3C068040, + 0x24C6D368, + 0x90CE0000, + 0x27BDFFE8, + 0xAFBF0014, + 0x15C00027, + 0x00802825, + 0x240400DB, + 0x0C0006B4, + 0xAFA50018, + 0x44802000, + 0x3C038040, + 0x2463D364, + 0x3C068040, + 0x24C6D368, + 0x8FA50018, + 0x1040000A, + 0xE4640000, + 0x8C4F0024, + 0x3C013F80, + 0x44814000, + 0xC5E60044, + 0xC4700000, + 0x3C018040, + 0x46083280, + 0x460A8480, + 0xE432D364, + 0x94A20038, + 0x2401000F, + 0x24180001, + 0x10410006, + 0x24010010, + 0x10410004, + 0x2401002F, + 0x10410002, + 0x24010030, + 0x14410005, + 0x3C014040, + 0x44813000, + 0xC4640000, + 0x46062200, + 0xE4680000, + 0xA0D80000, + 0x10000023, + 0x24020001, + 0x3C038040, + 0x2463D364, + 0xC4600000, + 0xC4A20068, + 0x3C038039, + 0x24639BD0, + 0x4600103E, + 0x00001025, + 0x45000006, + 0x00000000, + 0x44808000, + 0xE4A00068, + 0xA0C00000, + 0x10000014, + 0xE4700000, + 0x3C038039, + 0x24639BD0, + 0x3C018019, + 0xC42AC870, + 0xC4600000, + 0x460A003C, + 0x00000000, + 0x45000006, + 0x3C018019, + 0xC432C878, + 0x46120100, + 0xE4640000, + 0xC4600000, + 0xC4A20068, + 0x46001181, + 0x24020001, + 0xE4A60068, + 0xC4A80068, + 0xE4A80034, + 0x8FBF0014, + 0x27BD0018, + 0x03E00008, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x0000001B, + 0x060048E0, + 0x40000000, + 0x06AEFFD3, + 0x06004B30, + 0x40000000, + 0x00000000, + 0x06004CB8, + 0x0000031A, + 0x002C0000, + 0x060059B8, + 0x40000248, + 0xFFB50186, + 0x06005B68, + 0xC00001DF, + 0x00000000, + 0x06005C88, + 0x80000149, + 0x00000000, + 0x06005DC0, + 0xC0000248, + 0xFFB5FE7B, + 0x06005F70, + 0xC00001E0, + 0x00000000, + 0x06006090, + 0x8000014A, + 0x00000000, + 0x06007D28, + 0x4000010E, + 0xFFF100A5, + 0x06007F60, + 0xC0000275, + 0x00000000, + 0x06008208, + 0x800002B2, + 0x00000000, + 0x060083B0, + 0xC000010D, + 0xFFF2FF5C, + 0x060085E8, + 0xC0000275, + 0x00000000, + 0x06008890, + 0x800002B2, + 0x00000000, + 0x3D4CCCCD, + 0x3FC00000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0xB8000100, + 0xB8000100, +] + +subweapon_surface_checker = [ + # During the process of remotely giving an item received via multiworld, this will check to see if the item being + # received is a subweapon and, if it is, wait until the player is not above an abyss or instant kill surface before + # giving it. This is to ensure dropped previous subweapons won't land somewhere inaccessible. + 0x2408000D, # ADDIU T0, R0, 0x000D + 0x11040006, # BEQ T0, A0, [forward 0x06] + 0x2409000E, # ADDIU T1, R0, 0x000E + 0x11240004, # BEQ T1, A0, [forward 0x04] + 0x2408000F, # ADDIU T0, R0, 0x000F + 0x11040002, # BEQ T0, A0, [forward 0x02] + 0x24090010, # ADDIU T1, R0, 0x0010 + 0x1524000B, # BNE T1, A0, [forward 0x0B] + 0x3C0A800D, # LUI T2, 0x800D + 0x8D4A7B5C, # LW T2, 0x7B5C (T2) + 0x1140000E, # BEQZ T2, [forward 0x0E] + 0x00000000, # NOP + 0x914A0001, # LBU T2, 0x0001 (T2) + 0x240800A2, # ADDIU T0, R0, 0x00A2 + 0x110A000A, # BEQ T0, T2, [forward 0x0A] + 0x24090092, # ADDIU T1, R0, 0x0092 + 0x112A0008, # BEQ T1, T2, [forward 0x08] + 0x24080080, # ADDIU T0, R0, 0x0080 + 0x110A0006, # BEQ T0, T2, [forward 0x06] + 0x956C00DD, # LHU T4, 0x00DD (T3) + 0xA1600000, # SB R0, 0x0000 (T3) + 0x258C0001, # ADDIU T4, T4, 0x0001 + 0x080FF8D0, # J 0x803FE340 + 0xA56C00DD, # SH T4, 0x00DD (T3) + 0x00000000, # NOP + 0x03E00008 # JR RA +] + +countdown_number_displayer = [ + # Displays a number below the HUD clock of however many items are left to find in whichever stage the player is in. + # Which number in the save file to display depends on which map the player is currently on. It can track either + # items marked progression only or all locations in the stage. + # Courtesy of Moisés; see print_text_ovl.c in the src folder for the C source code. + 0x27BDFFD8, + 0xAFBF0024, + 0x00002025, + 0x0C000360, + 0x2405000C, + 0x3C038040, + 0x3C198034, + 0x2463D6D0, + 0x37392814, + 0x240E0002, + 0x3C0F0860, + 0x24180014, + 0xAC620000, + 0xAFB80018, + 0xAFAF0014, + 0xAFAE0010, + 0xAFB9001C, + 0x00002025, + 0x00402825, + 0x2406001E, + 0x0C0FF55D, + 0x24070028, + 0x8FBF0024, + 0x3C018040, + 0xAC22D6D4, + 0x03E00008, + 0x27BD0028, + 0x27BDFFE0, + 0xAFA40020, + 0x93AE0023, + 0x3C058039, + 0xAFBF001C, + 0x3C048040, + 0x3C068040, + 0x240F0014, + 0x00AE2821, + 0x90A59CA4, + 0xAFAF0010, + 0x8CC6D6D0, + 0x8C84D6D4, + 0x0C0FF58A, + 0x24070002, + 0x8FBF001C, + 0x27BD0020, + 0x03E00008, + 0x00000000, + 0x00000000, + 0x00000000, + 0x90820000, + 0x00001825, + 0x50400008, + 0xA4A00000, + 0xA4A20000, + 0x90820001, + 0x24840001, + 0x24A50002, + 0x1440FFFB, + 0x24630001, + 0xA4A00000, + 0x03E00008, + 0x00601025, + 0x27BDFFD8, + 0xAFBF0024, + 0xAFB0001C, + 0xAFA40028, + 0xAFA5002C, + 0xAFB10020, + 0xAFA60030, + 0xAFA70034, + 0x00008025, + 0x24050064, + 0x0C000360, + 0x00002025, + 0x8FA40040, + 0x00408825, + 0x3C05800A, + 0x10800004, + 0x8FA6003C, + 0x0C04B2E2, + 0x8CA5B450, + 0x00408025, + 0x5200001A, + 0x8FBF0024, + 0x12200017, + 0x8FAE0028, + 0x11C00015, + 0x02002025, + 0x97A5002E, + 0x97A60032, + 0x0C04B33F, + 0x24070001, + 0x02002025, + 0x83A50037, + 0x87A6003A, + 0x00003825, + 0x0C04B345, + 0xAFA00010, + 0x8FA40028, + 0x0C0FF51C, + 0x02202825, + 0x0C006CF0, + 0x02202025, + 0x02002025, + 0x02202825, + 0x00003025, + 0x0C04B34E, + 0x00003825, + 0x8FBF0024, + 0x02001025, + 0x8FB0001C, + 0x8FB10020, + 0x03E00008, + 0x27BD0028, + 0x27BDFFD8, + 0x8FAE0044, + 0xAFB00020, + 0xAFBF0024, + 0xAFA40028, + 0xAFA5002C, + 0xAFA60030, + 0xAFA70034, + 0x11C00007, + 0x00008025, + 0x3C05800A, + 0x8CA5B450, + 0x01C02025, + 0x0C04B2E2, + 0x8FA6003C, + 0x00408025, + 0x12000017, + 0x8FAF002C, + 0x11E00015, + 0x02002025, + 0x97A50032, + 0x97A60036, + 0x0C04B33F, + 0x24070001, + 0x02002025, + 0x24050001, + 0x24060064, + 0x00003825, + 0x0C04B345, + 0xAFA00010, + 0x8FA40028, + 0x8FA5002C, + 0x93A6003B, + 0x0C04B5BD, + 0x8FA70040, + 0x02002025, + 0x8FA5002C, + 0x00003025, + 0x0C04B34E, + 0x00003825, + 0x8FBF0024, + 0x02001025, + 0x8FB00020, + 0x03E00008, + 0x27BD0028, + 0x27BDFFE8, + 0xAFBF0014, + 0xAFA40018, + 0xAFA5001C, + 0xAFA60020, + 0x10C0000B, + 0xAFA70024, + 0x00A02025, + 0x00C02825, + 0x93A60027, + 0x0C04B5BD, + 0x8FA70028, + 0x8FA20018, + 0x3C010100, + 0x8C4F0000, + 0x01E1C025, + 0xAC580000, + 0x8FBF0014, + 0x27BD0018, + 0x03E00008, + 0x00000000, + 0xAFA50004, + 0x1080000E, + 0x30A500FF, + 0x24010001, + 0x54A10008, + 0x8C980000, + 0x8C8E0000, + 0x3C017FFF, + 0x3421FFFF, + 0x01C17824, + 0x03E00008, + 0xAC8F0000, + 0x8C980000, + 0x3C018000, + 0x0301C825, + 0xAC990000, + 0x03E00008, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000 +] + +countdown_number_manager = [ + # Tables and code for managing things about the Countdown number at the appropriate times. + 0x00010102, # Map ID offset table start + 0x02020D03, + 0x04050505, + 0x0E0E0E05, + 0x07090806, + 0x0C0C000B, + 0x0C050D0A, + 0x00000000, # Table end + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000001, # Major identifiers table start + 0x01000000, + 0x00000000, + 0x00000000, + 0x01000000, + 0x01010000, + 0x00010101, + 0x01010101, + 0x01010101, + 0x01010000, + 0x00000000, # Table end + # Decrements the counter upon picking up an item if the counter should be decremented. + 0x90E80039, # LBU T0, 0x0039 (A3) + 0x240B0011, # ADDIU T3, R0, 0x0011 + 0x110B0002, # BEQ T0, T3, [forward 0x02] + 0x90EA0040, # LBU T2, 0x0040 (A3) + 0x2548FFFF, # ADDIU T0, T2, 0xFFFF + 0x3C098040, # LUI T1, 0x8040 + 0x01284821, # ADDIU T1, T1, T0 + 0x9129D71C, # LBU T1, 0xD71C (T1) + 0x11200009, # BEQZ T1, [forward 0x09] + 0x3C088039, # LUI T0, 0x8039 + 0x91099EE1, # LBU T1, 0x9EE1 (T0) + 0x3C0A8040, # LUI T2, 0x8040 + 0x01495021, # ADDU T2, T2, T1 + 0x914AD6DC, # LBU T2, 0xD6DC (T2) + 0x010A4021, # ADDU T0, T0, T2 + 0x91099CA4, # LBU T1, 0x9CA4 (T0) + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1099CA4, # SB T1, 0x9CA4 (T0) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Moves the number to/from its pause menu position when pausing/un-pausing. + 0x3C088040, # LUI T0, 0x8040 + 0x8D08D6D4, # LW T0, 0xD6D4 + 0x11000009, # BEQZ T0, [forward 0x09] + 0x92090000, # LBU T1, 0x0000 (S0) + 0x14200004, # BNEZ AT, [forward 0x04] + 0x3C0A0033, # LUI T2, 0x0033 + 0x254A001F, # ADDIU T2, T2, 0x001F + 0x03E00008, # JR RA + 0xAD0A0014, # SW T2, 0x0014 (T0) + 0x3C0A00D4, # LUI T2, 0x00D4 + 0x254A003C, # ADDIU T2, T2, 0x003C + 0xAD0A0014, # SW T2, 0x0014 (T0) + 0x03E00008, # JR RA + 0x00000000, # NOP + # Hides the number when going into a cutscene or the Options menu. + 0x3C048040, # LUI A0, 0x8040 + 0x8C84D6D4, # LW A0, 0xD6D4 (A0) + 0x0C0FF59F, # JAL 0x803FD67C + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x0804DFE0, # J 0x80137FB0 + 0x3C048000, # LUI A0, 0x8000 + 0x00000000, # NOP + # Un-hides the number when leaving a cutscene or the Options menu. + 0x3C048040, # LUI A0, 0x8040 + 0x8C84D6D4, # LW A0, 0xD6D4 (A0) + 0x0C0FF59F, # JAL 0x803FD67C + 0x24050001, # ADDIU A1, R0, 0x0000 + 0x0804DFFA, # J 0x8013 + 0x3C047FFF, # LUI A0, 0x7FFFF + 0x00000000, # NOP + # Kills the last map's pointer to the Countdown stuff. + 0x3C088040, # LUI T0, 0x8040 + 0xFD00D6D0, # SD R0, 0xD6D0 (T0) + 0x03E00008 # JR RA +] + +new_game_extras = [ + # Upon starting a new game, this will write anything extra to the save file data that the run should have at the + # start. The initial Countdown numbers begin here. + 0x24080000, # ADDIU T0, R0, 0x0000 + 0x24090010, # ADDIU T1, R0, 0x0010 + 0x11090008, # BEQ T0, T1, [forward 0x08] + 0x3C0A8040, # LUI T2, 0x8040 + 0x01485021, # ADDU T2, T2, T0 + 0x8D4AD818, # LW T2, 0xD818 (T2) + 0x3C0B8039, # LUI T3, 0x8039 + 0x01685821, # ADDU T3, T3, T0 + 0xAD6A9CA4, # SW T2, 0x9CA4 (T3) + 0x1000FFF8, # B [backward 0x08] + 0x25080004, # ADDIU T0, T0, 0x0004 + # start_inventory begins here + 0x3C088039, # LUI T0, 0x8039 + 0x91099C27, # LBU T1, 0x9C27 (T0) + 0x31290010, # ANDI T1, T1, 0x0010 + 0x15200005, # BNEZ T1, [forward 0x05] + 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting jewels + 0xA1099C49, # SB T1, 0x9C49 + 0x3C0A8040, # LUI T2, 0x8040 + 0x8D4BE514, # LW T3, 0xE514 (T2) <- Starting money + 0xAD0B9C44, # SW T3, 0x9C44 (T0) + 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting PowerUps + 0xA1099CED, # SB T1, 0x9CED (T0) + 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting sub-weapon + 0xA1099C43, # SB T1, 0x9C43 (T0) + 0x24090000, # ADDIU T1, R0, 0x0000 <- Starting Ice Traps + 0xA1099BE2, # SB T1, 0x9BE2 (T0) + 0x240C0000, # ADDIU T4, R0, 0x0000 + 0x240D0022, # ADDIU T5, R0, 0x0022 + 0x11AC0007, # BEQ T5, T4, [forward 0x07] + 0x3C0A8040, # LUI T2, 0x8040 + 0x014C5021, # ADDU T2, T2, T4 + 0x814AE518, # LB T2, 0xE518 <- Starting inventory items + 0x25080001, # ADDIU T0, T0, 0x0001 + 0xA10A9C4A, # SB T2, 0x9C4A (T0) + 0x1000FFF9, # B [backward 0x07] + 0x258C0001, # ADDIU T4, T4, 0x0001 + 0x03E00008 # JR RA +] + +shopsanity_stuff = [ + # Everything related to shopsanity. + # Flag table (in bytes) start + 0x80402010, + 0x08000000, + 0x00000000, + 0x00000000, + 0x00040200, + # Replacement item table (in halfwords) start + 0x00030003, + 0x00030003, + 0x00030000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000003, + 0x00030000, + # Switches the vanilla item being bought with the randomized one, if its flag is un-set, and sets its flag. + 0x3C088040, # LUI T0, 0x8040 + 0x01044021, # ADDU T0, T0, A0 + 0x9109D8CA, # LBU T1, 0xD8CA (T0) + 0x3C0B8039, # LUI T3, 0x8039 + 0x916A9C1D, # LBU T2, 0x9C1D (T3) + 0x01496024, # AND T4, T2, T1 + 0x15800005, # BNEZ T4, [forward 0x05] + 0x01495025, # OR T2, T2, T1 + 0xA16A9C1D, # SB T2, 0x9C1D (T3) + 0x01044021, # ADDU T0, T0, A0 + 0x9504D8D8, # LHU A0, 0xD8D8 (T0) + 0x308400FF, # ANDI A0, A0, 0x00FF + 0x0804EFFB, # J 0x8013BFEC + 0x00000000, # NOP + # Switches the vanilla item model on the buy menu with the randomized item if the randomized item isn't purchased. + 0x3C088040, # LUI T0, 0x8040 + 0x01044021, # ADDU T0, T0, A0 + 0x9109D8CA, # LBU T1, 0xD8CA (T0) + 0x3C0B8039, # LUI T3, 0x8039 + 0x916A9C1D, # LBU T2, 0x9C1D (T3) + 0x01495024, # AND T2, T2, T1 + 0x15400005, # BNEZ T2, [forward 0x05] + 0x01044021, # ADDU T0, T0, A0 + 0x9504D8D8, # LHU A0, 0xD8D8 (T0) + 0x00046202, # SRL T4, A0, 8 + 0x55800001, # BNEZL T4, [forward 0x01] + 0x01802021, # ADDU A0, T4, R0 + 0x0804F180, # J 0x8013C600 + 0x00000000, # NOP + # Replacement item names table start. + 0x00010203, + 0x04000000, + 0x00000000, + 0x00000000, + 0x00050600, + 0x00000000, + # Switches the vanilla item name in the shop menu with the randomized item if the randomized item isn't purchased. + 0x3C088040, # LUI T0, 0x8040 + 0x01064021, # ADDU T0, T0, A2 + 0x9109D8CA, # LBU T1, 0xD8CA (T0) + 0x3C0B8039, # LUI T3, 0x8039 + 0x916A9C1D, # LBU T2, 0x9C1D (T3) + 0x01495024, # AND T2, T2, T1 + 0x15400004, # BNEZ T2, [forward 0x04] + 0x00000000, # NOP + 0x9105D976, # LBU A1, 0xD976 (T0) + 0x3C048001, # LUI A0, 8001 + 0x3484A100, # ORI A0, A0, 0xA100 + 0x0804B39F, # J 0x8012CE7C + 0x00000000, # NOP + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + # Displays "Not purchased." if the selected randomized item is nor purchased, or the current holding amount of that + # slot's vanilla item if it is. + 0x3C0C8040, # LUI T4, 0x8040 + 0x018B6021, # ADDU T4, T4, T3 + 0x918DD8CA, # LBU T5, 0xD8CA (T4) + 0x3C0E8039, # LUI T6, 0x8039 + 0x91D89C1D, # LBU T8, 0x9C1D (T6) + 0x030DC024, # AND T8, T8, T5 + 0x13000003, # BEQZ T8, [forward 0x03] + 0x00000000, # NOP + 0x0804E819, # J 0x8013A064 + 0x00000000, # NOP + 0x0804E852, # J 0x8013A148 + 0x820F0061, # LB T7, 0x0061 (S0) + 0x00000000, # NOP + # Displays a custom item description if the selected randomized item is not purchased. + 0x3C088040, # LUI T0, 0x8040 + 0x01054021, # ADDU T0, T0, A1 + 0x9109D8D0, # LBU T1, 0xD8D0 (T0) + 0x3C0A8039, # LUI T2, 0x8039 + 0x914B9C1D, # LBU T3, 0x9C1D (T2) + 0x01695824, # AND T3, T3, T1 + 0x15600003, # BNEZ T3, [forward 0x03] + 0x00000000, # NOP + 0x3C048002, # LUI A0, 0x8002 + 0x24849C00, # ADDIU A0, A0, 0x9C00 + 0x0804B39F # J 0x8012CE7C +] + +special_sound_notifs = [ + # Plays a distinct sound whenever you get enough Special1s to unlock a new location or enough Special2s to unlock + # Dracula's door. + 0x3C088013, # LUI A0, 0x8013 + 0x9108AC9F, # LBU T0, 0xAC57 (T0) + 0x3C098039, # LUI T1, 0x8039 + 0x91299C4C, # LBU T1, 0x9C4B (T1) + 0x15090003, # BNE T0, T1, [forward 0x03] + 0x00000000, # NOP + 0x0C004FAB, # JAL 0x80013EAC + 0x24040162, # ADDIU A0, R0, 0x0162 + 0x0804F0BF, # J 0x8013C2FC + 0x00000000, # NOP + 0x3C088013, # LUI T0, 0x8013 + 0x9108AC57, # LBU T0, 0xAC57 (T0) + 0x3C098039, # LUI T1, 0x8039 + 0x91299C4B, # LBU T1, 0x9C4B (T1) + 0x0128001B, # DIVU T1, T0 + 0x00005010, # MFHI + 0x15400006, # BNEZ T2, [forward 0x06] + 0x00005812, # MFLO T3 + 0x296C0008, # SLTI T4, T3, 0x0008 + 0x11800003, # BEQZ T4, [forward 0x03] + 0x00000000, # NOP + 0x0C004FAB, # JAL 0x80013EAC + 0x2404019E, # ADDIU A0, R0, 0x019E + 0x0804F0BF # J 0x8013C2FC +] + +map_text_redirector = [ + # Checks for Map Texts 06 or 08 if in the Forest or Castle Wall Main maps respectively and redirects the text + # pointer to a blank string, skipping all the yes/no prompt text for pulling levers. + 0x0002FFFF, # Dummy text string + 0x3C0B8039, # LUI T3, 0x8039 + 0x91689EE1, # LBU T0, 0x9EE1 (T3) + 0x1100000F, # BEQZ T0, [forward 0x0F] + 0x24090006, # ADDIU T1, R0, 0x0006 + 0x240A0002, # ADDIU T2, R0, 0x0002 + 0x110A000C, # BEQ T0, T2, [forward 0x0C] + 0x24090008, # ADDIU T1, R0, 0x0008 + 0x240A0009, # ADDIU T2, R0, 0x0009 + 0x110A0009, # BEQ T0, T2, [forward 0x09] + 0x24090004, # ADDIU T1, R0, 0x0004 + 0x240A000A, # ADDIU T2, R0, 0x000A + 0x110A0006, # BEQ T0, T2, [forward 0x06] + 0x24090001, # ADDIU T1, R0, 0x0001 + 0x240A000C, # ADDIU T2, R0, 0x000C + 0x110A0003, # BEQ T0, T2, [forward 0x03] + 0x2409000C, # ADDIU T1, R0, 0x000C + 0x10000008, # B 0x803FDB34 + 0x00000000, # NOP + 0x15250006, # BNE T1, A1, [forward 0x06] + 0x00000000, # NOP + 0x3C04803F, # LUI A0, 0x803F + 0x3484DACC, # ORI A0, A0, 0xDACC + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x0804B39F, # J 0x8012CE7C + 0x00000000, # NOP + # Redirects to a custom message if you try placing the bomb ingredients at the bottom CC crack before deactivating + # the seal. + 0x24090009, # ADDIU T1, R0, 0x0009 + 0x15090009, # BNE T0, T1, [forward 0x09] + 0x240A0002, # ADDIU T2, R0, 0x0002 + 0x15450007, # BNE T2, A1, [forward 0x07] + 0x916A9C18, # LBU T2, 0x9C18 (T3) + 0x314A0001, # ANDI T2, T2, 0x0001 + 0x15400004, # BNEZ T2, [forward 0x04] + 0x00000000, # NOP + 0x3C04803F, # LUI A0, 0x803F + 0x3484DBAC, # ORI A0, A0, 0xDBAC + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x0804B39F, # J 0x8012CE7C + 0x00000000, # NOP + # Checks for Map Texts 02 or 00 if in the Villa hallway or CC lizard lab maps respectively and redirects the text + # pointer to a blank string, skipping all the NPC dialogue mandatory for checks. + 0x3C088039, # LUI T0, 0x8039 + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + 0x240A0005, # ADDIU T2, R0, 0x0005 + 0x110A0006, # BEQ T0, T2, [forward 0x06] + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x240A000C, # ADDIU T2, R0, 0x000C + 0x110A0003, # BEQ T0, T2, [forward 0x03] + 0x24090000, # ADDIU T1, R0, 0x0000 + 0x0804B39F, # J 0x8012CE7C + 0x00000000, # NOP + 0x15250004, # BNE T1, A1, [forward 0x04] + 0x00000000, # NOP + 0x3C04803F, # LUI A0, 0x803F + 0x3484DACC, # ORI A0, A0, 0xDACC + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x0804B39F # J 0x8012CE7C +] + +special_descriptions_redirector = [ + # Redirects the menu description when looking at the Special1 and 2 items to different, custom strings that tell + # how many are needed per warp and to fight Dracula respectively, and how many there are of both in the whole seed. + 0x240A0003, # ADDIU T2, R0, 0x0003 + 0x10AA0005, # BEQ A1, T2, [forward 0x05] + 0x240A0004, # ADDIU T2, R0, 0x0004 + 0x10AA0003, # BEQ A1, T2, [forward 0x03] + 0x00000000, # NOP + 0x0804B39F, # J 0x8012CE7C + 0x00000000, # NOP + 0x3C04803F, # LUI A0, 0x803F + 0x3484E53C, # ORI A0, A0, 0xE53C + 0x24A5FFFD, # ADDIU A1, A1, 0xFFFD + 0x0804B39F # J 0x8012CE7C +] + +forest_cw_villa_intro_cs_player = [ + # Plays the Forest, Castle Wall, or Villa intro cutscene after transitioning to a different map if the map being + # transitioned to is the start of their levels respectively. Gets around the fact that they have to be set on the + # previous loading zone for them to play normally. + 0x3C088039, # LUI T0, 0x8039 + 0x8D099EE0, # LW T1, 0x9EE0 (T0) + 0x1120000B, # BEQZ T1 T1, [forward 0x0B] + 0x240B0000, # ADDIU T3, R0, 0x0000 + 0x3C0A0002, # LUI T2, 0x0002 + 0x112A0008, # BEQ T1, T2, [forward 0x08] + 0x240B0007, # ADDIU T3, R0, 0x0007 + 0x254A0007, # ADDIU T2, T2, 0x0007 + 0x112A0005, # BEQ T1, T2, [forward 0x05] + 0x3C0A0003, # LUI T2, 0x0003 + 0x112A0003, # BEQ T1, T2, [forward 0x03] + 0x240B0003, # ADDIU T3, R0, 0x0003 + 0x08005FAA, # J 0x80017EA8 + 0x00000000, # NOP + 0x010B6021, # ADDU T4, T0, T3 + 0x918D9C08, # LBU T5, 0x9C08 (T4) + 0x31AF0001, # ANDI T7, T5, 0x0001 + 0x15E00009, # BNEZ T7, [forward 0x09] + 0x240E0009, # ADDIU T6, R0, 0x0009 + 0x3C180003, # LUI T8, 0x0003 + 0x57090001, # BNEL T8, T1, [forward 0x01] + 0x240E0004, # ADDIU T6, R0, 0x0004 + 0x15200003, # BNEZ T1, [forward 0x03] + 0x240F0001, # ADDIU T7, R0, 0x0001 + 0xA18F9C08, # SB T7, 0x9C08 (T4) + 0x240E003C, # ADDIU T6, R0, 0x003C + 0xA10E9EFF, # SB T6, 0x9EFF (T0) + 0x08005FAA # J 0x80017EA8 +] + +map_id_refresher = [ + # After transitioning to a different map, if this detects the map ID being transitioned to as FF, it will write back + # the past map ID so that the map will reset. Useful for thngs like getting around a bug wherein the camera fixes in + # place if you enter a loading zone that doesn't actually change the map, which can happen in a seed that gives you + # any character tower stage at the very start. + 0x240800FF, # ADDIU T0, R0, 0x00FF + 0x110E0003, # BEQ T0, T6, [forward 0x03] + 0x00000000, # NOP + 0x03E00008, # JR RA + 0xA44E61D8, # SH T6, 0x61D8 + 0x904961D9, # LBU T1, 0x61D9 + 0xA0496429, # SB T1, 0x6429 + 0x03E00008 # JR RA +] + +character_changer = [ + # Changes the character being controlled if the player is holding L while loading into a map by swapping the + # character ID. + 0x3C08800D, # LUI T0, 0x800D + 0x910B5E21, # LBU T3, 0x5E21 (T0) + 0x31680020, # ANDI T0, T3, 0x0020 + 0x3C0A8039, # LUI T2, 0x8039 + 0x1100000B, # BEQZ T0, [forward 0x0B] + 0x91499C3D, # LBU T1, 0x9C3D (T2) + 0x11200005, # BEQZ T1, [forward 0x05] + 0x24080000, # ADDIU T0, R0, 0x0000 + 0xA1489C3D, # SB T0, 0x9C3D (T2) + 0x25080001, # ADDIU T0, T0, 0x0001 + 0xA1489BC2, # SB T0, 0x9BC2 (T2) + 0x10000004, # B [forward 0x04] + 0x24080001, # ADDIU T0, R0, 0x0001 + 0xA1489C3D, # SB T0, 0x9C3D (T2) + 0x25080001, # ADDIU T0, T0, 0x0001 + 0xA1489BC2, # SB T0, 0x9BC2 (T2) + # Changes the alternate costume variables if the player is holding C-up. + 0x31680008, # ANDI T0, T3, 0x0008 + 0x11000009, # BEQZ T0, [forward 0x09] + 0x91499C24, # LBU T1, 0x9C24 (T2) + 0x312B0040, # ANDI T3, T1, 0x0040 + 0x2528FFC0, # ADDIU T0, T1, 0xFFC0 + 0x15600003, # BNEZ T3, [forward 0x03] + 0x240C0000, # ADDIU T4, R0, 0x0000 + 0x25280040, # ADDIU T0, T1, 0x0040 + 0x240C0001, # ADDIU T4, R0, 0x0001 + 0xA1489C24, # SB T0, 0x9C24 (T2) + 0xA14C9CEE, # SB T4, 0x9CEE (T2) + 0x080062AA, # J 0x80018AA8 + 0x00000000, # NOP + # Plays the attack sound of the character being changed into to indicate the change was successful. + 0x3C088039, # LUI T0, 0x8039 + 0x91099BC2, # LBU T1, 0x9BC2 (T0) + 0xA1009BC2, # SB R0, 0x9BC2 (T0) + 0xA1009BC1, # SB R0, 0x9BC1 (T0) + 0x11200006, # BEQZ T1, [forward 0x06] + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0x240402F6, # ADDIU A0, R0, 0x02F6 + 0x55200001, # BNEZL T1, [forward 0x01] + 0x240402F8, # ADDIU A0, R0, 0x02F8 + 0x08004FAB, # J 0x80013EAC + 0x00000000, # NOP + 0x03E00008 # JR RA +] + +panther_dash = [ + # Changes various movement parameters when holding C-right so the player will move way faster. + # Increases movement speed and speeds up the running animation. + 0x3C08800D, # LUI T0, 0x800D + 0x91085E21, # LBU T0, 0x5E21 (T0) + 0x31080001, # ANDI T0, T0, 0x0001 + 0x24093FEA, # ADDIU T1, R0, 0x3FEA + 0x11000004, # BEQZ T0, [forward 0x04] + 0x240B0010, # ADDIU T3, R0, 0x0010 + 0x3C073F20, # LUI A3, 0x3F20 + 0x240940AA, # ADDIU T1, R0, 0x40AA + 0x240B000A, # ADDIU T3, R0, 0x000A + 0x3C0C8035, # LUI T4, 0x8035 + 0xA18B07AE, # SB T3, 0x07AE (T4) + 0xA18B07C2, # SB T3, 0x07C2 (T4) + 0x3C0A8034, # LUI T2, 0x8034 + 0x03200008, # JR T9 + 0xA5492BD8, # SH T1, 0x2BD8 (T2) + 0x00000000, # NOP + # Increases the turning speed so that handling is better. + 0x3C08800D, # LUI T0, 0x800D + 0x91085E21, # LBU T0, 0x5E21 (T0) + 0x31080001, # ANDI T0, T0, 0x0001 + 0x11000002, # BEQZ T0, [forward 0x02] + 0x240A00D9, # ADDIU T2, R0, 0x00D9 + 0x240A00F0, # ADDIU T2, R0, 0x00F0 + 0x3C0B8039, # LUI T3, 0x8039 + 0x916B9C3D, # LBU T3, 0x9C3D (T3) + 0x11600003, # BEQZ T3, [forward 0x03] + 0xD428DD58, # LDC1 F8, 0xDD58 (AT) + 0x03E00008, # JR RA + 0xA02ADD59, # SB T2, 0xDD59 (AT) + 0xD428D798, # LDC1 F8, 0xD798 (AT) + 0x03E00008, # JR RA + 0xA02AD799, # SB T2, 0xD799 (AT) + 0x00000000, # NOP + # Increases crouch-walking x and z speed. + 0x3C08800D, # LUI T0, 0x800D + 0x91085E21, # LBU T0, 0x5E21 (T0) + 0x31080001, # ANDI T0, T0, 0x0001 + 0x11000002, # BEQZ T0, [forward 0x02] + 0x240A00C5, # ADDIU T2, R0, 0x00C5 + 0x240A00F8, # ADDIU T2, R0, 0x00F8 + 0x3C0B8039, # LUI T3, 0x8039 + 0x916B9C3D, # LBU T3, 0x9C3D (T3) + 0x15600005, # BNEZ T3, [forward 0x05] + 0x00000000, # NOP + 0xA02AD801, # SB T2, 0xD801 (AT) + 0xA02AD809, # SB T2, 0xD809 (AT) + 0x03E00008, # JR RA + 0xD430D800, # LDC1 F16, 0xD800 (AT) + 0xA02ADDC1, # SB T2, 0xDDC1 (AT) + 0xA02ADDC9, # SB T2, 0xDDC9 (AT) + 0x03E00008, # JR RA + 0xD430DDC0 # LDC1 F16, 0xDDC0 (AT) +] + +panther_jump_preventer = [ + # Optional hack to prevent jumping while moving at the increased panther dash speed as a way to prevent logic + # sequence breaks that would otherwise be impossible without it. Such sequence breaks are never considered in logic + # either way. + + # Decreases a "can running jump" value by 1 per frame unless it's at 0, or while in the sliding state. When the + # player lets go of C-right, their running speed should have returned to a normal amount by the time it hits 0. + 0x9208007F, # LBU T0, 0x007F (S0) + 0x24090008, # ADDIU T1, R0, 0x0008 + 0x11090005, # BEQ T0, T1, [forward 0x05] + 0x3C088039, # LUI T0, 0x8039 + 0x91099BC1, # LBU T1, 0x9BC1 (T0) + 0x11200002, # BEQZ T1, [forward 0x02] + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1099BC1, # SB T1, 0x9BC1 (T0) + 0x080FF413, # J 0x803FD04C + 0x00000000, # NOP + # Increases the "can running jump" value by 2 per frame while panther dashing unless it's at 8 or higher, at which + # point the player should be at the max panther dash speed. + 0x00074402, # SRL T0, A3, 16 + 0x29083F7F, # SLTI T0, T0, 0x3F7F + 0x11000006, # BEQZ T0, [forward 0x06] + 0x3C098039, # LUI T1, 0x8039 + 0x912A9BC1, # LBU T2, 0x9BC1 (T1) + 0x254A0002, # ADDIU T2, T2, 0x0002 + 0x294B0008, # SLTI T3, T2, 0x0008 + 0x55600001, # BNEZL T3, [forward 0x01] + 0xA12A9BC1, # SB T2, 0x9BC1 (T1) + 0x03200008, # JR T9 + 0x00000000, # NOP + # Makes running jumps only work while the "can running jump" value is at 0. Otherwise, their state won't change. + 0x3C010001, # LUI AT, 0x0001 + 0x3C088039, # LUI T0, 0x8039 + 0x91089BC1, # LBU T0, 0x9BC1 (T0) + 0x55000001, # BNEZL T0, [forward 0x01] + 0x3C010000, # LUI AT, 0x0000 + 0x03E00008 # JR RA +] + +gondola_skipper = [ + # Upon stepping on one of the gondolas in Tunnel to activate it, this will instantly teleport you to the other end + # of the gondola course depending on which one activated, skipping the entire 3-minute wait to get there. + 0x3C088039, # LUI T0, 0x8039 + 0x240900FF, # ADDIU T1, R0, 0x00FF + 0xA1099EE1, # SB T1, 0x9EE1 (T0) + 0x31EA0020, # ANDI T2, T7, 0x0020 + 0x3C0C3080, # LUI T4, 0x3080 + 0x358C9700, # ORI T4, T4, 0x9700 + 0x154B0003, # BNE T2, T3, [forward 0x03] + 0x24090002, # ADDIU T1, R0, 0x0002 + 0x24090003, # ADDIU T1, R0, 0x0003 + 0x3C0C7A00, # LUI T4, 0x7A00 + 0xA1099EE3, # SB T1, 0x9EE3 (T0) + 0xAD0C9EE4, # SW T4, 0x9EE4 (T0) + 0x3C0D0010, # LUI T5, 0x0010 + 0x25AD0010, # ADDIU T5, T5, 0x0010 + 0xAD0D9EE8, # SW T5, 0x9EE8 (T0) + 0x08063E68 # J 0x8018F9A0 +] + +mandragora_with_nitro_setter = [ + # When setting a Nitro, if Mandragora is in the inventory too and the wall's "Mandragora set" flag is not set, this + # will automatically subtract a Mandragora from the inventory and set its flag so the wall can be blown up in just + # one interaction instead of two. + 0x3C088039, # LUI T0, 0x8039 + 0x81099EE1, # LB T1, 0x9EE1 (T0) + 0x240A000C, # ADDIU T2, R0, 0x000C + 0x112A000E, # BEQ T1, T2, [forward 0x0E] + 0x81099C18, # LB T1, 0x9C18 (T0) + 0x31290002, # ANDI T1, T1, 0x0002 + 0x11200009, # BEQZ T1, [forward 0x09] + 0x91099C5D, # LBU T1, 0x9C5D (T0) + 0x11200007, # BEQZ T1, [forward 0x07] + 0x910B9C1A, # LBU T3, 0x9C1A (T0) + 0x316A0001, # ANDI T2, T3, 0x0001 + 0x15400004, # BNEZ T2, [forward 0x04] + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1099C5D, # SB T1, 0x9C5D (T0) + 0x356B0001, # ORI T3, T3, 0x0001 + 0xA10B9C1A, # SB T3, 0x9C1A (T0) + 0x08000512, # J 0x80001448 + 0x00000000, # NOP + 0x810B9BF2, # LB T3, 0x9BF2 (T0) + 0x31690040, # ANDI T1, T3, 0x0040 + 0x11200008, # BEQZ T1, [forward 0x08] + 0x91099C5D, # LBU T1, 0x9C5D (T0) + 0x11200006, # BEQZ T1, [forward 0x06] + 0x316A0080, # ANDI T2, T3, 0x0080 + 0x15400004, # BNEZ T2, 0x803FE0E8 + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1099C5D, # SB T1, 0x9C5D (T0) + 0x356B0080, # ORI T3, T3, 0x0080 + 0xA10B9BF2, # SB T3, 0x9BF2 (T0) + 0x08000512 # J 0x80001448 +] + +ambience_silencer = [ + # Silences all map-specific ambience when loading into a different map, so we don't have to live with, say, Tower of + # Science/Clock Tower machinery noises everywhere until either resetting, dying, or going into a map that is + # normally set up to disable said noises. + 0x3C088039, # LUI T0, 0x8039 + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + 0x24090012, # ADDIU T1, R0, 0x0012 + 0x11090003, # BEQ T0, T1, [forward 0x03] + 0x00000000, # NOP + 0x0C004FAB, # JAL 0x80013EAC + 0x3404818C, # ORI A0, R0, 0x818C + 0x0C004FAB, # JAL 0x80013EAC + 0x34048134, # ORI A0, R0, 0x8134 + 0x0C004FAB, # JAL 0x80013EAC + 0x34048135, # ORI A0, R0, 0x8135 + 0x0C004FAB, # JAL 0x80013EAC + 0x34048136, # ORI A0, R0, 0x8136 + 0x08054987, # J 0x8015261C + 0x00000000, # NOP + # Plays the fan ambience when loading into the fan meeting room if this detects the active character's cutscene flag + # here already being set. + 0x3C088039, # LUI T0, 0x8039 + 0x91099EE1, # LBU T1, 0x9EE1 (T0) + 0x240A0019, # ADDIU T2, R0, 0x0019 + 0x152A000A, # BNE T1, T2, [forward 0x0A] + 0x910B9BFE, # LBU T3, 0x9BFE (T0) + 0x910C9C3D, # LBU T4, 0x9C3D (T0) + 0x240D0001, # ADDIU T5, R0, 0x0001 + 0x55800001, # BNEZL T4, [forward 0x01] + 0x240D0002, # ADDIU T5, R0, 0x0002 + 0x016D7024, # AND T6, T3, T5 + 0x11C00003, # BEQZ T6, [forward 0x03] + 0x00000000, # NOP + 0x0C0052B4, # JAL 0x80014AD0 + 0x34040169, # ORI A0, R0, 0x0169 + 0x0805581C # J 0x80156070 +] + +coffin_cutscene_skipper = [ + # Kills the normally-unskippable "Found a hidden path" cutscene at the end of Villa if this detects, in the current + # module in the modules array, the cutscene's module number of 0x205C and the "skip" value 0f 0x01 normally set by + # all cutscenes upon pressing Start. + 0x10A0000B, # BEQZ A1, [forward 0x0B] + 0x00000000, # NOP + 0x94A80000, # LHU T0, 0x0000 (A1) + 0x2409205C, # ADDIU T1, R0, 0x205C + 0x15090007, # BNE T0, T1, [forward 0x07] + 0x90AA0070, # LBU T2, 0x0070 (A1) + 0x11400005, # BEQZ T2, [forward 0x05] + 0x90AB0009, # LBU T3, 0x0009 (A1) + 0x240C0003, # ADDIU T4, R0, 0x0003 + 0x156C0002, # BNE T3, T4, [forward 0x02] + 0x240B0004, # ADDIU T3, R0, 0x0004 + 0xA0AB0009, # SB T3, 0x0009 (A1) + 0x03E00008 # JR RA +] + +multiworld_item_name_loader = [ + # When picking up an item from another world, this will load from ROM the custom message for that item explaining + # in the item textbox what the item is and who it's for. The flag index it calculates determines from what part of + # the ROM to load the item name from. If the item being picked up is a white jewel or a contract, it will always + # load from a part of the ROM that has nothing in it to ensure their set "flag" values don't yield unintended names. + 0x3C088040, # LUI T0, 0x8040 + 0xAD03E238, # SW V1, 0xE238 (T0) + 0x92080039, # LBU T0, 0x0039 (S0) + 0x11000003, # BEQZ T0, [forward 0x03] + 0x24090012, # ADDIU T1, R0, 0x0012 + 0x15090003, # BNE T0, T1, [forward 0x03] + 0x24080000, # ADDIU T0, R0, 0x0000 + 0x10000010, # B [forward 0x10] + 0x24080000, # ADDIU T0, R0, 0x0000 + 0x920C0055, # LBU T4, 0x0055 (S0) + 0x8E090058, # LW T1, 0x0058 (S0) + 0x1120000C, # BEQZ T1, [forward 0x0C] + 0x298A0011, # SLTI T2, T4, 0x0011 + 0x51400001, # BEQZL T2, [forward 0x01] + 0x258CFFED, # ADDIU T4, T4, 0xFFED + 0x240A0000, # ADDIU T2, R0, 0x0000 + 0x00094840, # SLL T1, T1, 1 + 0x5520FFFE, # BNEZL T1, [backward 0x02] + 0x254A0001, # ADDIU T2, T2, 0x0001 + 0x240B0020, # ADDIU T3, R0, 0x0020 + 0x018B0019, # MULTU T4, T3 + 0x00004812, # MFLO T1 + 0x012A4021, # ADDU T0, T1, T2 + 0x00084200, # SLL T0, T0, 8 + 0x3C0400BB, # LUI A0, 0x00BB + 0x24847164, # ADDIU A0, A0, 0x7164 + 0x00882020, # ADD A0, A0, T0 + 0x3C058018, # LUI A1, 0x8018 + 0x34A5BF98, # ORI A1, A1, 0xBF98 + 0x0C005DFB, # JAL 0x800177EC + 0x24060100, # ADDIU A2, R0, 0x0100 + 0x3C088040, # LUI T0, 0x8040 + 0x8D03E238, # LW V1, 0xE238 (T0) + 0x3C1F8012, # LUI RA, 0x8012 + 0x27FF5BA4, # ADDIU RA, RA, 0x5BA4 + 0x0804EF54, # J 0x8013BD50 + 0x94640002, # LHU A0, 0x0002 (V1) + # Changes the Y screen position of the textbox depending on how many line breaks there are. + 0x3C088019, # LUI T0, 0x8019 + 0x9108C097, # LBU T0, 0xC097 (T0) + 0x11000005, # BEQZ T0, [forward 0x05] + 0x2508FFFF, # ADDIU T0, T0, 0xFFFF + 0x11000003, # BEQZ T0, [forward 0x03] + 0x00000000, # NOP + 0x1000FFFC, # B [backward 0x04] + 0x24C6FFF1, # ADDIU A2, A2, 0xFFF1 + 0x0804B33F, # J 0x8012CCFC + # Changes the length and number of lines on the textbox if there's a multiworld message in the buffer. + 0x3C088019, # LUI T0, 0x8019 + 0x9108C097, # LBU T0, 0xC097 (T0) + 0x11000003, # BEQZ T0, [forward 0x03] + 0x00000000, # NOP + 0x00082821, # ADDU A1, R0, T0 + 0x240600B6, # ADDIU A2, R0, 0x00B6 + 0x0804B345, # J 0x8012CD14 + 0x00000000, # NOP + # Redirects the text to the multiworld message buffer if a message exists in it. + 0x3C088019, # LUI T0, 0x8019 + 0x9108C097, # LBU T0, 0xC097 (T0) + 0x11000004, # BEQZ T0, [forward 0x04] + 0x00000000, # NOP + 0x3C048018, # LUI A0, 0x8018 + 0x3484BF98, # ORI A0, A0, 0xBF98 + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x0804B39F, # J 0x8012CE7C + # Copy the "item from player" text when being given an item through the multiworld via the game's copy function. + 0x00000000, # NOP + 0x00000000, # NOP + 0x00000000, # NOP + 0x3C088040, # LUI T0, 0x8040 + 0xAD1FE33C, # SW RA, 0xE33C (T0) + 0xA104E338, # SB A0, 0xE338 (T0) + 0x3C048019, # LUI A0, 0x8019 + 0x2484C0A8, # ADDIU A0, A0, 0xC0A8 + 0x3C058019, # LUI A1, 0x8019 + 0x24A5BF98, # ADDIU A1, A1, 0xBF98 + 0x0C000234, # JAL 0x800008D0 + 0x24060100, # ADDIU A2, R0, 0x0100 + 0x3C088040, # LUI T0, 0x8040 + 0x8D1FE33C, # LW RA, 0xE33C (T0) + 0x0804EDCE, # J 0x8013B738 + 0x9104E338, # LBU A0, 0xE338 (T0) + 0x00000000, # NOP + # Neuters the multiworld item text buffer if giving a non-multiworld item through the in-game remote item rewarder + # byte before then jumping to item_prepareTextbox. + 0x24080011, # ADDIU T0, R0, 0x0011 + 0x10880004, # BEQ A0, T0, [forward 0x04] + 0x24080012, # ADDIU T0, R0, 0x0012 + 0x10880002, # BEQ A0, T0, [forward 0x02] + 0x3C088019, # LUI T0, 0x8019 + 0xA100C097, # SB R0, 0xC097 (T0) + 0x0804EDCE # J 0x8013B738 +] + +ice_trap_initializer = [ + # During a map load, creates the module that allows the ice block model to appear while in the frozen state if not + # on the intro narration map (map 0x16). + 0x3C088039, # LUI T0, 0x8039 + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + 0x24090016, # ADDIU T1, R0, 0x0016 + 0x11090004, # BEQ T0, T1, [forward 0x04] + 0x3C048034, # LUI A0, 0x8034 + 0x24842ACC, # ADDIU A0, A0, 0x2ACC + 0x08000660, # J 0x80001980 + 0x24052125, # ADDIU A1, R0, 0x2125 + 0x03E00008 # JR RA +] + +the_deep_freezer = [ + # Writes 000C0000 into the player state to freeze the player on the spot if Ice Traps have been received, writes the + # Ice Trap code into the pointer value (0x20B8, which is also Camilla's boss code),and decrements the Ice Traps + # remaining counter. All after verifying the player is in a "safe" state to be frozen in. + 0x3C0B8039, # LUI T3, 0x8039 + 0x91699BE2, # LBU T3, 0x9BE2 (T0) + 0x11200015, # BEQZ T1, [forward 0x15] + 0x3C088034, # LUI T0, 0x8034 + 0x910827A9, # LBU T0, 0x27A9 (T0) + 0x240A0005, # ADDIU T2, R0, 0x0005 + 0x110A0011, # BEQ T0, T2, [forward 0x11] + 0x240A000C, # ADDIU T2, R0, 0x000C + 0x110A000F, # BEQ T0, T2, [forward 0x0F] + 0x240A0002, # ADDIU T2, R0, 0x0002 + 0x110A000D, # BEQ T0, T2, [forward 0x0D] + 0x240A0008, # ADDIU T2, R0, 0x0008 + 0x110A000B, # BEQ T0, T2, [forward 0x0B] + 0x2529FFFF, # ADDIU T1, T1, 0xFFFF + 0xA1699BE2, # SB T1, 0x9BE2 (T3) + 0x3C088034, # LUI T0, 0x8034 + 0x3C09000C, # LUI T1, 0x000C + 0xAD0927A8, # SW T1, 0x27A8 (T0) + 0x240C20B8, # ADDIU T4, R0, 0x20B8 + 0xA56C9E6E, # SH T4, 0x9E6E (T3) + 0x8D0927C8, # LW T1, 0x27C8 (T0) + 0x912A0048, # LBU T2, 0x0068 (T1) + 0x314A007F, # ANDI T2, T2, 0x007F + 0xA12A0048, # SB T2, 0x0068 (T1) + 0x03E00008 # JR RA +] + +freeze_verifier = [ + # Verifies for the ice chunk module that a freeze should spawn the ice model. The player must be in the frozen state + # (0x000C) and 0x20B8 must be in either the freeze pointer value or the current boss ID (Camilla's); otherwise, we + # weill assume that the freeze happened due to a vampire grab or Actrise shard tornado and not spawn the ice chunk. + 0x8C4E000C, # LW T6, 0x000C (V0) + 0x00803025, # OR A2, A0, R0 + 0x8DC30008, # LW V1, 0x0008 (T6) + 0x3C088039, # LUI T0, 0x8039 + 0x240920B8, # ADDIU T1, R0, 0x20B8 + 0x950A9E72, # LHU T2, 0x9E72 (T0) + 0x3C0C8034, # LUI T4, 0x8034 + 0x918C27A9, # LBU T4, 0x27A9 (T4) + 0x240D000C, # ADDIU T5, R0, 0x000C + 0x158D0004, # BNE T4, T5, [forward 0x04] + 0x3C0B0F00, # LUI T3, 0x0F00 + 0x112A0005, # BEQ T1, T2, [forward 0x05] + 0x950A9E78, # LHU T2, 0x9E78 (T0) + 0x112A0003, # BEQ T1, T2, [forward 0x03] + 0x357996A0, # ORI T9, T3, 0x96A0 + 0x03200008, # JR T9 + 0x00000000, # NOP + 0x35799640, # ORI T9, T3, 0x9640 + 0x03200008, # JR T9 +] + +countdown_extra_safety_check = [ + # Checks to see if the multiworld message is a red flashing trap before then truly deciding to decrement the + # Countdown number. This was a VERY last minute thing I caught, since Ice Traps for other CV64 players can take the + # appearance of majors with no other way of the game knowing. + 0x3C0B8019, # LUI T3, 0x8019 + 0x956BBF98, # LHU T3, 0xBF98 (T3) + 0x240C0000, # ADDIU T4, R0, 0x0000 + 0x358CA20B, # ORI T4, T4, 0xA20B + 0x556C0001, # BNEL T3, T4, [forward 0x01] + 0xA1099CA4, # SB T1, 0x9CA4 (T0) + 0x03E00008 # JR RA +] + +countdown_demo_hider = [ + # Hides the Countdown number if we are not in the Gameplay state (state 2), which would happen if we were in the + # Demo state (state 9). This is to ensure the demo maps' number is not peep-able before starting a run proper, for + # the sake of preventing a marginal unfair advantage. Otherwise, updates the number once per frame. + 0x3C088039, # LUI T0, 0x8039 + 0x91089EE1, # LBU T0, 0x9EE1 (T0) + 0x3C098040, # LUI T1, 0x8040 + 0x01284821, # ADDU T1, T1, T0 + 0x0C0FF507, # JAL 0x803FD41C + 0x9124D6DC, # LBU A0, 0xD6DC (T1) + 0x3C088034, # LUI T0, 0x8034 + 0x91092087, # LBU T0, 0x2087 (T0) + 0x240A0002, # ADDIU T2, R0, 0x0002 + 0x112A0003, # BEQ T1, T2, [forward 0x03] + 0x3C048040, # LUI A0, 0x8040 + 0x8C84D6D4, # LW A0, 0xD6D4 (A0) + 0x0C0FF59F, # JAL 0x803FD67C + 0x24050000, # ADDIU A1, R0, 0x0000 + 0x080FF411, # J 0x803FD044 +] + +item_drop_spin_corrector = [ + # Corrects how far AP-placed items drop and how fast they spin based on what appearance they take. + + # Pickup actor ID table for the item appearance IDs to reference. + 0x01020304, + 0x05060708, + 0x090A0B0C, + 0x100D0E0F, + 0x11121314, + 0x15161718, + 0x191D1E1F, + 0x20212223, + 0x24252627, + 0x28291A1B, + 0x1C000000, + 0x00000000, + # Makes AP-placed items in 1-hit breakables drop to their correct, dev-intended height depending on what appearance + # we gave it. Primarily intended for the Axe and the Cross to ensure they don't land half buried in the ground. + 0x000C4202, # SRL T0, T4, 8 + 0x318C00FF, # ANDI T4, T4, 0x00FF + 0x11000003, # BEQZ T0, [forward 0x03] + 0x3C098040, # LUI T1, 0x8040 + 0x01284821, # ADDU T1, T1, T0 + 0x912CE7DB, # LBU T4, 0xE7D8 + 0x03E00008, # JR RA + 0xAC600000, # SW R0, 0x0000 (V1) + 0x00000000, # NOP + # Makes items with changed appearances spin at their correct speed. Unless it's a local Ice Trap, wherein it will + # instead spin at the speed it isn't supposed to. + 0x920B0040, # LBU T3, 0x0040 (S0) + 0x1160000D, # BEQZ T3, [forward 0x0D] + 0x3C0C8040, # LUI T4, 0x8040 + 0x016C6021, # ADDU T4, T3, T4 + 0x918CE7DB, # LBU T4, 0xE7DB (T4) + 0x258CFFFF, # ADDIU T4, T4, 0xFFFF + 0x240D0011, # ADDIU T5, R0, 0x0011 + 0x154D0006, # BNE T2, T5, [forward 0x06] + 0x29AE0006, # SLTI T6, T5, 0x0006 + 0x240A0001, # ADDIU T2, R0, 0x0001 + 0x55C00001, # BNEZL T6, [forward 0x01] + 0x240A0007, # ADDIU T2, R0, 0x0007 + 0x10000002, # B [forward 0x02] + 0x00000000, # NOP + 0x258A0000, # ADDIU T2, T4, 0x0000 + 0x08049648, # J 0x80125920 + 0x3C028017, # LUI V0, 0x8017 + 0x00000000, + 0x00000000, + 0x00000000, + 0x00000000, + # Makes AP-placed items in 3-hit breakables drop to their correct, dev-intended height depending on what appearance + # we gave it. + 0x00184202, # SRL T0, T8, 8 + 0x331800FF, # ANDI T8, T8, 0x00FF + 0x11000003, # BEQZ T0, [forward 0x03] + 0x3C098040, # LUI T1, 0x8040 + 0x01284821, # ADDU T1, T1, T0 + 0x9138E7DB, # LBU T8, 0xE7D8 + 0x03E00008, # JR RA + 0xAC60FFD8, # SW R0, 0xFFD8 (V1) + 0x00000000, + # Makes AP-placed items in the Villa chandelier drop to their correct, dev-intended height depending on what + # appearance we gave it. (why must this singular breakable be such a problem child with its own code? :/) + 0x000D4202, # SRL T0, T5, 8 + 0x31AD00FF, # ANDI T5, T5, 0x00FF + 0x11000003, # BEQZ T0, [forward 0x03] + 0x3C098040, # LUI T1, 0x8040 + 0x01284821, # ADDU T1, T1, T0 + 0x912DE7DB, # LBU T5, 0xE7D8 + 0x03E00008, # JR RA + 0xAC60FFD8, # SW R0, 0xFFD8 (V1) +] + +big_tosser = [ + # Makes every hit the player takes that does not immobilize them send them flying backwards with the power of + # Behemoth's charge. + 0x3C0A8038, # LUI T2, 0x8038 + 0x914A7D7E, # LBU T2, 0x7D7E (T2) + 0x314A0020, # ANDI T2, T2, 0x0020 + 0x1540000D, # BEQZ T2, [forward 0x0D] + 0x3C0A800E, # LUI T2, 0x800E + 0x954B8290, # LHU T3, 0x8290 (T2) + 0x356B2000, # ORI T3, T3, 0x2000 + 0xA54B8290, # SH T3, 0x8290 (T2) + 0x3C0C8035, # LUI T4, 0x8035 + 0x958C09DE, # LHU T4, 0x09DE (T4) + 0x258C8000, # ADDIU T4, T4, 0x8000 + 0x3C0D8039, # LUI T5, 0x8039 + 0xA5AC9CF0, # SH T4, 0x9CF0 (T5) + 0x3C0C4160, # LUI T4, 0x4160 + 0xADAC9CF4, # SW T4, 0x9CF4 (T5) + 0x3C0C4040, # LUI T4, 0x4040 + 0xADAC9CF8, # SW T4, 0x9CF8 (T5) + 0x03E00008, # JR RA + 0x8C680048, # LW T0, 0x0048 (V1) + 0x00000000, + 0x00000000, + # Allows pressing A while getting launched to cancel all XZ momentum. Useful for saving oneself from getting + # launched into an instant death trap. + 0x3C088038, # LUI T0, 0x8038 + 0x91087D80, # LBU T0, 0x7D80 (T0) + 0x31090080, # ANDI T1, T0, 0x0080 + 0x11200009, # BEQZ T1, [forward 0x09] + 0x3C088035, # LUI T0, 0x8035 + 0x8D0A079C, # LW T2, 0x079C (T0) + 0x3C0B000C, # LUI T3, 0x000C + 0x256B4000, # ADDIU T3, T3, 0x4000 + 0x014B5024, # AND T2, T2, T3 + 0x154B0003, # BNE T2, T3, [forward 0x03] + 0x00000000, # NOP + 0xAD00080C, # SW R0, 0x080C (T0) + 0xAD000814, # SW R0, 0x0814 (T0) + 0x03200008 # JR T9 +] diff --git a/worlds/cv64/data/rname.py b/worlds/cv64/data/rname.py new file mode 100644 index 000000000000..851ee618af05 --- /dev/null +++ b/worlds/cv64/data/rname.py @@ -0,0 +1,63 @@ +forest_of_silence = "Forest of Silence" +forest_start = "Forest of Silence: first half" +forest_mid = "Forest of Silence: second half" +forest_end = "Forest of Silence: end area" + +castle_wall = "Castle Wall" +cw_start = "Castle Wall: main area" +cw_exit = "Castle Wall: exit room" +cw_ltower = "Castle Wall: left tower" + +villa = "Villa" +villa_start = "Villa: dog gates" +villa_main = "Villa: main interior" +villa_storeroom = "Villa: storeroom" +villa_archives = "Villa: archives" +villa_maze = "Villa: maze" +villa_servants = "Villa: servants entrance" +villa_crypt = "Villa: crypt" + +tunnel = "Tunnel" +tunnel_start = "Tunnel: first half" +tunnel_end = "Tunnel: second half" + +underground_waterway = "Underground Waterway" +uw_main = "Underground Waterway: main area" +uw_end = "Underground Waterway: end" + +castle_center = "Castle Center" +cc_main = "Castle Center: main area" +cc_crystal = "Castle Center: big crystal" +cc_torture_chamber = "Castle Center: torture chamber" +cc_library = "Castle Center: library" +cc_elev_top = "Castle Center: elevator top" + +duel_tower = "Duel Tower" +dt_main = "Duel Tower" + +tower_of_sorcery = "Tower of Sorcery" +tosor_main = "Tower of Sorcery" + +tower_of_execution = "Tower of Execution" +toe_main = "Tower of Execution: main area" +toe_ledge = "Tower of Execution: gated ledge" + +tower_of_science = "Tower of Science" +tosci_start = "Tower of Science: turret lab" +tosci_three_doors = "Tower of Science: locked key1 room" +tosci_conveyors = "Tower of Science: spiky conveyors" +tosci_key3 = "Tower of Science: locked key3 room" + +room_of_clocks = "Room of Clocks" +roc_main = "Room of Clocks" + +clock_tower = "Clock Tower" +ct_start = "Clock Tower: start" +ct_middle = "Clock Tower: middle" +ct_end = "Clock Tower: end" + +castle_keep = "Castle Keep" +ck_main = "Castle Keep: exterior" +ck_drac_chamber = "Castle Keep: Dracula's chamber" + +renon = "Renon's shop" diff --git a/worlds/cv64/docs/en_Castlevania 64.md b/worlds/cv64/docs/en_Castlevania 64.md new file mode 100644 index 000000000000..5fe85555c40a --- /dev/null +++ b/worlds/cv64/docs/en_Castlevania 64.md @@ -0,0 +1,148 @@ +# Castlevania 64 + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +All items that you would normally pick up throughout the game, be it from candles, breakables, or sitting out, have been +moved around. This includes the key items that the player would normally need to find to progress in some stages, which can +now be found outside their own stages, so returning to previously-visited stages will very likely be necessary (see: [How do +I jump to a different stage?](#how-do-i-jump-to-a-different-stage?)). The positions of the stages can optionally be randomized +too, so you may start out in Duel Tower and get Forest of Silence as your penultimate stage before Castle Keep, amongst +many other possibilities. + +## How do I jump to a different stage? + +Instant travel to an earlier or later stage is made possible through the Warp Menu, a major addition to the game that can +be pulled up while not in a boss fight by pressing START while holding Z and R. By finding Special1 jewels (the item that +unlocks Hard Mode in vanilla Castlevania 64), more destinations become available to be selected on this menu. The destinations +on the list are randomized per seed and the first one, which requires no Special1s to select, will always be your starting +area. + +NOTE: Regardless of which option on the menu you are currently highlighting, you can hold Z or R while making your selection +to return to Villa's crypt or Castle Center's top elevator room respectively, provided you've already been to that place at +least once. This can make checking out both character stages at the start of a route divergence far less of a hassle. + +## Can I do everything as one character? + +Yes! The Villa end-of-level coffin has had its behavior modified so that which character stage slot it sends you to +depends on the time of day, and likewise both bridges at the top of Castle Center's elevator are intact so both exits are +reachable regardless of who you are. With these changes in game behavior, every stage can be accessed by any character +in a singular run. + +NOTE: By holding L while loading into a map (this can even be done while cancelling out of the Warp Menu), you can swap to +the other character you are not playing as, and/or hold C-Up to swap to and from the characters' alternate costumes. Unless +you have Carrie Logic enabled, and you are not playing as her, switching should never be necessary. + +## What is the goal of Castlevania 64 when randomized? + +Make it to Castle Keep, enter Dracula's chamber, and defeat him to trigger an ending and complete your goal. Whether you +get your character's good or bad ending does **not** matter; the goal will send regardless. Options exist to force a specific +ending for those who prefer a specific one. + +Dracula's chamber's entrance door is initially locked until whichever of the following objectives that was specified on your +YAML under `draculas_condition` is completed: +- `crystal`: Activate the big crystal in the basement of Castle Center. Doing this entails finding two Magical Nitros and +two Mandragoras to blow up both cracked walls (see: [How does the Nitro transport work in this?](#how-does-the-nitro-transport-work-in-this?)). +Behemoth and Rosa/Camilla do **NOT** have to be defeated. +- `bosses`: Kill bosses with visible health meters to earn Trophies. The number of Trophies required can be specified under +`bosses_required`. +- `special2s`: Find enough Special2 jewels (the item that normally unlocks alternate costumes) that are shuffled in the +regular item pool. The total amount and percent needed can be specified under `total_special2s` and `percent_special2s_required` respectively. + +If `none` was specified, then there is no objective. Dracula's chamber door is unlocked from the start, and you merely have to reach it. + +## What items and locations get shuffled? + +Inventory items, jewels, moneybags, and PowerUps are all placed in the item pool by default. Randomizing Sub-weapons is optional, +and they can be shuffled in their own separate pool or in the main item pool. An optional hack can be enabled to make your +old sub-weapon drop behind you when you receive a different one, so you can pick it up again if you still want it. Location +checks by default include freestanding items, items from one-hit breakables, and the very few items given through NPC text. Additional +locations that can be toggled are: +- Objects that break in three hits. +- Sub-weapon locations if they have been shuffled anywhere. +- Seven items sold by the shopkeeper Renon. +- The two items beyond the crawlspace in Waterway that normally require Carrie, if Carrie Logic is on. +- The six items inside the Lizard-man generators in Castle Center that open randomly to spawn Lizard-men. These are particularly annoying! + +## How does the Nitro transport work in this? + +Two Magical Nitros and two Mandragoras are placed into the item pool for blowing up the cracked walls in Castle Center +and two randomized items are placed on both of their shelves. The randomized Magical Nitro will **NOT** kill you upon landing +or taking damage, so don't panic when you receive one! Hazardous Waste Dispoal bins are disabled and the basement crack with +a seal will not let you set anything at it until said seal is removed so none of the limited ingredients can be wasted. + +In short, Nitro is still in, explode-y business is not! Unless you turn on explosive DeathLink, that is... + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. The exception is if sub-weapons +are shuffled in their own pool, in which case they will only appear in your world in sub-weapon spots. + +## What does another world's item look like in Castlevania 64? + +An item belonging to another world will show up as that item if it's from another Castlevania 64 world, or one of two +Archipelago logo icons if it's from a different game entirely. If the icon is big and has an orange arrow in the top-right +corner, it is a progression item for that world; definitely get these! Otherwise, if it's small and with no arrow, it is +either filler, useful, or a trap. + +When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you +found and who it was for. The color of the text will tell you its classification: +- Light brown-ish: Common +- White/Yellow: Useful +- Yellow/Green: Progression +- Yellow/Red: Trap + +## When the player receives an item, what happens? + +A textbox containing the name of the item and the player who sent it will appear, and they will get it. +Just like the textbox that appears when sending an item, the color of the text will tell you its classification. + +NOTE: You can press B to close the item textbox instantly and get through your item queue quicker. + +## What tricks and glitches should I know for Hard Logic? + +The following tricks always have a chance to be required: +- Left Tower Skip in Castle Wall +- Copper Door Skip in Villa (both characters have their own methods for this) +- Waterfall Skip if you travel backwards into Underground Waterway +- Slope Jump to Room of Clocks from Castle Keep +- Jump to the gated ledge from the level above in Tower of Execution + +Enabling Carrie Logic will also expect the following: + +- Orb-sniping dogs through the front gates in Villa + +Library Skip is **NOT** logically expected on any setting. The basement hallway crack will always logically expect two Nitros +and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing +to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room. + +## What are the item name groups? +The groups you can use for Castlevania 64 are `bomb` and `ingredient`, both of which will hint randomly for either a +Magical Nitro or Mandragora. + +## What are the location name groups? +In Castlevania 64, every location that is specific to a stage is part of a location group under that stage's name. +So if you want to exclude all of, say, Duel Tower, you can do so by just excluding "Duel Tower" as a whole. + +## I'm stuck and/or I can't find this hinted location...is there a map tracker? +At the moment, no map tracker exists. [Here](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds/cv64/docs/obscure_checks.md) +is a list of many checks that someone could very likely miss, with instructions on how to find them. See if the check you +are missing is on there and if it isn't, or you still can't find it, reach out in the [Archipelago Discord server](https://discord.gg/archipelago) +to inquire about having the list updated if you think it should be. + +If you are new to this randomizer, it is strongly recommended to play with the Countdown option enabled to at least give you a general +idea of where you should be looking if you get completely stuck. It can track the total number of unchecked locations in the +area you are currently in, or the total remaining majors. + +## Why does the game stop working when I sit on the title screen for too long? +This is an issue that existed with Castlevania 64 on mupen64plus way back in 2017, and BizHawk never updated their +mupen64plus core since it was fixed way back then. This is a Castlevania 64 in general problem that happens even with the +vanilla ROM, so there's not much that can done about it besides opening an issue to them (which [has been done](https://github.com/TASEmulators/BizHawk/issues/3670)) +and hoping they update their mupen64plus core one day... + +## How the f*** do I set Nitro/Mandragora? +(>) diff --git a/worlds/cv64/docs/obscure_checks.md b/worlds/cv64/docs/obscure_checks.md new file mode 100644 index 000000000000..4aafc2db1c5f --- /dev/null +++ b/worlds/cv64/docs/obscure_checks.md @@ -0,0 +1,429 @@ +# Obscure locations in the AP Castlevania 64 randomizer + + + +## Forest of Silence + +#### Invisible bridge platform +A square platform floating off to the side of the broken bridge between the three-crypt and Werewolf areas. There's an +invisible bridge connecting it with the middle piece on the broken bridge that you can cross over to reach it. This is +where you normally get the Special1 in vanilla that unlocks Hard Mode. + +### Invisible Items +#### Dirge maiden pedestal plaque +This plaque in question can be found on the statue pedestal with a torch on it in the area right after the first switch gate, +near the cliff where the breakable rock can be found. The plaque reads "A maiden sings a dirge" if you check it after you +pick up the item there, hence the name of all the locations in this area. + +#### Werewolf statue plaque +The plaque on the statue pedestal in the area inhabited by the Werewolf. Reading this plaque after picking up the item +says it's "the lady who blesses and restores." + +### 3-Hit Breakables +#### Dirge maiden pedestal rock +This rock can be found near the cliff behind the empty above-mentioned dirge maiden pedestal. Normally has a ton of money +in vanilla, contains 5 checks in rando. + +#### Bat archway rock +After the broken bridge containing the invisible pathway to the Special1 in vanilla, this rock is off to the side in front +of the gate frame with a swarm of bats that come at you, before the Werewolf's territory. Contains 4 checks. If you are new +to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guranteed spot to find a PowerUp at. + + + +## Castle Wall +#### Above bottom right/left tower door +These checks are located on top of the bottom doorways inside the both the Left and Right Tower. You have to drop from above +to reach them. In the case of the left tower, it's probably easiest to wait on the green platform directly above it until it flips. + +#### Left tower child ledge +When you reach the bridge of four rotating green platforms, look towards the pillar in the center of the room (hold C-up to +enter first person view), and you'll see this. There's an invisible bridge between the rotating platforms and the tiny ledge +that you can use to get to and from it. In Legacy of Darkness, it is on this ledge where one of Henry's children is found. + +### Invisible Items +#### Sandbag shelf - Left +If you thought the PowerUp on this shelf in vanilla CV64 was the only thing here, then you'd be wrong! Hidden inside the +sandbags near the item is another item you can pick up before subsequent checks on this spot yield "only sand and gravel". +Legacy took this item out entirely, interestingly enough. + +### 3-Hit Breakables +#### Upper rampart savepoint slab +After killing the two White Dragons and flipping their switch, drop down onto this platform from the top, and you'll find +it near the White Jewel. Contains 5 checks that are all normally Red Jewels in vanilla, making it an excellent place to +fuel up at if you're not doing Left Tower Skip. Just be careful of the infinitely spawning skeletons! + +#### Dracula switch slab +Located behind the door that you come out of at the top of the left tower where you encounter Totally Real Dracula in a +cutscene. Contains 5 checks that are all normally money; take note of all these money spots if you're plaing vanilla and +plan on trying to trigger the Renon fight. + + + +## Villa +#### Outer front gate platform +From the start of the level, turn right, and you'll see a platform with a torch above a torch on the ground. This upper torch +is reachable via an invisible platform that you can grab and pull yourself up onto. The PAL version and onwards removed +this secret entirely, interestingly enough. + +#### Front yard cross grave near gates/porch +In the Villa's front yard area are two cross-shaped grave markers that are actually 1-hit breakables just like torches. +They contain a check each. + +#### Midnight fountain +At exactly midnight (0:00 on the pause screen), a pillar in the fountain's base will rise to give you access to the six +checks on the upper parts of the fountain. If you're playing with Disable Time Requirements enabled, this pillar will be +raised regardless of the current time. + +#### Vincent +Vincent has a check that he will give to you by speaking to him after triggering the Rosa cutscene at 3 AM in the rose +garden. With Disable Time Requirements enabled, the Rosa cutscene will trigger at any time. + +#### Living room ceiling light +In the rectangular living room with ghosts and flying skulls that come at you, there are two yellow lights on the ceiling +and one red light between them. The red light can be broken for a check; just jump directly below it and use your c-left +attack to hit it. + +#### Front maze garden - Frankie's right dead-end urn +When you first enter the maze, before going to trigger the Malus cutscene, go forward, right at the one-way door, then right +at the T-junction, and you'll reach a dead-end where the Gardner is just going about his business. The urn on the left +at this dead-end can be broken for a check; it's the ONLY urn in the entire maze that can be broken like this. + +#### Crypt bridge upstream +After unlocking the Copper Door, follow the stream all the way past the bridge to end up at this torch. +I see many people miss this one. + +### Invisible Items +#### Front yard visitor's tombstone +The tombstone closest to the Villa building itself, in the top-right corner if approaching from the gates. If you are +familiar with the puzzle here in Cornell's quest in Legacy, it's the tombstone prepared for "anybody else who drops by +to visit". + +#### Foyer sofa +The first sofa in the foyer, on the upper floor to the right. + +#### Mary's room table +The table closer to the mirror on the right in the small room adjacent to the bedroom, where Mary would normally be found +in Cornell's story in Legacy. + +#### Dining room rose vase +The vase of roses in the dining room that a rose falls out of in the cutscene here to warn Reinhardt/Carrie of the vampire +villager. + +#### Living room clawed painting +The painting with claw marks on it above the fireplace in the middle of the living room. + +#### Living room lion head +The lion head on the left wall of the living room (if you entered from one of the doors to the main hallway). + +#### Maze garden exit knight +The suit of armor in the stairs room before the Maze Garden, where Renon normally introduces himself. + +#### Storeroom statue +The weird statue in the back of the Storeroom. If you check it again after taking its item, the game questions why would +someone make something like it. + +#### Archives table +The table in the middle of the Archives. In Legacy, this is where Oldrey's diary normally sits if you are playing Cornell. + +#### Malus's hiding bush +The bush that Reinhardt/Carrie find Malus hiding in at the start of the Maze Garden chase sequence. + +### 3-Hit Breakables +#### Foyer chandelier +The big chandelier above the foyer can be broken for 5 assorted items, all of which become checks in rando with the multi +hits setting on. This is the only 3-hit breakable in the entire stage.
+ +Here's a fun fact about the chandelier: for some reason, KCEK made this thing a completely separate object from every other +3-hit breakable in the game, complete with its own distinct code that I had to modify entirely separately as I was making +this rando to make the 3-hit breakable setting feasible! What fun! + + + +## Tunnel +#### Stepping stone alcove +After the first save following the initial Spider Women encounter, take the first right you see, and you'll arrive back at +the poison river where there's a second set of stepping stones similar to the one you just jumped across earlier. Jump on +these to find the secret alcove containing one or two checks depending on whether sub-weapons are randomized anywhere or not. + +### Sun/Moon Doors + +In total, there are six of these throughout the entire stage. One of them you are required to open in order to leave the stage, +while the other five lead to optional side rooms containing items. These are all very skippable in the vanilla game, but in a +rando context, it is obviously *very* important you learn where all of them are for when the day comes that a Hookshot +lands behind one! If it helps, two of them are before the gondolas, while all the rest are after them. + +#### Lonesome bucket moon door +After you ride up on the second elevator, turn left at the first split path you see, and you will find, as I called it, the +"Lonesome bucket". Keep moving forward past this, and you will arrive at the moon door. The only thing of value, beside a shop +point, is a sub-weapon location. So if you don't have sub-weapons shuffled anywhere, you can very much skip this one. + +#### Gondola rock crusher sun door +Once you get past the first poison pit that you are literally required to platform over, go forward at the next junction +instead of left (going left is progress and will take you to the rock crusher right before the gondolas). This door notably +hides two Roast Beefs normally, making it probably the most worthwhile one to visit in vanilla. + +#### Corpse bucket moon door +After the poison pit immediately following the gondola ride, you will arrive at a bucket surrounded by corpses (hence the name). +Go left here, and you will arrive at this door. + +#### Shovel zone moon door +On the straight path to the end-of-level sun door are two separate poison pits on the right that you can platform over. +Both of these lead to and from the same optional area, the "shovel zone" as I call it due to the random shovel you can find +here. Follow the path near the shovel that leads away from both poison pits, and you'll arrive at a junction with a save jewel. +Go straight on at this junction to arrive at this moon door. This particular one is more notable in Legacy of Darkness as it +contains one of the locations of Henry's children. + +#### Shovel zone sun door +Same as the above moon door, but go left at the save jewel junction instead of straight. + +### Invisible Items +#### Twin arrow signs +From the save point after the stepping stones following the initial Spider Women encounter, travel forward until you reach a +T-junction with two arrow signs at it. The right-pointing sign here contains an item on its post. + +#### Near lonesome bucket +After riding the first upwards elevator following turning left at the twin arrow signs, you'll arrive at the lonesome bucket +area, with said bucket being found if you turn left at the first opportunity after said elevator. The item here is not +found *in* the bucket, but rather on a completely unmarked spot some meters from it. This had to have been a mistake, +seeing as Legacy moved it to actually be in the bucket. + +#### Shovel +Can be found by taking either platforming course on the right side of the straightaway to the end after the gondolas. +This entire zone is noteable for the fact that there's no reason for Reinhardt to come here in either game; it's only ever +required for Henry to rescue one of his children. + +### 3-Hit Breakables +#### Twin arrow signs rock +Turn right at the twin arrow signs junction, and you'll find this rock at the dead-end by the river. It contains a bunch of +healing and status items that translate into 5 rando checks. + +#### Lonesome bucket poison pit rock +Near the lonesome bucket is the start of a platforming course over poison water that connects near Albert's campsite...which +you could reach anyway just by traveling forward at the prior junction instead of left. So what's the point of this poison +pit, then? Look out into the middle of it, and you'll see this rock on a tiny island out in the middle of it. If you choose +to take the hard way here, your reward will be three meat checks. + + + +## Underground Waterway +#### Carrie Crawlspace +This is located shortly after the corridor following the ledges that let you reach the first waterfall's source alcove. +Notably, only Carrie is able to crouch and go through this, making these the only checks in the *entire* game that are +hard impossible without being a specific character. So if you have Carrie Logic on and your character is Reinhardt, you'll +have to hold L while loading into a map to change to Carrie just for this one secret. If Carrie Logic is off, then these +locations will not be added and you can just skip them entirely. + +### 3-Hit Breakables +#### First poison parkour ledge +Near the start of the level is a series of ledges you can climb onto and platform across to reach a corner with a lantern +that you can normally get a Cure Ampoule from. The first of these ledges can be broken for an assortment of 6 things. + +#### Inside skeleton crusher ledge +To the left of the hallway entrance leading to the third switch is a long shimmy-able ledge that you can grab onto and shimmy +for a whole eternity (I implemented a setting JUST to make shimmying this ledge faster!) to get to a couple stand on-able ledges, +one of which has a lantern above it containing a check. This ledge can be broken for 3 chickens. I'd highly suggest bringing +Holy Water for this because otherwise you're forced to break it from the other, lower ledge that's here. And this ledge +will drop endless crawling skeletons on you as long as you're on it. + + + +## Castle Center +#### Atop elevator room machine +In the elevator room, right from the entrance coming in from the vampire triplets' room, is a machine that you can press +C-Right on to get dialog reading "An enormous machine." There's a torch on top of this machine that you can reach by +climbing onto the slanted part of the walls in the room. + +#### Heinrich Meyer +The friendly lizard-man who normally gives you the Chamber Key in vanilla has a check for you just like Vincent. +Yes, he has a name! And you'd best not forget it! + +#### Torture chamber rafters +A check can be found in the rafters in the room with the Mandragora shelf. Get onto and jump off the giant scythe or the +torture instrument shelf to make it up there. It's less annoying to do without Mandragora since having it will cause ghosts to +infinitely spawn in here. + +### Invisible Items +#### Red carpet hall knight +The suit of armor in the red carpet hallway after the bottom elevator room, directly next to the door leading into the +Lizard Locker Room. + +#### Lizard locker knight +The suit of armor in the Lizard Locker Room itself, directly across from the door connecting to the red carpet hallway. + +#### Broken staircase knight +The suit of armor in the broken staircase room following the Lizard Locker Room. + +#### Inside cracked wall hallway flamethrower +In the upper cracked wall hallway, it is in the lower flamethrower that is part of the pair between the Butler Bros. Room +and the main part of the hallway. + +#### Nitro room crates +The wall of crates in the Nitro room on Heinrich Meyer's side. This is notable for being one of the very rare Healing Kits +that you can get for free in vanilla. + +#### Hell Knight landing corner knight +The inactive suit of armor in the corner of the room before the Maid Sisters' Room, which also contains an active knight. + +#### Maid sisters room vase +The lone vase in the vampire Maid Sisters' Room, directly across from the door leading to the Hell Knight Landing. +Yes, you are actually supposed to *check* this with C-right to get its item; not break it like you did to the pots in +the Villa earlier! + +#### Invention room giant Famicart +The giant square-shaped thing in one corner of the invention room that looks vaguely like a massive video game cartridge. +A Famicom cartridge, perhaps? + +#### Invention room round machine +The brown circular machine in the invention room, close to the middle of the wall on the side of the Spike Crusher Room. + +#### Inside nitro hallway flamethrower +The lower flamethrower in the hallway between the Nitro room from the Spike Crusher Room, near the two doors to said rooms. + +#### Torture chamber instrument rack +The shelf full of torture instruments in the torture chamber, to the right of the Mandragora shelf. + +### 3-Hit Breakables +#### Behemoth arena crate +This large crate can be found in the back-right corner of Behemoth's arena and is pretty hard to miss. Break it to get 5 +moneybags-turned-checks. + +#### Elevator room unoccupied statue stand +In the bottom elevator room is a statue on a stand that will cry literal Bloody Tears if you get near it. On the opposite +side of the room from this, near the enormous machine, is a stand much like the one the aforementioned statue is on only +this one is completely vacant. This stand can be broken for 3 roast beefs-turned checks. + +#### Lizard locker room slab +In the Lizard Locker Room, on top of the second locker from the side of the room with the door to the red carpet hallway, +is a metallic box-like slab thingy that can be broken normally for 4 status items. This 3HB is notable for being one of two +funny ones in the game that does NOT set a flag when you break it in vanilla, meaning you can keep breaking it over and +over again for infinite Purifyings and Cure Ampoules! + +### The Lizard Lockers +If you turned on the Lizard Locker Items setting, then hoo boy, you are in for a FUN =) time! Inside each one of the six Lizard +Lockers is a check, and the way you get these checks is by camping near each of the lockers as you defeat said Lizards, +praying that one will emerge from it and cause it to open to give you a chance to grab it. It is *completely* luck-based, +you have a 1-in-6 (around 16%) chance per Lizard-man that emerges, and you have to repeat this process six times for each +check inside each locker. You can open and cancel the warp menu to make things easier, but other than that, enjoy playing +with the Lizards! + + + +## Duel Tower +#### Invisible bridge balcony +Located between Werewolf and Were-bull's arenas. Use an invisible bridge to reach it; it starts at the highest platform +on the same wall as it that appears after defeating Werewolf. The balcony contains two checks and a save point that I +added specifically for this rando to make the level less frustrating. + +#### Above Were-bull arena +The only check that can be permanently missed in the vanilla game depending on the order you do things, not counting any +points of no return. Between Werewolf and Were-bull's arenas is an alternate path that you can take downward and around +to avoid Were-bull completely and get on top of his still-raised arena, so you can reach this. In the rando, I set it up so +that his arena will go back to being raised if you leave the area and come back, and if you get the item later his arena flag +will be set then. If you're playing with Dracula's bosses condition, then you can only get one Special2 off of him the first +time you beat him and then none more after that. + + + +## Tower of Execution +#### Invisible bridge ledge +There are two ways to reach this one; use the invisible bridge that starts at the walkway above the entrance, or jump to +it from the Execution Key gate alcove. Collecting this Special2 in vanilla unlocks Reinhardt's alternate costume. + +#### Guillotine tower top level +This iron maiden is strategically placed in such a way that you will very likely miss it if you aren't looking carefully for +it, so I am including it here. When you make it to the top floor of the level, as you approach the tower for the final time, +look on the opposite side of it from where you approach it, and you will find this. The laggiest check in the whole game! +I'd dare someone to find some check in some other Archipelago game that lags harder than this. + +### 3-Hit Breakables +#### Pre-mid-savepoint platforms ledge +Here's a really weird one that even I never found about until well after I finished the 3HB setting and moved on to deving +other things, and I'd honestly be shocked if ANYONE knew about outside the context of this rando! This square-shaped +platform can be found right before the second set of expanding and retracting wall platforms, leading up to the mid-save +point, after going towards the central tower structure for the second time on the second floor. Breaking it will drop an +assortment of 5 items, one of which is notable for being the ONE sub-weapon that drops from a 3HB. This meant I had to really +change how things work to account for sub-weapons being in 3HBs! + + + +## Tower of Science +#### Invisible bridge platform +Following the hallway with a save jewel beyond the Science Key2 door, look to your right, and you'll see this. Mind the +gap separating the invisible bridge from the solid ground of the bottom part of this section! + +### 3-Hit Breakables +#### Invisible bridge platform crate +Near the candle on the above-mentioned invisible bridge platform is a small metallic crate. Break it for a total of 6 +checks, which in vanilla are 2 chickens, moneybags, and jewels. + + + +## Tower of Sorcery +#### Trick shot from mid-savepoint platform +From the platform with the save jewel, look back towards the vanishing red platforms that you had to conquer to get up +there, and you'll see a breakable diamond floating high above a solid platform before it. An ultra-precise orb shot from +Carrie can hit it to reveal the check, so you can then go down and get it. If you are playing as Reinhardt, you'll have +to use a sub-weapon. Any sub-weapon that's not Holy Water will work. Sub-weapons and the necessary jewels can both be +farmed off the local Icemen if it really comes down to it. + +#### Above yellow bubble +Directly above the yellow bubble that you break to raise the middle yellow large platform. Jump off the final red platform +in the series of red platforms right after the save point when said platform bobs all the way up, and you'll be able to +hit this diamond with good timing. + +#### Above tiny blue platforms start +Above the large platform after the yellow ones from whence you can reach the first of the series of tiny blue platforms. +This diamond is low enough that you can reach it by simply jumping straight up to it. + +#### Invisible bridge platform +Located at the very end of the stage, off to the side. Use the invisible bridge to reach it. The Special2 that unlocks +Carrie's costume can be normally found here (or Special3, depending on if you are playing the PAL version or not). + + + +## Clock Tower +All the normal items here and in Room of Clocks are self-explanatory, but the 3HBs in Clock Tower are a whoooooooole 'nother story. So buckle up... + +### 3-Hit Breakables +#### Gear climb room battery underside slab +In the first room, on the side you initially climb up, go up until you find a battery-like object that you can stand on +as a platform. From the platform right after this, you can hit this 3HB that can be found on the underside of the structure +in the corner, above the structure before the first set of gears that you initially start the climb on. 3 chickens/checks +can be gotten out of this one. + +#### Gear climb room door underside slab +Now THIS one can be very annoying to get, doubly so if you're playing as Reinhardt, triply so if you're playing Hard Mode +on top of that because you will also have to deal with invincible, bone-throwing red skeletons here! This slab can be +found on the underside of the platform in the first room with the door leading out into the second area and drops 3 +beefs/checks when you break it. Carrie is small enough to crouch under the platform and shoot the thing a few times +without *too* much hassle, but the only way I know of for Reinhardt to get it is to hang him off the side of the gear +and pull him up once he gets moved under the platform. Getting him to then face the breakable and whip it without falling +off due to the gear's rotation is next-to-impossible, I find, so the safest method to breaking it as him is to just rush +it with his sword, go back up, repeat 2 more times. Pray the aftermentioned Hard Mode skellies don't decide to cause *too* +much trouble during all of this! + +#### Final room entrance slab +Simply next to the entrance when you come into the third and final room from the intended way. Drops 2 moneybags-turned-checks, +which funnily normally have their item IDs shared with the other 3HB in this room's items, so I had to separate those for +the rando. + +#### Renon's final offers slab +At the top of the final room, on a platform near the Renon contract that would normally be the very last in the game. +This 3HB drops a whopping 8 items, more than any other 3HB in the entire game, and 6 of those are all moneybags. They +*really* shower you in gold in preparation for the finale, huh? + + + +## Castle Keep +#### Behind Dracula's chamber/Dracula's floating cube +This game continues the CV tradition of having a hidden secret around Dracula's chamber that you can get helpful things +from before the final battle begins. Jump onto the torch ledges and use the thin walkway to reach the backside of Dracula's +chamber where these can both be found. The floating cube, in question, can be reached with an invisible bridge. The other +candle here is noteworthy for being the sole jewel candle in vanilla that doesn't set a flag, meaning you can keep going +back down and up the stairs to farm infinite sub-weapon ammo for Dracula like in old school Castlevania! + +### Invisible Items +#### Left/Right Dracula door flame +Inside the torches on either side of the door to Dracula's chamber. Similar to the above-mentioned jewel torch, these do +not set flags. So you can get infinite healing kits for free by constantly going down and back up! \ No newline at end of file diff --git a/worlds/cv64/docs/setup_en.md b/worlds/cv64/docs/setup_en.md new file mode 100644 index 000000000000..6065b142c82c --- /dev/null +++ b/worlds/cv64/docs/setup_en.md @@ -0,0 +1,63 @@ +# Castlevania 64 Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) +- A Castlevania 64 ROM of the US 1.0 version specifically. The Archipelago community cannot provide this. +- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later + +### Configuring BizHawk + +Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings: + +- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from +`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.) +- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're +tabbed out of EmuHawk. +- Open a `.z64` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click +`Controllers…`, load any `.z64` ROM first. +- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to +clear it. +- All non-Japanese versions of the N64 Castlevanias require a Controller Pak to save game data. To enable this, while +you still have the `.z64` ROM loaded, go to `N64 > Controller Settings...`, click the dropdown by `Controller 1`, and +click `Memory Card`. You must then restart EmuHawk for it to take effect. +- After enabling the `Memory Card` setting, next time you boot up your Castlevania 64 ROM, you will see the +No "CASTLEVANIA" Note Found screen. Pick `Create "CASTLEVANIA" Note Now > Yes` to create save data and enable saving at +the White Jewels. + + +## Generating and Patching a Game + +1. Create your settings file (YAML). You can make one on the +[Castlevania 64 settings page](../../../games/Castlevania 64/player-settings). +2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). +This will generate an output file for you. Your patch file will have the `.apcv64` file extension. +3. Open `ArchipelagoLauncher.exe` +4. Select "Open Patch" on the left side and select your patch file. +5. If this is your first time patching, you will be prompted to locate your vanilla ROM. +6. A patched `.z64` file will be created in the same place as the patch file. +7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your +BizHawk install. + +If you're playing a single-player seed, and you don't care about hints, you can stop here, close the client, and load +the patched ROM in any emulator or EverDrive of your choice. However, for multiworlds and other Archipelago features, +continue below using BizHawk as your emulator. + +## Connecting to a Server + +By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just +in case you have to close and reopen a window mid-game for some reason. + +1. Castlevania 64 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game, +you can re-open it from the launcher. +2. Ensure EmuHawk is running the patched ROM. +3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing. +4. In the Lua Console window, go to `Script > Open Script…`. +5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`. +6. The emulator may freeze every few seconds until it manages to connect to the client. This is expected. The BizHawk +Client window should indicate that it connected and recognized Castlevania 64. +7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the +top text field of the client and click Connect. + +You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is +perfectly safe to make progress offline; everything will re-sync when you reconnect. diff --git a/worlds/cv64/entrances.py b/worlds/cv64/entrances.py new file mode 100644 index 000000000000..74537f92441b --- /dev/null +++ b/worlds/cv64/entrances.py @@ -0,0 +1,149 @@ +from .data import ename, iname, rname +from .stages import get_stage_info +from .options import CV64Options + +from typing import Dict, List, Tuple, Union + +# # # KEY # # # +# "connection" = The name of the Region the Entrance connects into. If it's a Tuple[str, str], we take the stage in +# active_stage_exits given in the second string and then the stage given in that stage's slot given in +# the first string, and take the start or end Region of that stage. +# "rule" = What rule should be applied to the Entrance during set_rules, as defined in self.rules in the CV64Rules class +# definition in rules.py. +# "add conds" = A list of player options conditions that must be satisfied for the Entrance to be added. Can be of +# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples, +# the first element is the name of the option, the second is the option value to check for, and the third +# is a boolean for whether we are evaluating for the option value or not. +entrance_info = { + # Forest of Silence + ename.forest_dbridge_gate: {"connection": rname.forest_mid}, + ename.forest_werewolf_gate: {"connection": rname.forest_end}, + ename.forest_end: {"connection": ("next", rname.forest_of_silence)}, + # Castle Wall + ename.cw_portcullis_c: {"connection": rname.cw_exit}, + ename.cw_lt_skip: {"connection": ("next", rname.castle_wall), "add conds": ["hard"]}, + ename.cw_lt_door: {"connection": rname.cw_ltower, "rule": iname.left_tower_key}, + ename.cw_end: {"connection": ("next", rname.castle_wall)}, + # Villa + ename.villa_dog_gates: {"connection": rname.villa_main}, + ename.villa_snipe_dogs: {"connection": rname.villa_start, "add conds": ["carrie", "hard"]}, + ename.villa_to_storeroom: {"connection": rname.villa_storeroom, "rule": iname.storeroom_key}, + ename.villa_to_archives: {"connection": rname.villa_archives, "rule": iname.archives_key}, + ename.villa_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.villa_to_maze: {"connection": rname.villa_maze, "rule": iname.garden_key}, + ename.villa_from_storeroom: {"connection": rname.villa_main, "rule": iname.storeroom_key}, + ename.villa_from_maze: {"connection": rname.villa_servants, "rule": iname.garden_key}, + ename.villa_servant_door: {"connection": rname.villa_main}, + ename.villa_copper_door: {"connection": rname.villa_crypt, "rule": iname.copper_key, + "add conds": ["not hard"]}, + ename.villa_copper_skip: {"connection": rname.villa_crypt, "add conds": ["hard"]}, + ename.villa_bridge_door: {"connection": rname.villa_maze}, + ename.villa_end_r: {"connection": ("next", rname.villa)}, + ename.villa_end_c: {"connection": ("alt", rname.villa)}, + # Tunnel + ename.tunnel_start_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.tunnel_gondolas: {"connection": rname.tunnel_end}, + ename.tunnel_end_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.tunnel_end: {"connection": ("next", rname.tunnel)}, + # Underground Waterway + ename.uw_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.uw_final_waterfall: {"connection": rname.uw_end}, + ename.uw_waterfall_skip: {"connection": rname.uw_main, "add conds": ["hard"]}, + ename.uw_end: {"connection": ("next", rname.underground_waterway)}, + # Castle Center + ename.cc_tc_door: {"connection": rname.cc_torture_chamber, "rule": iname.chamber_key}, + ename.cc_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.cc_lower_wall: {"connection": rname.cc_crystal, "rule": "Bomb 2"}, + ename.cc_upper_wall: {"connection": rname.cc_library, "rule": "Bomb 1"}, + ename.cc_elevator: {"connection": rname.cc_elev_top}, + ename.cc_exit_r: {"connection": ("next", rname.castle_center)}, + ename.cc_exit_c: {"connection": ("alt", rname.castle_center)}, + # Duel Tower + ename.dt_start: {"connection": ("prev", rname.duel_tower)}, + ename.dt_end: {"connection": ("next", rname.duel_tower)}, + # Tower of Execution + ename.toe_start: {"connection": ("prev", rname.tower_of_execution)}, + ename.toe_gate: {"connection": rname.toe_ledge, "rule": iname.execution_key, + "add conds": ["not hard"]}, + ename.toe_gate_skip: {"connection": rname.toe_ledge, "add conds": ["hard"]}, + ename.toe_end: {"connection": ("next", rname.tower_of_execution)}, + # Tower of Science + ename.tosci_start: {"connection": ("prev", rname.tower_of_science)}, + ename.tosci_key1_door: {"connection": rname.tosci_three_doors, "rule": iname.science_key1}, + ename.tosci_to_key2_door: {"connection": rname.tosci_conveyors, "rule": iname.science_key2}, + ename.tosci_from_key2_door: {"connection": rname.tosci_start, "rule": iname.science_key2}, + ename.tosci_key3_door: {"connection": rname.tosci_key3, "rule": iname.science_key3}, + ename.tosci_end: {"connection": ("next", rname.tower_of_science)}, + # Tower of Sorcery + ename.tosor_start: {"connection": ("prev", rname.tower_of_sorcery)}, + ename.tosor_end: {"connection": ("next", rname.tower_of_sorcery)}, + # Room of Clocks + ename.roc_gate: {"connection": ("next", rname.room_of_clocks)}, + # Clock Tower + ename.ct_to_door1: {"connection": rname.ct_middle, "rule": iname.clocktower_key1}, + ename.ct_from_door1: {"connection": rname.ct_start, "rule": iname.clocktower_key1}, + ename.ct_to_door2: {"connection": rname.ct_end, "rule": iname.clocktower_key2}, + ename.ct_from_door2: {"connection": rname.ct_middle, "rule": iname.clocktower_key2}, + ename.ct_renon: {"connection": rname.renon, "add conds": ["shopsanity"]}, + ename.ct_door_3: {"connection": ("next", rname.clock_tower), "rule": iname.clocktower_key3}, + # Castle Keep + ename.ck_slope_jump: {"connection": rname.roc_main, "add conds": ["hard"]}, + ename.ck_drac_door: {"connection": rname.ck_drac_chamber, "rule": "Dracula"} +} + +add_conds = {"carrie": ("carrie_logic", True, True), + "hard": ("hard_logic", True, True), + "not hard": ("hard_logic", False, True), + "shopsanity": ("shopsanity", True, True)} + +stage_connection_types = {"prev": "end region", + "next": "start region", + "alt": "start region"} + + +def get_entrance_info(entrance: str, info: str) -> Union[str, Tuple[str, str], List[str], None]: + return entrance_info[entrance].get(info, None) + + +def get_warp_entrances(active_warp_list: List[str]) -> Dict[str, str]: + # Create the starting stage Entrance. + warp_entrances = {get_stage_info(active_warp_list[0], "start region"): "Start stage"} + + # Create the warp Entrances. + for i in range(1, len(active_warp_list)): + mid_stage_region = get_stage_info(active_warp_list[i], "mid region") + warp_entrances.update({mid_stage_region: f"Warp {i}"}) + + return warp_entrances + + +def verify_entrances(options: CV64Options, entrances: List[str], + active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[str, str]: + verified_entrances = {} + + for ent_name in entrances: + ent_add_conds = get_entrance_info(ent_name, "add conds") + + # Check any options that might be associated with the Entrance before adding it. + add_it = True + if ent_add_conds is not None: + for cond in ent_add_conds: + if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]): + add_it = False + + if not add_it: + continue + + # Add the Entrance to the verified Entrances if the above check passes. + connection = get_entrance_info(ent_name, "connection") + + # If the Entrance is a connection to a different stage, get the corresponding other stage Region. + if isinstance(connection, tuple): + connecting_stage = active_stage_exits[connection[1]][connection[0]] + # Stages that lead backwards at the beginning of the line will appear leading to "Menu". + if connecting_stage in ["Menu", None]: + continue + connection = get_stage_info(connecting_stage, stage_connection_types[connection[0]]) + verified_entrances.update({connection: ent_name}) + + return verified_entrances diff --git a/worlds/cv64/items.py b/worlds/cv64/items.py new file mode 100644 index 000000000000..d40f5d53cb41 --- /dev/null +++ b/worlds/cv64/items.py @@ -0,0 +1,214 @@ +from BaseClasses import Item +from .data import iname +from .locations import base_id, get_location_info +from .options import DraculasCondition, SpareKeys + +from typing import TYPE_CHECKING, Dict, Union + +if TYPE_CHECKING: + from . import CV64World + +import math + + +class CV64Item(Item): + game: str = "Castlevania 64" + + +# # # KEY # # # +# "code" = The unique part of the Item's AP code attribute, as well as the value to call the in-game "prepare item +# textbox" function with to give the Item in-game. Add this + base_id to get the actual AP code. +# "default classification" = The AP Item Classification that gets assigned to instances of that Item in create_item +# by default, unless I deliberately override it (as is the case for some Special1s). +# "inventory offset" = What offset from the start of the in-game inventory array (beginning at 0x80389C4B) stores the +# current count for that Item. Used for start inventory purposes. +# "pickup actor id" = The ID for the Item's in-game Item pickup actor. If it's not in the Item's data dict, it's the +# same as the Item's code. This is what gets written in the ROM to replace non-NPC/shop items. +# "sub equip id" = For sub-weapons specifically, this is the number to put in the game's "current sub-weapon" value to +# indicate the player currently having that weapon. Used for start inventory purposes. +item_info = { + # White jewel + iname.red_jewel_s: {"code": 0x02, "default classification": "filler"}, + iname.red_jewel_l: {"code": 0x03, "default classification": "filler"}, + iname.special_one: {"code": 0x04, "default classification": "progression_skip_balancing", + "inventory offset": 0}, + iname.special_two: {"code": 0x05, "default classification": "progression_skip_balancing", + "inventory offset": 1}, + iname.roast_chicken: {"code": 0x06, "default classification": "filler", "inventory offset": 2}, + iname.roast_beef: {"code": 0x07, "default classification": "filler", "inventory offset": 3}, + iname.healing_kit: {"code": 0x08, "default classification": "useful", "inventory offset": 4}, + iname.purifying: {"code": 0x09, "default classification": "filler", "inventory offset": 5}, + iname.cure_ampoule: {"code": 0x0A, "default classification": "filler", "inventory offset": 6}, + # pot-pourri + iname.powerup: {"code": 0x0C, "default classification": "filler"}, + iname.permaup: {"code": 0x10C, "default classification": "useful", "pickup actor id": 0x0C, + "inventory offset": 8}, + iname.knife: {"code": 0x0D, "default classification": "filler", "pickup actor id": 0x10, + "sub equip id": 1}, + iname.holy_water: {"code": 0x0E, "default classification": "filler", "pickup actor id": 0x0D, + "sub equip id": 2}, + iname.cross: {"code": 0x0F, "default classification": "filler", "pickup actor id": 0x0E, + "sub equip id": 3}, + iname.axe: {"code": 0x10, "default classification": "filler", "pickup actor id": 0x0F, + "sub equip id": 4}, + # Wooden stake (AP item) + iname.ice_trap: {"code": 0x12, "default classification": "trap"}, + # The contract + # engagement ring + iname.magical_nitro: {"code": 0x15, "default classification": "progression", "inventory offset": 17}, + iname.mandragora: {"code": 0x16, "default classification": "progression", "inventory offset": 18}, + iname.sun_card: {"code": 0x17, "default classification": "filler", "inventory offset": 19}, + iname.moon_card: {"code": 0x18, "default classification": "filler", "inventory offset": 20}, + # Incandescent gaze + iname.archives_key: {"code": 0x1A, "default classification": "progression", "pickup actor id": 0x1D, + "inventory offset": 22}, + iname.left_tower_key: {"code": 0x1B, "default classification": "progression", "pickup actor id": 0x1E, + "inventory offset": 23}, + iname.storeroom_key: {"code": 0x1C, "default classification": "progression", "pickup actor id": 0x1F, + "inventory offset": 24}, + iname.garden_key: {"code": 0x1D, "default classification": "progression", "pickup actor id": 0x20, + "inventory offset": 25}, + iname.copper_key: {"code": 0x1E, "default classification": "progression", "pickup actor id": 0x21, + "inventory offset": 26}, + iname.chamber_key: {"code": 0x1F, "default classification": "progression", "pickup actor id": 0x22, + "inventory offset": 27}, + iname.execution_key: {"code": 0x20, "default classification": "progression", "pickup actor id": 0x23, + "inventory offset": 28}, + iname.science_key1: {"code": 0x21, "default classification": "progression", "pickup actor id": 0x24, + "inventory offset": 29}, + iname.science_key2: {"code": 0x22, "default classification": "progression", "pickup actor id": 0x25, + "inventory offset": 30}, + iname.science_key3: {"code": 0x23, "default classification": "progression", "pickup actor id": 0x26, + "inventory offset": 31}, + iname.clocktower_key1: {"code": 0x24, "default classification": "progression", "pickup actor id": 0x27, + "inventory offset": 32}, + iname.clocktower_key2: {"code": 0x25, "default classification": "progression", "pickup actor id": 0x28, + "inventory offset": 33}, + iname.clocktower_key3: {"code": 0x26, "default classification": "progression", "pickup actor id": 0x29, + "inventory offset": 34}, + iname.five_hundred_gold: {"code": 0x27, "default classification": "filler", "pickup actor id": 0x1A}, + iname.three_hundred_gold: {"code": 0x28, "default classification": "filler", "pickup actor id": 0x1B}, + iname.one_hundred_gold: {"code": 0x29, "default classification": "filler", "pickup actor id": 0x1C}, + iname.crystal: {"default classification": "progression"}, + iname.trophy: {"default classification": "progression"}, + iname.victory: {"default classification": "progression"} +} + +filler_item_names = [iname.red_jewel_s, iname.red_jewel_l, iname.five_hundred_gold, iname.three_hundred_gold, + iname.one_hundred_gold] + + +def get_item_info(item: str, info: str) -> Union[str, int, None]: + return item_info[item].get(info, None) + + +def get_item_names_to_ids() -> Dict[str, int]: + return {name: get_item_info(name, "code")+base_id for name in item_info if get_item_info(name, "code") is not None} + + +def get_item_counts(world: "CV64World") -> Dict[str, Dict[str, int]]: + + active_locations = world.multiworld.get_unfilled_locations(world.player) + + item_counts = { + "progression": {}, + "progression_skip_balancing": {}, + "useful": {}, + "filler": {}, + "trap": {} + } + total_items = 0 + extras_count = 0 + + # Get from each location its vanilla item and add it to the default item counts. + for loc in active_locations: + if loc.address is None: + continue + + if world.options.hard_item_pool and get_location_info(loc.name, "hard item") is not None: + item_to_add = get_location_info(loc.name, "hard item") + else: + item_to_add = get_location_info(loc.name, "normal item") + + classification = get_item_info(item_to_add, "default classification") + + if item_to_add not in item_counts[classification]: + item_counts[classification][item_to_add] = 1 + else: + item_counts[classification][item_to_add] += 1 + total_items += 1 + + # Replace all but 2 PowerUps with junk if Permanent PowerUps is on and mark those two PowerUps as Useful. + if world.options.permanent_powerups: + for i in range(item_counts["filler"][iname.powerup] - 2): + item_counts["filler"][world.get_filler_item_name()] += 1 + del(item_counts["filler"][iname.powerup]) + item_counts["useful"][iname.permaup] = 2 + + # Add the total Special1s. + item_counts["progression_skip_balancing"][iname.special_one] = world.options.total_special1s.value + extras_count += world.options.total_special1s.value + + # Add the total Special2s if Dracula's Condition is Special2s. + if world.options.draculas_condition == DraculasCondition.option_specials: + item_counts["progression_skip_balancing"][iname.special_two] = world.options.total_special2s.value + extras_count += world.options.total_special2s.value + + # Determine the extra key counts if applicable. Doing this before moving Special1s will ensure only the keys and + # bomb components are affected by this. + for key in item_counts["progression"]: + spare_keys = 0 + if world.options.spare_keys == SpareKeys.option_on: + spare_keys = item_counts["progression"][key] + elif world.options.spare_keys == SpareKeys.option_chance: + if item_counts["progression"][key] > 0: + for i in range(item_counts["progression"][key]): + spare_keys += world.random.randint(0, 1) + item_counts["progression"][key] += spare_keys + extras_count += spare_keys + + # Move the total number of Special1s needed to warp everywhere to normal progression balancing if S1s per warp is + # 3 or lower. + if world.s1s_per_warp <= 3: + item_counts["progression_skip_balancing"][iname.special_one] -= world.s1s_per_warp * 7 + item_counts["progression"][iname.special_one] = world.s1s_per_warp * 7 + + # Determine the total amounts of replaceable filler and non-filler junk. + total_filler_junk = 0 + total_non_filler_junk = 0 + for junk in item_counts["filler"]: + if junk in filler_item_names: + total_filler_junk += item_counts["filler"][junk] + else: + total_non_filler_junk += item_counts["filler"][junk] + + # Subtract from the filler counts total number of "extra" items we've added. get_filler_item_name() filler will be + # subtracted from first until we run out of that, at which point we'll start subtracting from the rest. At this + # moment, non-filler item name filler cannot run out no matter the settings, so I haven't bothered adding handling + # for when it does yet. + available_filler_junk = filler_item_names.copy() + for i in range(extras_count): + if total_filler_junk > 0: + total_filler_junk -= 1 + item_to_subtract = world.random.choice(available_filler_junk) + else: + total_non_filler_junk -= 1 + item_to_subtract = world.random.choice(list(item_counts["filler"].keys())) + + item_counts["filler"][item_to_subtract] -= 1 + if item_counts["filler"][item_to_subtract] == 0: + del(item_counts["filler"][item_to_subtract]) + if item_to_subtract in available_filler_junk: + available_filler_junk.remove(item_to_subtract) + + # Determine the Ice Trap count by taking a certain % of the total filler remaining at this point. + item_counts["trap"][iname.ice_trap] = math.floor((total_filler_junk + total_non_filler_junk) * + (world.options.ice_trap_percentage.value / 100.0)) + for i in range(item_counts["trap"][iname.ice_trap]): + # Subtract the remaining filler after determining the ice trap count. + item_to_subtract = world.random.choice(list(item_counts["filler"].keys())) + item_counts["filler"][item_to_subtract] -= 1 + if item_counts["filler"][item_to_subtract] == 0: + del (item_counts["filler"][item_to_subtract]) + + return item_counts diff --git a/worlds/cv64/locations.py b/worlds/cv64/locations.py new file mode 100644 index 000000000000..264f2f7c0b9c --- /dev/null +++ b/worlds/cv64/locations.py @@ -0,0 +1,699 @@ +from BaseClasses import Location +from .data import lname, iname +from .options import CV64Options, SubWeaponShuffle, DraculasCondition, RenonFightCondition, VincentFightCondition + +from typing import Dict, Optional, Union, List, Tuple + +base_id = 0xC64000 + + +class CV64Location(Location): + game: str = "Castlevania 64" + + +# # # KEY # # # +# "code" = The unique part of the Location's AP code attribute, as well as the in-game bitflag index starting from +# 0x80389BE4 that indicates the Location has been checked. Add this + base_id to get the actual AP code. +# "offset" = The offset in the ROM to overwrite to change the Item on that Location. +# "normal item" = The Item normally there in vanilla on most difficulties in most versions of the game. Used to +# determine the World's Item counts by checking what Locations are active. +# "hard item" = The Item normally there in Hard Mode in the PAL version of CV64 specifically. Used instead of the +# normal Item when the hard Item pool is enabled if it's in the Location's data dict. +# "add conds" = A list of player options conditions that must be satisfied for the Location to be added. Can be of +# varying length depending on how many conditions need to be satisfied. In the add_conds dict's tuples, +# the first element is the name of the option, the second is the option value to check for, and the third +# is a boolean for whether we are evaluating for the option value or not. +# "event" = What event Item to place on that Location, for Locations that are events specifically. +# "countdown" = What Countdown number in the array of Countdown numbers that Location contributes to. For the most part, +# this is figured out by taking that Location's corresponding stage's postion in the vanilla stage order, +# but there are some exceptions made for Locations in parts of Villa and Castle Center that split off into +# their own numbers. +# "type" = Anything special about this Location in-game, whether it be NPC-given, invisible, etc. +location_info = { + # Forest of Silence + lname.forest_pillars_right: {"code": 0x1C, "offset": 0x10C67B, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.forest_pillars_left: {"code": 0x46, "offset": 0x10C6EB, "normal item": iname.knife, + "add conds": ["sub"]}, + lname.forest_pillars_top: {"code": 0x13, "offset": 0x10C71B, "normal item": iname.roast_beef, + "hard item": iname.red_jewel_l}, + lname.forest_boss_one: {"event": iname.trophy, "add conds": ["boss"]}, + lname.forest_king_skeleton: {"code": 0xC, "offset": 0x10C6BB, "normal item": iname.five_hundred_gold}, + lname.forest_lgaz_in: {"code": 0x1A, "offset": 0x10C68B, "normal item": iname.moon_card}, + lname.forest_lgaz_top: {"code": 0x19, "offset": 0x10C693, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.forest_hgaz_in: {"code": 0xB, "offset": 0x10C6C3, "normal item": iname.sun_card}, + lname.forest_hgaz_top: {"code": 0x3, "offset": 0x10C6E3, "normal item": iname.roast_chicken, + "hard item": iname.five_hundred_gold}, + lname.forest_weretiger_sw: {"code": 0xA, "offset": 0x10C6CB, "normal item": iname.five_hundred_gold}, + lname.forest_boss_two: {"event": iname.trophy, "add conds": ["boss"]}, + lname.forest_weretiger_gate: {"code": 0x7, "offset": 0x10C683, "normal item": iname.powerup}, + lname.forest_dirge_tomb_l: {"code": 0x59, "offset": 0x10C74B, "normal item": iname.one_hundred_gold, + "add conds": ["empty"]}, + lname.forest_dirge_tomb_u: {"code": 0x8, "offset": 0x10C743, "normal item": iname.one_hundred_gold}, + lname.forest_dirge_plaque: {"code": 0x6, "offset": 0x7C7F9D, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold, "type": "inv"}, + lname.forest_dirge_ped: {"code": 0x45, "offset": 0x10C6FB, "normal item": iname.cross, + "add conds": ["sub"]}, + lname.forest_dirge_rock1: {"code": 0x221, "offset": 0x10C791, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.forest_dirge_rock2: {"code": 0x222, "offset": 0x10C793, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.forest_dirge_rock3: {"code": 0x223, "offset": 0x10C795, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.forest_dirge_rock4: {"code": 0x224, "offset": 0x10C797, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.forest_dirge_rock5: {"code": 0x225, "offset": 0x10C799, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.forest_corpse_save: {"code": 0xF, "offset": 0x10C6A3, "normal item": iname.red_jewel_s}, + lname.forest_dbridge_wall: {"code": 0x18, "offset": 0x10C69B, "normal item": iname.red_jewel_s}, + lname.forest_dbridge_sw: {"code": 0x9, "offset": 0x10C6D3, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold}, + lname.forest_dbridge_gate_l: {"code": 0x44, "offset": 0x10C6F3, "normal item": iname.axe, "add conds": ["sub"]}, + lname.forest_dbridge_gate_r: {"code": 0xE, "offset": 0x10C6AB, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.forest_dbridge_tomb_l: {"code": 0xEA, "offset": 0x10C763, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.forest_dbridge_tomb_ur: {"code": 0xE4, "offset": 0x10C773, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.forest_dbridge_tomb_uf: {"code": 0x1B, "offset": 0x10C76B, "normal item": iname.red_jewel_s}, + lname.forest_bface_tomb_lf: {"code": 0x10, "offset": 0x10C75B, "normal item": iname.roast_chicken}, + lname.forest_bface_tomb_lr: {"code": 0x58, "offset": 0x10C753, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.forest_bface_tomb_u: {"code": 0x1E, "offset": 0x10C77B, "normal item": iname.one_hundred_gold}, + lname.forest_ibridge: {"code": 0x2, "offset": 0x10C713, "normal item": iname.one_hundred_gold}, + lname.forest_bridge_rock1: {"code": 0x227, "offset": 0x10C79D, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.forest_bridge_rock2: {"code": 0x228, "offset": 0x10C79F, "normal item": iname.five_hundred_gold, + "hard item": iname.three_hundred_gold, "add conds": ["3hb"]}, + lname.forest_bridge_rock3: {"code": 0x229, "offset": 0x10C7A1, "normal item": iname.powerup, + "hard item": iname.three_hundred_gold, "add conds": ["3hb"]}, + lname.forest_bridge_rock4: {"code": 0x22A, "offset": 0x10C7A3, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.forest_werewolf_tomb_lf: {"code": 0xE7, "offset": 0x10C783, "normal item": iname.one_hundred_gold, + "add conds": ["empty"]}, + lname.forest_werewolf_tomb_lr: {"code": 0xE6, "offset": 0x10C73B, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.forest_werewolf_tomb_r: {"code": 0x4, "offset": 0x10C733, "normal item": iname.sun_card}, + lname.forest_werewolf_plaque: {"code": 0x1, "offset": 0xBFC8AF, "normal item": iname.roast_chicken, + "type": "inv"}, + lname.forest_werewolf_tree: {"code": 0xD, "offset": 0x10C6B3, "normal item": iname.red_jewel_s}, + lname.forest_werewolf_island: {"code": 0x41, "offset": 0x10C703, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.forest_final_sw: {"code": 0x12, "offset": 0x10C72B, "normal item": iname.roast_beef}, + lname.forest_boss_three: {"event": iname.trophy, "add conds": ["boss"]}, + + # Castle Wall + lname.cwr_bottom: {"code": 0x1DD, "offset": 0x10C7E7, "normal item": iname.sun_card, + "hard item": iname.one_hundred_gold}, + lname.cw_dragon_sw: {"code": 0x153, "offset": 0x10C817, "normal item": iname.roast_chicken}, + lname.cw_boss: {"event": iname.trophy, "add conds": ["boss"]}, + lname.cw_save_slab1: {"code": 0x22C, "offset": 0x10C84D, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.cw_save_slab2: {"code": 0x22D, "offset": 0x10C84F, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.cw_save_slab3: {"code": 0x22E, "offset": 0x10C851, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.cw_save_slab4: {"code": 0x22F, "offset": 0x10C853, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.cw_save_slab5: {"code": 0x230, "offset": 0x10C855, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.cw_rrampart: {"code": 0x156, "offset": 0x10C7FF, "normal item": iname.five_hundred_gold}, + lname.cw_lrampart: {"code": 0x155, "offset": 0x10C807, "normal item": iname.moon_card, + "hard item": iname.one_hundred_gold}, + lname.cw_pillar: {"code": 0x14D, "offset": 0x7F9A0F, "normal item": iname.holy_water, "add conds": ["sub"]}, + lname.cw_shelf_visible: {"code": 0x158, "offset": 0x7F99A9, "normal item": iname.powerup}, + lname.cw_shelf_sandbags: {"code": 0x14E, "offset": 0x7F9A3E, "normal item": iname.five_hundred_gold, "type": "inv"}, + lname.cw_shelf_torch: {"code": 0x14C, "offset": 0x10C82F, "normal item": iname.cross, "add conds": ["sub"]}, + lname.cw_ground_left: {"code": 0x14B, "offset": 0x10C827, "normal item": iname.knife, "add conds": ["sub"]}, + lname.cw_ground_middle: {"code": 0x159, "offset": 0x10C7F7, "normal item": iname.left_tower_key}, + lname.cw_ground_right: {"code": 0x14A, "offset": 0x10C81F, "normal item": iname.axe, "add conds": ["sub"]}, + lname.cwl_bottom: {"code": 0x1DE, "offset": 0x10C7DF, "normal item": iname.moon_card}, + lname.cwl_bridge: {"code": 0x1DC, "offset": 0x10C7EF, "normal item": iname.roast_beef}, + lname.cw_drac_sw: {"code": 0x154, "offset": 0x10C80F, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold}, + lname.cw_drac_slab1: {"code": 0x232, "offset": 0x10C859, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.cw_drac_slab2: {"code": 0x233, "offset": 0x10C85B, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.cw_drac_slab3: {"code": 0x234, "offset": 0x10C85D, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.cw_drac_slab4: {"code": 0x235, "offset": 0x10C85F, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.cw_drac_slab5: {"code": 0x236, "offset": 0x10C861, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + # Villa + lname.villafy_outer_gate_l: {"code": 0x133, "offset": 0x10C87F, "normal item": iname.red_jewel_l}, + lname.villafy_outer_gate_r: {"code": 0x132, "offset": 0x10C887, "normal item": iname.red_jewel_l}, + lname.villafy_dog_platform: {"code": 0x134, "offset": 0x10C89F, "normal item": iname.red_jewel_l}, + lname.villafy_inner_gate: {"code": 0x138, "offset": 0xBFC8D7, "normal item": iname.roast_beef}, + lname.villafy_gate_marker: {"code": 0x131, "offset": 0x10C8A7, "normal item": iname.powerup, + "hard item": iname.one_hundred_gold}, + lname.villafy_villa_marker: {"code": 0x13E, "offset": 0x10C897, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold}, + lname.villafy_tombstone: {"code": 0x12F, "offset": 0x8099CC, "normal item": iname.moon_card, + "type": "inv"}, + lname.villafy_fountain_fl: {"code": 0x139, "offset": 0xBFC8CF, "normal item": iname.five_hundred_gold}, + lname.villafy_fountain_fr: {"code": 0x130, "offset": 0x80997D, "normal item": iname.purifying}, + lname.villafy_fountain_ml: {"code": 0x13A, "offset": 0x809956, "normal item": iname.sun_card}, + lname.villafy_fountain_mr: {"code": 0x13D, "offset": 0x80992D, "normal item": iname.moon_card}, + lname.villafy_fountain_rl: {"code": 0x13B, "offset": 0xBFC8D3, "normal item": iname.roast_beef, + "hard item": iname.five_hundred_gold}, + lname.villafy_fountain_rr: {"code": 0x13C, "offset": 0x80993C, "normal item": iname.five_hundred_gold}, + lname.villafo_front_r: {"code": 0x3D, "offset": 0x10C8E7, "normal item": iname.red_jewel_l, + "hard item": iname.five_hundred_gold}, + lname.villafo_front_l: {"code": 0x3B, "offset": 0x10C8DF, "normal item": iname.red_jewel_s}, + lname.villafo_mid_l: {"code": 0x3C, "offset": 0x10C8D7, "normal item": iname.red_jewel_s}, + lname.villafo_mid_r: {"code": 0xE5, "offset": 0x10C8CF, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.villafo_rear_r: {"code": 0x38, "offset": 0x10C8C7, "normal item": iname.red_jewel_s}, + lname.villafo_rear_l: {"code": 0x39, "offset": 0x10C8BF, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.villafo_pot_r: {"code": 0x2E, "offset": 0x10C8AF, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.villafo_pot_l: {"code": 0x2F, "offset": 0x10C8B7, "normal item": iname.red_jewel_s}, + lname.villafo_sofa: {"code": 0x2D, "offset": 0x81F07C, "normal item": iname.purifying, + "type": "inv"}, + lname.villafo_chandelier1: {"code": 0x27D, "offset": 0x10C8F5, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.villafo_chandelier2: {"code": 0x27E, "offset": 0x10C8F7, "normal item": iname.purifying, + "add conds": ["3hb"]}, + lname.villafo_chandelier3: {"code": 0x27F, "offset": 0x10C8F9, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.villafo_chandelier4: {"code": 0x280, "offset": 0x10C8FB, "normal item": iname.cure_ampoule, + "add conds": ["3hb"]}, + lname.villafo_chandelier5: {"code": 0x281, "offset": 0x10C8FD, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + lname.villala_hallway_stairs: {"code": 0x34, "offset": 0x10C927, "normal item": iname.red_jewel_l}, + lname.villala_hallway_l: {"code": 0x40, "offset": 0xBFC903, "normal item": iname.knife, + "add conds": ["sub"]}, + lname.villala_hallway_r: {"code": 0x4F, "offset": 0xBFC8F7, "normal item": iname.axe, + "add conds": ["sub"]}, + lname.villala_bedroom_chairs: {"code": 0x33, "offset": 0x83A588, "normal item": iname.purifying, + "hard item": iname.three_hundred_gold}, + lname.villala_bedroom_bed: {"code": 0x32, "offset": 0xBFC95B, "normal item": iname.red_jewel_l, + "hard item": iname.three_hundred_gold}, + lname.villala_vincent: {"code": 0x23, "offset": 0xBFE42F, "normal item": iname.archives_key, + "type": "npc"}, + lname.villala_slivingroom_table: {"code": 0x2B, "offset": 0xBFC96B, "normal item": iname.five_hundred_gold, + "type": "inv"}, + lname.villala_slivingroom_mirror: {"code": 0x49, "offset": 0x83A5D9, "normal item": iname.cross, + "add conds": ["sub"]}, + lname.villala_diningroom_roses: {"code": 0x2A, "offset": 0xBFC90B, "normal item": iname.purifying, + "hard item": iname.three_hundred_gold, "type": "inv"}, + lname.villala_llivingroom_pot_r: {"code": 0x26, "offset": 0x10C90F, "normal item": iname.storeroom_key}, + lname.villala_llivingroom_pot_l: {"code": 0x25, "offset": 0x10C917, "normal item": iname.roast_chicken}, + lname.villala_llivingroom_painting: {"code": 0x2C, "offset": 0xBFC907, "normal item": iname.purifying, + "hard item": iname.one_hundred_gold, "type": "inv"}, + lname.villala_llivingroom_light: {"code": 0x28, "offset": 0x10C91F, "normal item": iname.purifying}, + lname.villala_llivingroom_lion: {"code": 0x30, "offset": 0x83A610, "normal item": iname.roast_chicken, + "hard item": iname.five_hundred_gold, "type": "inv"}, + lname.villala_exit_knight: {"code": 0x27, "offset": 0xBFC967, "normal item": iname.purifying, + "type": "inv"}, + lname.villala_storeroom_l: {"code": 0x36, "offset": 0xBFC95F, "normal item": iname.roast_beef}, + lname.villala_storeroom_r: {"code": 0x37, "offset": 0xBFC8FF, "normal item": iname.roast_chicken, + "hard item": iname.five_hundred_gold}, + lname.villala_storeroom_s: {"code": 0x31, "offset": 0xBFC963, "normal item": iname.purifying, + "hard item": iname.one_hundred_gold, "type": "inv"}, + lname.villala_archives_entrance: {"code": 0x48, "offset": 0x83A5E5, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.villala_archives_table: {"code": 0x29, "offset": 0xBFC90F, "normal item": iname.purifying, + "type": "inv"}, + lname.villala_archives_rear: {"code": 0x24, "offset": 0x83A5B1, "normal item": iname.garden_key}, + lname.villam_malus_torch: {"code": 0x173, "offset": 0x10C967, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villam_malus_bush: {"code": 0x16C, "offset": 0x850FEC, "normal item": iname.roast_chicken, + "type": "inv", "countdown": 13}, + lname.villam_fplatform: {"code": 0x16B, "offset": 0x10C987, "normal item": iname.knife, + "add conds": ["sub"], "countdown": 13}, + lname.villam_frankieturf_l: {"code": 0x177, "offset": 0x10C947, "normal item": iname.three_hundred_gold, + "countdown": 13}, + lname.villam_frankieturf_r: {"code": 0x16A, "offset": 0x10C98F, "normal item": iname.holy_water, + "add conds": ["sub"], "countdown": 13}, + lname.villam_frankieturf_ru: {"code": 0x16E, "offset": 0x10C9A7, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villam_fgarden_f: {"code": 0x172, "offset": 0x10C96F, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villam_fgarden_mf: {"code": 0x171, "offset": 0x10C977, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villam_fgarden_mr: {"code": 0x174, "offset": 0x10C95F, "normal item": iname.roast_chicken, + "countdown": 13}, + lname.villam_fgarden_r: {"code": 0x170, "offset": 0x10C97F, "normal item": iname.red_jewel_l, + "countdown": 13}, + lname.villam_rplatform: {"code": 0x169, "offset": 0x10C997, "normal item": iname.axe, + "add conds": ["sub"], "countdown": 13}, + lname.villam_rplatform_de: {"code": 0x176, "offset": 0x10C94F, "normal item": iname.five_hundred_gold, + "countdown": 13}, + lname.villam_exit_de: {"code": 0x175, "offset": 0x10C957, "normal item": iname.three_hundred_gold, + "countdown": 13}, + lname.villam_serv_path: {"code": 0x17A, "offset": 0x10C92F, "normal item": iname.copper_key, + "countdown": 13}, + lname.villafo_serv_ent: {"code": 0x3E, "offset": 0x10C8EF, "normal item": iname.roast_chicken}, + lname.villam_crypt_ent: {"code": 0x178, "offset": 0x10C93F, "normal item": iname.purifying, + "countdown": 13}, + lname.villam_crypt_upstream: {"code": 0x179, "offset": 0x10C937, "normal item": iname.roast_beef, + "countdown": 13}, + lname.villac_ent_l: {"code": 0xC9, "offset": 0x10CF4B, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villac_ent_r: {"code": 0xC0, "offset": 0x10CF63, "normal item": iname.five_hundred_gold, + "countdown": 13}, + lname.villac_wall_l: {"code": 0xC2, "offset": 0x10CF6B, "normal item": iname.roast_chicken, + "countdown": 13}, + lname.villac_wall_r: {"code": 0xC1, "offset": 0x10CF5B, "normal item": iname.red_jewel_l, + "countdown": 13}, + lname.villac_coffin_l: {"code": 0xD8, "offset": 0x10CF73, "normal item": iname.knife, + "add conds": ["sub"], "countdown": 13}, + lname.villac_coffin_r: {"code": 0xC8, "offset": 0x10CF53, "normal item": iname.red_jewel_s, + "countdown": 13}, + lname.villa_boss_one: {"event": iname.trophy, "add conds": ["boss"]}, + lname.villa_boss_two: {"event": iname.trophy, "add conds": ["boss"]}, + # Tunnel + lname.tunnel_landing: {"code": 0x197, "offset": 0x10C9AF, "normal item": iname.red_jewel_l, + "hard item": iname.one_hundred_gold}, + lname.tunnel_landing_rc: {"code": 0x196, "offset": 0x10C9B7, "normal item": iname.red_jewel_s, + "hard item": iname.one_hundred_gold}, + lname.tunnel_stone_alcove_r: {"code": 0xE1, "offset": 0x10CA57, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.tunnel_stone_alcove_l: {"code": 0x187, "offset": 0x10CA9F, "normal item": iname.roast_beef, + "hard item": iname.roast_chicken}, + lname.tunnel_twin_arrows: {"code": 0x195, "offset": 0xBFC993, "normal item": iname.cure_ampoule, + "type": "inv"}, + lname.tunnel_arrows_rock1: {"code": 0x238, "offset": 0x10CABD, "normal item": iname.purifying, + "add conds": ["3hb"]}, + lname.tunnel_arrows_rock2: {"code": 0x239, "offset": 0x10CABF, "normal item": iname.purifying, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.tunnel_arrows_rock3: {"code": 0x23A, "offset": 0x10CAC1, "normal item": iname.cure_ampoule, + "add conds": ["3hb"]}, + lname.tunnel_arrows_rock4: {"code": 0x23B, "offset": 0x10CAC3, "normal item": iname.cure_ampoule, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.tunnel_arrows_rock5: {"code": 0x23C, "offset": 0x10CAC5, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.tunnel_lonesome_bucket: {"code": 0x189, "offset": 0xBFC99B, "normal item": iname.cure_ampoule, + "type": "inv"}, + lname.tunnel_lbucket_mdoor_l: {"code": 0x198, "offset": 0x10CA67, "normal item": iname.knife, + "add conds": ["sub"]}, + lname.tunnel_lbucket_quag: {"code": 0x191, "offset": 0x10C9DF, "normal item": iname.red_jewel_l}, + lname.tunnel_bucket_quag_rock1: {"code": 0x23E, "offset": 0x10CAC9, "normal item": iname.roast_beef, + "hard item": iname.roast_chicken, "add conds": ["3hb"]}, + lname.tunnel_bucket_quag_rock2: {"code": 0x23F, "offset": 0x10CACB, "normal item": iname.roast_beef, + "hard item": iname.roast_chicken, "add conds": ["3hb"]}, + lname.tunnel_bucket_quag_rock3: {"code": 0x240, "offset": 0x10CACD, "normal item": iname.roast_beef, + "hard item": iname.roast_chicken, "add conds": ["3hb"]}, + lname.tunnel_lbucket_albert: {"code": 0x190, "offset": 0x10C9E7, "normal item": iname.red_jewel_s}, + lname.tunnel_albert_camp: {"code": 0x192, "offset": 0x10C9D7, "normal item": iname.red_jewel_s}, + lname.tunnel_albert_quag: {"code": 0x193, "offset": 0x10C9CF, "normal item": iname.red_jewel_l}, + lname.tunnel_gondola_rc_sdoor_l: {"code": 0x53, "offset": 0x10CA5F, "normal item": iname.cross, + "add conds": ["sub"]}, + lname.tunnel_gondola_rc_sdoor_m: {"code": 0x19E, "offset": 0x10CAA7, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold}, + lname.tunnel_gondola_rc_sdoor_r: {"code": 0x188, "offset": 0x10CA27, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold}, + lname.tunnel_gondola_rc: {"code": 0x19C, "offset": 0x10CAB7, "normal item": iname.powerup}, + lname.tunnel_rgondola_station: {"code": 0x194, "offset": 0x10C9C7, "normal item": iname.red_jewel_s}, + lname.tunnel_gondola_transfer: {"code": 0x186, "offset": 0x10CA2F, "normal item": iname.five_hundred_gold}, + lname.tunnel_corpse_bucket_quag: {"code": 0x18E, "offset": 0x10C9F7, "normal item": iname.red_jewel_s}, + lname.tunnel_corpse_bucket_mdoor_l: {"code": 0x52, "offset": 0x10CA6F, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.tunnel_corpse_bucket_mdoor_r: {"code": 0x185, "offset": 0x10CA37, "normal item": iname.sun_card, + "hard item": iname.one_hundred_gold}, + lname.tunnel_shovel_quag_start: {"code": 0x18D, "offset": 0x10C9FF, "normal item": iname.red_jewel_l}, + lname.tunnel_exit_quag_start: {"code": 0x18C, "offset": 0x10CA07, "normal item": iname.red_jewel_l}, + lname.tunnel_shovel_quag_end: {"code": 0x18B, "offset": 0x10CA0F, "normal item": iname.red_jewel_l}, + lname.tunnel_exit_quag_end: {"code": 0x184, "offset": 0x10CA3F, "normal item": iname.five_hundred_gold}, + lname.tunnel_shovel: {"code": 0x18F, "offset": 0x86D8FC, "normal item": iname.roast_beef, + "type": "inv"}, + lname.tunnel_shovel_save: {"code": 0x18A, "offset": 0x10CA17, "normal item": iname.red_jewel_l}, + lname.tunnel_shovel_mdoor_l: {"code": 0x183, "offset": 0x10CA47, "normal item": iname.sun_card, + "hard item": iname.one_hundred_gold}, + lname.tunnel_shovel_mdoor_r: {"code": 0x51, "offset": 0x10CA77, "normal item": iname.axe, + "add conds": ["sub"]}, + lname.tunnel_shovel_sdoor_l: {"code": 0x182, "offset": 0x10CA4F, "normal item": iname.moon_card}, + lname.tunnel_shovel_sdoor_m: {"code": 0x19D, "offset": 0x10CAAF, "normal item": iname.roast_chicken}, + lname.tunnel_shovel_sdoor_r: {"code": 0x50, "offset": 0x10CA7F, "normal item": iname.cross, + "add conds": ["sub"]}, + # Underground Waterway + lname.uw_near_ent: {"code": 0x4C, "offset": 0x10CB03, "normal item": iname.three_hundred_gold}, + lname.uw_across_ent: {"code": 0x4E, "offset": 0x10CAF3, "normal item": iname.five_hundred_gold}, + lname.uw_first_ledge1: {"code": 0x242, "offset": 0x10CB39, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.uw_first_ledge2: {"code": 0x243, "offset": 0x10CB3B, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.uw_first_ledge3: {"code": 0x244, "offset": 0x10CB3D, "normal item": iname.purifying, + "hard item": iname.five_hundred_gold, "add conds": ["3hb"]}, + lname.uw_first_ledge4: {"code": 0x245, "offset": 0x10CB3F, "normal item": iname.cure_ampoule, + "hard item": iname.five_hundred_gold, "add conds": ["3hb"]}, + lname.uw_first_ledge5: {"code": 0x246, "offset": 0x10CB41, "normal item": iname.purifying, + "hard item": iname.three_hundred_gold, "add conds": ["3hb"]}, + lname.uw_first_ledge6: {"code": 0x247, "offset": 0x10CB43, "normal item": iname.cure_ampoule, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.uw_poison_parkour: {"code": 0x4D, "offset": 0x10CAFB, "normal item": iname.cure_ampoule}, + lname.uw_boss: {"event": iname.trophy, "add conds": ["boss"]}, + lname.uw_waterfall_alcove: {"code": 0x57, "offset": 0x10CB23, "normal item": iname.five_hundred_gold}, + lname.uw_carrie1: {"code": 0x4B, "offset": 0x10CB0B, "normal item": iname.moon_card, + "hard item": iname.five_hundred_gold, "add conds": ["carrie"]}, + lname.uw_carrie2: {"code": 0x4A, "offset": 0x10CB13, "normal item": iname.roast_beef, + "hard item": iname.five_hundred_gold, "add conds": ["carrie"]}, + lname.uw_bricks_save: {"code": 0x5A, "offset": 0x10CB33, "normal item": iname.powerup, + "hard item": iname.one_hundred_gold}, + lname.uw_above_skel_ledge: {"code": 0x56, "offset": 0x10CB2B, "normal item": iname.roast_chicken}, + lname.uw_in_skel_ledge1: {"code": 0x249, "offset": 0x10CB45, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + lname.uw_in_skel_ledge2: {"code": 0x24A, "offset": 0x10CB47, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + lname.uw_in_skel_ledge3: {"code": 0x24B, "offset": 0x10CB49, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + # Castle Center + lname.ccb_skel_hallway_ent: {"code": 0x1AF, "offset": 0x10CB67, "normal item": iname.red_jewel_s}, + lname.ccb_skel_hallway_jun: {"code": 0x1A8, "offset": 0x10CBD7, "normal item": iname.powerup}, + lname.ccb_skel_hallway_tc: {"code": 0x1AE, "offset": 0x10CB6F, "normal item": iname.red_jewel_l}, + lname.ccb_skel_hallway_ba: {"code": 0x1B6, "offset": 0x10CBC7, "normal item": iname.cross, + "add conds": ["sub"]}, + lname.ccb_behemoth_l_ff: {"code": 0x1AD, "offset": 0x10CB77, "normal item": iname.red_jewel_s}, + lname.ccb_behemoth_l_mf: {"code": 0x1B3, "offset": 0x10CBA7, "normal item": iname.three_hundred_gold, + "hard item": iname.one_hundred_gold}, + lname.ccb_behemoth_l_mr: {"code": 0x1AC, "offset": 0x10CB7F, "normal item": iname.red_jewel_l}, + lname.ccb_behemoth_l_fr: {"code": 0x1B2, "offset": 0x10CBAF, "normal item": iname.three_hundred_gold, + "hard item": iname.one_hundred_gold}, + lname.ccb_behemoth_r_ff: {"code": 0x1B1, "offset": 0x10CBB7, "normal item": iname.three_hundred_gold, + "hard item": iname.one_hundred_gold}, + lname.ccb_behemoth_r_mf: {"code": 0x1AB, "offset": 0x10CB87, "normal item": iname.red_jewel_s}, + lname.ccb_behemoth_r_mr: {"code": 0x1B0, "offset": 0x10CBBF, "normal item": iname.three_hundred_gold, + "hard item": iname.one_hundred_gold}, + lname.ccb_behemoth_r_fr: {"code": 0x1AA, "offset": 0x10CB8F, "normal item": iname.red_jewel_l}, + lname.ccb_behemoth_crate1: {"code": 0x24D, "offset": 0x10CBDD, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ccb_behemoth_crate2: {"code": 0x24E, "offset": 0x10CBDF, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ccb_behemoth_crate3: {"code": 0x24F, "offset": 0x10CBE1, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ccb_behemoth_crate4: {"code": 0x250, "offset": 0x10CBE3, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ccb_behemoth_crate5: {"code": 0x251, "offset": 0x10CBE5, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ccelv_near_machine: {"code": 0x11A, "offset": 0x10CBF7, "normal item": iname.red_jewel_s}, + lname.ccelv_atop_machine: {"code": 0x118, "offset": 0x10CC17, "normal item": iname.powerup, + "hard item": iname.three_hundred_gold}, + lname.ccelv_stand1: {"code": 0x253, "offset": 0x10CC1D, "normal item": iname.roast_beef, + "add conds": ["3hb"]}, + lname.ccelv_stand2: {"code": 0x254, "offset": 0x10CC1F, "normal item": iname.roast_beef, + "hard item": iname.three_hundred_gold, "add conds": ["3hb"]}, + lname.ccelv_stand3: {"code": 0x255, "offset": 0x10CC21, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.ccelv_pipes: {"code": 0x11B, "offset": 0x10CC07, "normal item": iname.one_hundred_gold}, + lname.ccelv_switch: {"code": 0x100, "offset": 0x10CC0F, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.ccelv_staircase: {"code": 0x119, "offset": 0x10CBFF, "normal item": iname.red_jewel_l, + "hard item": iname.five_hundred_gold}, + lname.ccff_redcarpet_knight: {"code": 0x10A, "offset": 0x8C44D9, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s, "type": "inv"}, + lname.ccff_gears_side: {"code": 0x10F, "offset": 0x10CC33, "normal item": iname.red_jewel_s}, + lname.ccff_gears_mid: {"code": 0x10E, "offset": 0x10CC3B, "normal item": iname.purifying, + "hard item": iname.one_hundred_gold}, + lname.ccff_gears_corner: {"code": 0x10D, "offset": 0x10CC43, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold}, + lname.ccff_lizard_knight: {"code": 0x109, "offset": 0x8C44E7, "normal item": iname.roast_chicken, + "hard item": iname.three_hundred_gold, "type": "inv"}, + lname.ccff_lizard_near_knight: {"code": 0x101, "offset": 0x10CC5B, "normal item": iname.axe, + "add conds": ["sub"]}, + lname.ccff_lizard_pit: {"code": 0x10C, "offset": 0x10CC4B, "normal item": iname.sun_card, + "hard item": iname.five_hundred_gold}, + lname.ccff_lizard_corner: {"code": 0x10B, "offset": 0x10CC53, "normal item": iname.moon_card, + "hard item": iname.five_hundred_gold}, + lname.ccff_lizard_locker_nfr: {"code": 0x104, "offset": 0x8C450A, "normal item": iname.red_jewel_l, + "add conds": ["liz"]}, + lname.ccff_lizard_locker_nmr: {"code": 0x105, "offset": 0xBFC9C3, "normal item": iname.five_hundred_gold, + "add conds": ["liz"]}, + lname.ccff_lizard_locker_nml: {"code": 0x106, "offset": 0xBFC9C7, "normal item": iname.red_jewel_l, + "hard item": iname.cure_ampoule, "add conds": ["liz"]}, + lname.ccff_lizard_locker_nfl: {"code": 0x107, "offset": 0xBFCA07, "normal item": iname.powerup, + "add conds": ["liz"]}, + lname.ccff_lizard_locker_fl: {"code": 0x102, "offset": 0xBFCA03, "normal item": iname.five_hundred_gold, + "add conds": ["liz"]}, + lname.ccff_lizard_locker_fr: {"code": 0x103, "offset": 0x8C44F5, "normal item": iname.sun_card, + "hard item": iname.three_hundred_gold, "add conds": ["liz"]}, + lname.ccff_lizard_slab1: {"code": 0x257, "offset": 0x10CC61, "normal item": iname.purifying, + "hard item": iname.roast_chicken, "add conds": ["3hb"]}, + lname.ccff_lizard_slab2: {"code": 0x258, "offset": 0x10CC63, "normal item": iname.purifying, + "hard item": iname.powerup, "add conds": ["3hb"]}, + lname.ccff_lizard_slab3: {"code": 0x259, "offset": 0x10CC65, "normal item": iname.cure_ampoule, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.ccff_lizard_slab4: {"code": 0x25A, "offset": 0x10CC67, "normal item": iname.cure_ampoule, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.ccb_mandrag_shelf_l: {"code": 0x1A0, "offset": 0xBFCBB3, "normal item": iname.mandragora}, + lname.ccb_mandrag_shelf_r: {"code": 0x1A1, "offset": 0xBFCBAF, "normal item": iname.mandragora}, + lname.ccb_torture_rack: {"code": 0x1A9, "offset": 0x8985E5, "normal item": iname.purifying, + "type": "inv"}, + lname.ccb_torture_rafters: {"code": 0x1A2, "offset": 0x8985D6, "normal item": iname.roast_beef}, + lname.cc_behind_the_seal: {"event": iname.crystal, "add conds": ["crystal"]}, + lname.cc_boss_one: {"event": iname.trophy, "add conds": ["boss"]}, + lname.cc_boss_two: {"event": iname.trophy, "add conds": ["boss"]}, + lname.ccll_brokenstairs_floor: {"code": 0x7B, "offset": 0x10CC8F, "normal item": iname.red_jewel_l, + "countdown": 14}, + lname.ccll_brokenstairs_knight: {"code": 0x74, "offset": 0x8DF782, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14}, + lname.ccll_brokenstairs_save: {"code": 0x7C, "offset": 0x10CC87, "normal item": iname.red_jewel_l, + "countdown": 14}, + lname.ccll_glassknight_l: {"code": 0x7A, "offset": 0x10CC97, "normal item": iname.red_jewel_s, + "hard item": iname.five_hundred_gold, "countdown": 14}, + lname.ccll_glassknight_r: {"code": 0x7E, "offset": 0x10CC77, "normal item": iname.red_jewel_s, + "hard item": iname.five_hundred_gold, "countdown": 14}, + lname.ccll_butlers_door: {"code": 0x7D, "offset": 0x10CC7F, "normal item": iname.red_jewel_s, + "countdown": 14}, + lname.ccll_butlers_side: {"code": 0x79, "offset": 0x10CC9F, "normal item": iname.purifying, + "hard item": iname.one_hundred_gold, "countdown": 14}, + lname.ccll_cwhall_butlerflames_past: {"code": 0x78, "offset": 0x10CCA7, "normal item": iname.cure_ampoule, + "hard item": iname.red_jewel_l, "countdown": 14}, + lname.ccll_cwhall_flamethrower: {"code": 0x73, "offset": 0x8DF580, "normal item": iname.five_hundred_gold, + "type": "inv", "countdown": 14}, + lname.ccll_cwhall_cwflames: {"code": 0x77, "offset": 0x10CCAF, "normal item": iname.roast_chicken, + "hard item": iname.red_jewel_l, "countdown": 14}, + lname.ccll_heinrich: {"code": 0x69, "offset": 0xBFE443, "normal item": iname.chamber_key, + "type": "npc", "countdown": 14}, + lname.ccia_nitro_crates: {"code": 0x66, "offset": 0x90FCE9, "normal item": iname.healing_kit, + "hard item": iname.one_hundred_gold, "type": "inv", "countdown": 14}, + lname.ccia_nitro_shelf_h: {"code": 0x55, "offset": 0xBFCC03, "normal item": iname.magical_nitro, + "countdown": 14}, + lname.ccia_stairs_knight: {"code": 0x61, "offset": 0x90FE5C, "normal item": iname.five_hundred_gold, + "type": "inv", "countdown": 14}, + lname.ccia_maids_vase: {"code": 0x63, "offset": 0x90FF1D, "normal item": iname.red_jewel_l, + "type": "inv", "countdown": 14}, + lname.ccia_maids_outer: {"code": 0x6B, "offset": 0x10CCFF, "normal item": iname.purifying, + "hard item": iname.three_hundred_gold, "countdown": 14}, + lname.ccia_maids_inner: {"code": 0x6A, "offset": 0x10CD07, "normal item": iname.cure_ampoule, + "hard item": iname.three_hundred_gold, "countdown": 14}, + lname.ccia_inventions_maids: {"code": 0x6C, "offset": 0x10CCE7, "normal item": iname.moon_card, + "hard item": iname.one_hundred_gold, "countdown": 14}, + lname.ccia_inventions_crusher: {"code": 0x6E, "offset": 0x10CCDF, "normal item": iname.sun_card, + "hard item": iname.one_hundred_gold, "countdown": 14}, + lname.ccia_inventions_famicart: {"code": 0x64, "offset": 0x90FBB3, "normal item": iname.five_hundred_gold, + "type": "inv", "countdown": 14}, + lname.ccia_inventions_zeppelin: {"code": 0x6D, "offset": 0x90FBC0, "normal item": iname.roast_beef, + "countdown": 14}, + lname.ccia_inventions_round: {"code": 0x65, "offset": 0x90FBA7, "normal item": iname.roast_beef, + "hard item": iname.five_hundred_gold, "type": "inv", "countdown": 14}, + lname.ccia_nitrohall_flamethrower: {"code": 0x62, "offset": 0x90FCDA, "normal item": iname.red_jewel_l, + "type": "inv", "countdown": 14}, + lname.ccia_nitrohall_torch: {"code": 0x6F, "offset": 0x10CCD7, "normal item": iname.roast_chicken, + "hard item": iname.red_jewel_s, "countdown": 14}, + lname.ccia_nitro_shelf_i: {"code": 0x60, "offset": 0xBFCBFF, "normal item": iname.magical_nitro, + "countdown": 14}, + lname.ccll_cwhall_wall: {"code": 0x76, "offset": 0x10CCB7, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold, "countdown": 14}, + lname.ccl_bookcase: {"code": 0x166, "offset": 0x8F1197, "normal item": iname.sun_card, + "countdown": 14}, + # Duel Tower + lname.dt_boss_one: {"event": iname.trophy, "add conds": ["boss"]}, + lname.dt_boss_two: {"event": iname.trophy, "add conds": ["boss"]}, + lname.dt_ibridge_l: {"code": 0x81, "offset": 0x10CE8B, "normal item": iname.roast_beef, + "hard item": iname.five_hundred_gold}, + lname.dt_ibridge_r: {"code": 0x80, "offset": 0x10CE93, "normal item": iname.powerup}, + lname.dt_stones_start: {"code": 0x83, "offset": 0x10CE73, "normal item": iname.roast_chicken, + "hard item": iname.five_hundred_gold}, + lname.dt_stones_end: {"code": 0x97, "offset": 0x10CE83, "normal item": iname.knife, "add conds": ["sub"]}, + lname.dt_werebull_arena: {"code": 0x82, "offset": 0x10CE7B, "normal item": iname.roast_beef}, + lname.dt_boss_three: {"event": iname.trophy, "add conds": ["boss"]}, + lname.dt_boss_four: {"event": iname.trophy, "add conds": ["boss"]}, + # Tower of Execution + lname.toe_ledge1: {"code": 0x25C, "offset": 0x10CD5D, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.toe_ledge2: {"code": 0x25D, "offset": 0x10CD5F, "normal item": iname.purifying, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.toe_ledge3: {"code": 0x25E, "offset": 0x10CD61, "normal item": iname.five_hundred_gold, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.toe_ledge4: {"code": 0x25F, "offset": 0x10CD63, "normal item": iname.cure_ampoule, + "hard item": iname.five_hundred_gold, "add conds": ["3hb"]}, + lname.toe_ledge5: {"code": 0x260, "offset": 0x10CD65, "normal item": iname.holy_water, + "add conds": ["3hb", "sub"]}, + lname.toe_midsavespikes_r: {"code": 0x9C, "offset": 0x10CD1F, "normal item": iname.five_hundred_gold}, + lname.toe_midsavespikes_l: {"code": 0x9B, "offset": 0x10CD27, "normal item": iname.roast_chicken, + "hard item": iname.five_hundred_gold}, + lname.toe_elec_grate: {"code": 0x99, "offset": 0x10CD17, "normal item": iname.execution_key}, + lname.toe_ibridge: {"code": 0x98, "offset": 0x10CD47, "normal item": iname.one_hundred_gold}, + lname.toe_top: {"code": 0x9D, "offset": 0x10CD4F, "normal item": iname.red_jewel_l}, + lname.toe_keygate_l: {"code": 0x9A, "offset": 0x10CD37, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold}, + lname.toe_keygate_r: {"code": 0x9E, "offset": 0x10CD3F, "normal item": iname.cross, "add conds": ["sub"]}, + # Tower of Science + lname.tosci_elevator: {"code": 0x1FC, "offset": 0x10CE0B, "normal item": iname.three_hundred_gold}, + lname.tosci_plain_sr: {"code": 0x1FF, "offset": 0x10CDF3, "normal item": iname.science_key1}, + lname.tosci_stairs_sr: {"code": 0x1FB, "offset": 0x10CE13, "normal item": iname.three_hundred_gold}, + lname.tosci_three_door_hall: {"code": 0x1FE, "offset": 0x10CDFB, "normal item": iname.science_key2}, + lname.tosci_ibridge_t: {"code": 0x1F3, "offset": 0x10CE3B, "normal item": iname.roast_beef, + "hard item": iname.red_jewel_l}, + lname.tosci_ibridge_b1: {"code": 0x262, "offset": 0x10CE59, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.tosci_ibridge_b2: {"code": 0x263, "offset": 0x10CE5B, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.tosci_ibridge_b3: {"code": 0x264, "offset": 0x10CE5D, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.tosci_ibridge_b4: {"code": 0x265, "offset": 0x10CE5F, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.tosci_ibridge_b5: {"code": 0x266, "offset": 0x10CE61, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + lname.tosci_ibridge_b6: {"code": 0x267, "offset": 0x10CE63, "normal item": iname.roast_chicken, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.tosci_conveyor_sr: {"code": 0x1F7, "offset": 0x10CE33, "normal item": iname.red_jewel_l, + "hard item": iname.red_jewel_s}, + lname.tosci_exit: {"code": 0x1FD, "offset": 0x10CE03, "normal item": iname.science_key3}, + lname.tosci_key3_r: {"code": 0x1FA, "offset": 0x10CE1B, "normal item": iname.five_hundred_gold}, + lname.tosci_key3_m: {"code": 0x1F2, "offset": 0x10CE2B, "normal item": iname.cross, "add conds": ["sub"]}, + lname.tosci_key3_l: {"code": 0x1F9, "offset": 0x10CE23, "normal item": iname.five_hundred_gold}, + # Tower of Sorcery + lname.tosor_stained_tower: {"code": 0x96, "offset": 0x10CDB3, "normal item": iname.red_jewel_l}, + lname.tosor_savepoint: {"code": 0x95, "offset": 0x10CDBB, "normal item": iname.red_jewel_l}, + lname.tosor_trickshot: {"code": 0x92, "offset": 0x10CDD3, "normal item": iname.roast_beef}, + lname.tosor_yellow_bubble: {"code": 0x91, "offset": 0x10CDDB, "normal item": iname.five_hundred_gold}, + lname.tosor_blue_platforms: {"code": 0x94, "offset": 0x10CDC3, "normal item": iname.red_jewel_s}, + lname.tosor_side_isle: {"code": 0x93, "offset": 0x10CDCB, "normal item": iname.red_jewel_s}, + lname.tosor_ibridge: {"code": 0x90, "offset": 0x10CDE3, "normal item": iname.three_hundred_gold}, + # Room of Clocks + lname.roc_ent_l: {"code": 0xC6, "offset": 0x10CF7B, "normal item": iname.roast_beef, + "hard item": iname.red_jewel_l}, + lname.roc_ent_r: {"code": 0xC3, "offset": 0x10CFBB, "normal item": iname.powerup, + "hard item": iname.five_hundred_gold}, + lname.roc_elev_r: {"code": 0xD4, "offset": 0x10CF93, "normal item": iname.holy_water, "add conds": ["sub"]}, + lname.roc_elev_l: {"code": 0xD5, "offset": 0x10CF8B, "normal item": iname.axe, "add conds": ["sub"]}, + lname.roc_cont_r: {"code": 0xC5, "offset": 0x10CFB3, "normal item": iname.powerup, + "hard item": iname.one_hundred_gold}, + lname.roc_cont_l: {"code": 0xDF, "offset": 0x10CFA3, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.roc_exit: {"code": 0xDC, "offset": 0x10CF9B, "normal item": iname.three_hundred_gold, + "add conds": ["empty"]}, + lname.roc_boss: {"event": iname.trophy, "add conds": ["boss"]}, + # Clock Tower + lname.ct_gearclimb_battery_slab1: {"code": 0x269, "offset": 0x10CEF9, "normal item": iname.roast_chicken, + "add conds": ["3hb"]}, + lname.ct_gearclimb_battery_slab2: {"code": 0x26A, "offset": 0x10CEFB, "normal item": iname.roast_chicken, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.ct_gearclimb_battery_slab3: {"code": 0x26B, "offset": 0x10CEFD, "normal item": iname.roast_chicken, + "hard item": iname.red_jewel_s, "add conds": ["3hb"]}, + lname.ct_gearclimb_corner: {"code": 0xA7, "offset": 0x10CEB3, "normal item": iname.red_jewel_s}, + lname.ct_gearclimb_side: {"code": 0xAD, "offset": 0x10CEC3, "normal item": iname.clocktower_key1}, + lname.ct_gearclimb_door_slab1: {"code": 0x26D, "offset": 0x10CF01, "normal item": iname.roast_beef, + "add conds": ["3hb"]}, + lname.ct_gearclimb_door_slab2: {"code": 0x26E, "offset": 0x10CF03, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.ct_gearclimb_door_slab3: {"code": 0x26F, "offset": 0x10CF05, "normal item": iname.roast_beef, + "hard item": iname.one_hundred_gold, "add conds": ["3hb"]}, + lname.ct_bp_chasm_fl: {"code": 0xA5, "offset": 0x99BC4D, "normal item": iname.five_hundred_gold}, + lname.ct_bp_chasm_fr: {"code": 0xA6, "offset": 0x99BC3E, "normal item": iname.red_jewel_l}, + lname.ct_bp_chasm_rl: {"code": 0xA4, "offset": 0x99BC5A, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.ct_bp_chasm_k: {"code": 0xAC, "offset": 0x99BC30, "normal item": iname.clocktower_key2}, + lname.ct_finalroom_door_slab1: {"code": 0x271, "offset": 0x10CEF5, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_door_slab2: {"code": 0x272, "offset": 0x10CEF7, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_fl: {"code": 0xB3, "offset": 0x10CED3, "normal item": iname.axe, + "add conds": ["sub"]}, + lname.ct_finalroom_fr: {"code": 0xB4, "offset": 0x10CECB, "normal item": iname.knife, + "add conds": ["sub"]}, + lname.ct_finalroom_rl: {"code": 0xB2, "offset": 0x10CEE3, "normal item": iname.holy_water, + "add conds": ["sub"]}, + lname.ct_finalroom_rr: {"code": 0xB0, "offset": 0x10CEDB, "normal item": iname.cross, + "add conds": ["sub"]}, + lname.ct_finalroom_platform: {"code": 0xAB, "offset": 0x10CEBB, "normal item": iname.clocktower_key3}, + lname.ct_finalroom_renon_slab1: {"code": 0x274, "offset": 0x10CF09, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab2: {"code": 0x275, "offset": 0x10CF0B, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab3: {"code": 0x276, "offset": 0x10CF0D, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab4: {"code": 0x277, "offset": 0x10CF0F, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab5: {"code": 0x278, "offset": 0x10CF11, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab6: {"code": 0x279, "offset": 0x10CF13, "normal item": iname.five_hundred_gold, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab7: {"code": 0x27A, "offset": 0x10CF15, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + lname.ct_finalroom_renon_slab8: {"code": 0x27B, "offset": 0x10CF17, "normal item": iname.red_jewel_l, + "add conds": ["3hb"]}, + # Castle Keep + lname.ck_boss_one: {"event": iname.trophy, "add conds": ["boss", "renon"]}, + lname.ck_boss_two: {"event": iname.trophy, "add conds": ["boss", "vincent"]}, + lname.ck_flame_l: {"code": 0xAF, "offset": 0x9778C8, "normal item": iname.healing_kit, "type": "inv"}, + lname.ck_flame_r: {"code": 0xAE, "offset": 0xBFCA67, "normal item": iname.healing_kit, "type": "inv"}, + lname.ck_behind_drac: {"code": 0xBF, "offset": 0x10CE9B, "normal item": iname.red_jewel_l}, + lname.ck_cube: {"code": 0xB5, "offset": 0x10CEA3, "normal item": iname.healing_kit}, + lname.renon1: {"code": 0x1C8, "offset": 0xBFD8E5, "normal item": iname.roast_chicken, "type": "shop"}, + lname.renon2: {"code": 0x1C9, "offset": 0xBFD8E7, "normal item": iname.roast_beef, "type": "shop"}, + lname.renon3: {"code": 0x1CA, "offset": 0xBFD8E9, "normal item": iname.healing_kit, "type": "shop"}, + lname.renon4: {"code": 0x1CB, "offset": 0xBFD8EB, "normal item": iname.purifying, "type": "shop"}, + lname.renon5: {"code": 0x1CC, "offset": 0xBFD8ED, "normal item": iname.cure_ampoule, "type": "shop"}, + lname.renon6: {"code": 0x1CD, "offset": 0xBFD907, "normal item": iname.sun_card, "type": "shop"}, + lname.renon7: {"code": 0x1CE, "offset": 0xBFD909, "normal item": iname.moon_card, "type": "shop"}, + lname.the_end: {"event": iname.victory}, +} + + +add_conds = {"carrie": ("carrie_logic", True, True), + "liz": ("lizard_locker_items", True, True), + "sub": ("sub_weapon_shuffle", SubWeaponShuffle.option_anywhere, True), + "3hb": ("multi_hit_breakables", True, True), + "empty": ("empty_breakables", True, True), + "shop": ("shopsanity", True, True), + "crystal": ("draculas_condition", DraculasCondition.option_crystal, True), + "boss": ("draculas_condition", DraculasCondition.option_bosses, True), + "renon": ("renon_fight_condition", RenonFightCondition.option_never, False), + "vincent": ("vincent_fight_condition", VincentFightCondition.option_never, False)} + + +def get_location_info(location: str, info: str) -> Union[int, str, List[str], None]: + return location_info[location].get(info, None) + + +def get_location_names_to_ids() -> Dict[str, int]: + return {name: get_location_info(name, "code")+base_id for name in location_info if get_location_info(name, "code") + is not None} + + +def verify_locations(options: CV64Options, locations: List[str]) -> Tuple[Dict[str, Optional[int]], Dict[str, str]]: + + verified_locations = {} + events = {} + + for loc in locations: + loc_add_conds = get_location_info(loc, "add conds") + loc_code = get_location_info(loc, "code") + + # Check any options that might be associated with the Location before adding it. + add_it = True + if isinstance(loc_add_conds, list): + for cond in loc_add_conds: + if not ((getattr(options, add_conds[cond][0]).value == add_conds[cond][1]) == add_conds[cond][2]): + add_it = False + + if not add_it: + continue + + # Add the location to the verified Locations if the above check passes. + # If we are looking at an event Location, add its associated event Item to the events' dict. + # Otherwise, add the base_id to the Location's code. + if loc_code is None: + events[loc] = get_location_info(loc, "event") + else: + loc_code += base_id + verified_locations.update({loc: loc_code}) + + return verified_locations, events diff --git a/worlds/cv64/lzkn64.py b/worlds/cv64/lzkn64.py new file mode 100644 index 000000000000..9a94cebbb4eb --- /dev/null +++ b/worlds/cv64/lzkn64.py @@ -0,0 +1,266 @@ +# ************************************************************** +# * LZKN64 Compression and Decompression Utility * +# * Original repo at https://github.com/Fluvian/lzkn64, * +# * converted from C to Python with permission from Fluvian. * +# ************************************************************** + +TYPE_COMPRESS = 1 +TYPE_DECOMPRESS = 2 + +MODE_NONE = 0x7F +MODE_WINDOW_COPY = 0x00 +MODE_RAW_COPY = 0x80 +MODE_RLE_WRITE_A = 0xC0 +MODE_RLE_WRITE_B = 0xE0 +MODE_RLE_WRITE_C = 0xFF + +WINDOW_SIZE = 0x3FF +COPY_SIZE = 0x21 +RLE_SIZE = 0x101 + + +# Compresses the data in the buffer specified in the arguments. +def compress_buffer(file_buffer: bytearray) -> bytearray: + # Size of the buffer to compress + buffer_size = len(file_buffer) - 1 + + # Position of the current read location in the buffer. + buffer_position = 0 + + # Position of the current write location in the written buffer. + write_position = 4 + + # Allocate write_buffer with size of 0xFFFFFF (24-bit). + write_buffer = bytearray(0xFFFFFF) + + # Position in the input buffer of the last time one of the copy modes was used. + buffer_last_copy_position = 0 + + while buffer_position < buffer_size: + # Calculate maximum length we are able to copy without going out of bounds. + if COPY_SIZE < (buffer_size - 1) - buffer_position: + sliding_window_maximum_length = COPY_SIZE + else: + sliding_window_maximum_length = (buffer_size - 1) - buffer_position + + # Calculate how far we are able to look back without going behind the start of the uncompressed buffer. + if buffer_position - WINDOW_SIZE > 0: + sliding_window_maximum_offset = buffer_position - WINDOW_SIZE + else: + sliding_window_maximum_offset = 0 + + # Calculate maximum length the forwarding looking window is able to search. + if RLE_SIZE < (buffer_size - 1) - buffer_position: + forward_window_maximum_length = RLE_SIZE + else: + forward_window_maximum_length = (buffer_size - 1) - buffer_position + + sliding_window_match_position = -1 + sliding_window_match_size = 0 + + forward_window_match_value = 0 + forward_window_match_size = 0 + + # The current mode the compression algorithm prefers. (0x7F == None) + current_mode = MODE_NONE + + # The current submode the compression algorithm prefers. + current_submode = MODE_NONE + + # How many bytes will have to be copied in the raw copy command. + raw_copy_size = buffer_position - buffer_last_copy_position + + # How many bytes we still have to copy in RLE matches with more than 0x21 bytes. + rle_bytes_left = 0 + + """Go backwards in the buffer, is there a matching value? + If yes, search forward and check for more matching values in a loop. + If no, go further back and repeat.""" + for search_position in range(buffer_position - 1, sliding_window_maximum_offset - 1, -1): + matching_sequence_size = 0 + + while file_buffer[search_position + matching_sequence_size] == file_buffer[buffer_position + + matching_sequence_size]: + matching_sequence_size += 1 + + if matching_sequence_size >= sliding_window_maximum_length: + break + + # Once we find a match or a match that is bigger than the match before it, we save its position and length. + if matching_sequence_size > sliding_window_match_size: + sliding_window_match_position = search_position + sliding_window_match_size = matching_sequence_size + + """Look one step forward in the buffer, is there a matching value? + If yes, search further and check for a repeating value in a loop. + If no, continue to the rest of the function.""" + matching_sequence_value = file_buffer[buffer_position] + matching_sequence_size = 0 + + while file_buffer[buffer_position + matching_sequence_size] == matching_sequence_value: + matching_sequence_size += 1 + + if matching_sequence_size >= forward_window_maximum_length: + break + + # If we find a sequence of matching values, save them. + if matching_sequence_size >= 1: + forward_window_match_value = matching_sequence_value + forward_window_match_size = matching_sequence_size + + # Try to pick which mode works best with the current values. + if sliding_window_match_size >= 3: + current_mode = MODE_WINDOW_COPY + elif forward_window_match_size >= 3: + current_mode = MODE_RLE_WRITE_A + + if forward_window_match_value != 0x00 and forward_window_match_size <= COPY_SIZE: + current_submode = MODE_RLE_WRITE_A + elif forward_window_match_value != 0x00 and forward_window_match_size > COPY_SIZE: + current_submode = MODE_RLE_WRITE_A + rle_bytes_left = forward_window_match_size + elif forward_window_match_value == 0x00 and forward_window_match_size <= COPY_SIZE: + current_submode = MODE_RLE_WRITE_B + elif forward_window_match_value == 0x00 and forward_window_match_size > COPY_SIZE: + current_submode = MODE_RLE_WRITE_C + elif forward_window_match_size >= 2 and forward_window_match_value == 0x00: + current_mode = MODE_RLE_WRITE_A + current_submode = MODE_RLE_WRITE_B + + """Write a raw copy command when these following conditions are met: + The current mode is set and there are raw bytes available to be copied. + The raw byte length exceeds the maximum length that can be stored. + Raw bytes need to be written due to the proximity to the end of the buffer.""" + if (current_mode != MODE_NONE and raw_copy_size >= 1) or raw_copy_size >= 0x1F or \ + (buffer_position + 1) == buffer_size: + if buffer_position + 1 == buffer_size: + raw_copy_size = buffer_size - buffer_last_copy_position + + write_buffer[write_position] = MODE_RAW_COPY | raw_copy_size & 0x1F + write_position += 1 + + for written_bytes in range(raw_copy_size): + write_buffer[write_position] = file_buffer[buffer_last_copy_position] + write_position += 1 + buffer_last_copy_position += 1 + + if current_mode == MODE_WINDOW_COPY: + write_buffer[write_position] = MODE_WINDOW_COPY | ((sliding_window_match_size - 2) & 0x1F) << 2 | \ + (((buffer_position - sliding_window_match_position) & 0x300) >> 8) + write_position += 1 + write_buffer[write_position] = (buffer_position - sliding_window_match_position) & 0xFF + write_position += 1 + + buffer_position += sliding_window_match_size + buffer_last_copy_position = buffer_position + elif current_mode == MODE_RLE_WRITE_A: + if current_submode == MODE_RLE_WRITE_A: + if rle_bytes_left > 0: + while rle_bytes_left > 0: + # Dump raw bytes if we have less than two bytes left, not doing so would cause an underflow + # error. + if rle_bytes_left < 2: + write_buffer[write_position] = MODE_RAW_COPY | rle_bytes_left & 0x1F + write_position += 1 + + for writtenBytes in range(rle_bytes_left): + write_buffer[write_position] = forward_window_match_value & 0xFF + write_position += 1 + + rle_bytes_left = 0 + break + + if rle_bytes_left < COPY_SIZE: + write_buffer[write_position] = MODE_RLE_WRITE_A | (rle_bytes_left - 2) & 0x1F + write_position += 1 + else: + write_buffer[write_position] = MODE_RLE_WRITE_A | (COPY_SIZE - 2) & 0x1F + write_position += 1 + write_buffer[write_position] = forward_window_match_value & 0xFF + write_position += 1 + rle_bytes_left -= COPY_SIZE + else: + write_buffer[write_position] = MODE_RLE_WRITE_A | (forward_window_match_size - 2) & 0x1F + write_position += 1 + write_buffer[write_position] = forward_window_match_value & 0xFF + write_position += 1 + + elif current_submode == MODE_RLE_WRITE_B: + write_buffer[write_position] = MODE_RLE_WRITE_B | (forward_window_match_size - 2) & 0x1F + write_position += 1 + elif current_submode == MODE_RLE_WRITE_C: + write_buffer[write_position] = MODE_RLE_WRITE_C + write_position += 1 + write_buffer[write_position] = (forward_window_match_size - 2) & 0xFF + write_position += 1 + + buffer_position += forward_window_match_size + buffer_last_copy_position = buffer_position + else: + buffer_position += 1 + + # Write the compressed size. + write_buffer[1] = 0x00 + write_buffer[1] = write_position >> 16 & 0xFF + write_buffer[2] = write_position >> 8 & 0xFF + write_buffer[3] = write_position & 0xFF + + # Return the compressed write buffer. + return write_buffer[0:write_position] + + +# Decompresses the data in the buffer specified in the arguments. +def decompress_buffer(file_buffer: bytearray) -> bytearray: + # Position of the current read location in the buffer. + buffer_position = 4 + + # Position of the current write location in the written buffer. + write_position = 0 + + # Get compressed size. + compressed_size = (file_buffer[1] << 16) + (file_buffer[2] << 8) + file_buffer[3] - 1 + + # Allocate writeBuffer with size of 0xFFFFFF (24-bit). + write_buffer = bytearray(0xFFFFFF) + + while buffer_position < compressed_size: + mode_command = file_buffer[buffer_position] + buffer_position += 1 + + if MODE_WINDOW_COPY <= mode_command < MODE_RAW_COPY: + copy_length = (mode_command >> 2) + 2 + copy_offset = file_buffer[buffer_position] + (mode_command << 8) & 0x3FF + buffer_position += 1 + + for current_length in range(copy_length, 0, -1): + write_buffer[write_position] = write_buffer[write_position - copy_offset] + write_position += 1 + elif MODE_RAW_COPY <= mode_command < MODE_RLE_WRITE_A: + copy_length = mode_command & 0x1F + + for current_length in range(copy_length, 0, -1): + write_buffer[write_position] = file_buffer[buffer_position] + write_position += 1 + buffer_position += 1 + elif MODE_RLE_WRITE_A <= mode_command <= MODE_RLE_WRITE_C: + write_length = 0 + write_value = 0x00 + + if MODE_RLE_WRITE_A <= mode_command < MODE_RLE_WRITE_B: + write_length = (mode_command & 0x1F) + 2 + write_value = file_buffer[buffer_position] + buffer_position += 1 + elif MODE_RLE_WRITE_B <= mode_command < MODE_RLE_WRITE_C: + write_length = (mode_command & 0x1F) + 2 + elif mode_command == MODE_RLE_WRITE_C: + write_length = file_buffer[buffer_position] + 2 + buffer_position += 1 + + for current_length in range(write_length, 0, -1): + write_buffer[write_position] = write_value + write_position += 1 + + # Return the current position of the write buffer, essentially giving us the size of the write buffer. + while write_position % 16 != 0: + write_position += 1 + return write_buffer[0:write_position] diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py new file mode 100644 index 000000000000..4545cd0b5c28 --- /dev/null +++ b/worlds/cv64/options.py @@ -0,0 +1,490 @@ +from dataclasses import dataclass +from Options import Choice, DefaultOnToggle, Range, Toggle, PerGameCommonOptions, StartInventoryPool + + +class CharacterStages(Choice): + """Whether to include Reinhardt-only stages, Carrie-only stages, or both with or without branching paths at the end + of Villa and Castle Center.""" + display_name = "Character Stages" + option_both = 0 + option_branchless_both = 1 + option_reinhardt_only = 2 + option_carrie_only = 3 + default = 0 + + +class StageShuffle(Toggle): + """Shuffles which stages appear in which stage slots. Villa and Castle Center will never appear in any character + stage slots if Character Stages is set to Both; they can only be somewhere on the main path. + Castle Keep will always be at the end of the line.""" + display_name = "Stage Shuffle" + + +class StartingStage(Choice): + """Which stage to start at if Stage Shuffle is turned on.""" + display_name = "Starting Stage" + option_forest_of_silence = 0 + option_castle_wall = 1 + option_villa = 2 + option_tunnel = 3 + option_underground_waterway = 4 + option_castle_center = 5 + option_duel_tower = 6 + option_tower_of_execution = 7 + option_tower_of_science = 8 + option_tower_of_sorcery = 9 + option_room_of_clocks = 10 + option_clock_tower = 11 + default = "random" + + +class WarpOrder(Choice): + """Arranges the warps in the warp menu in whichever stage order chosen, + thereby changing the order they are unlocked in.""" + display_name = "Warp Order" + option_seed_stage_order = 0 + option_vanilla_stage_order = 1 + option_randomized_order = 2 + default = 0 + + +class SubWeaponShuffle(Choice): + """Shuffles all sub-weapons in the game within each other in their own pool or in the main item pool.""" + display_name = "Sub-weapon Shuffle" + option_off = 0 + option_own_pool = 1 + option_anywhere = 2 + default = 0 + + +class SpareKeys(Choice): + """Puts an additional copy of every non-Special key item in the pool for every key item that there is. + Chance gives each key item a 50% chance of having a duplicate instead of guaranteeing one for all of them.""" + display_name = "Spare Keys" + option_off = 0 + option_on = 1 + option_chance = 2 + default = 0 + + +class HardItemPool(Toggle): + """Replaces some items in the item pool with less valuable ones, to make the item pool sort of resemble Hard Mode + in the PAL version.""" + display_name = "Hard Item Pool" + + +class Special1sPerWarp(Range): + """Sets how many Special1 jewels are needed per warp menu option unlock.""" + range_start = 1 + range_end = 10 + default = 1 + display_name = "Special1s Per Warp" + + +class TotalSpecial1s(Range): + """Sets how many Speical1 jewels are in the pool in total. + If this is set to be less than Special1s Per Warp x 7, it will decrease by 1 until it isn't.""" + range_start = 7 + range_end = 70 + default = 7 + display_name = "Total Special1s" + + +class DraculasCondition(Choice): + """Sets the requirement for unlocking and opening the door to Dracula's chamber. + None: No requirement. Door is unlocked from the start. + Crystal: Activate the big crystal in Castle Center's basement. Neither boss afterwards has to be defeated. + Bosses: Kill a specified number of bosses with health bars and claim their Trophies. + Specials: Find a specified number of Special2 jewels shuffled in the main item pool.""" + display_name = "Dracula's Condition" + option_none = 0 + option_crystal = 1 + option_bosses = 2 + option_specials = 3 + default = 1 + + +class PercentSpecial2sRequired(Range): + """Percentage of Special2s required to enter Dracula's chamber when Dracula's Condition is Special2s.""" + range_start = 1 + range_end = 100 + default = 80 + display_name = "Percent Special2s Required" + + +class TotalSpecial2s(Range): + """How many Speical2 jewels are in the pool in total when Dracula's Condition is Special2s.""" + range_start = 1 + range_end = 70 + default = 25 + display_name = "Total Special2s" + + +class BossesRequired(Range): + """How many bosses need to be defeated to enter Dracula's chamber when Dracula's Condition is set to Bosses. + This will automatically adjust if there are fewer available bosses than the chosen number.""" + range_start = 1 + range_end = 16 + default = 14 + display_name = "Bosses Required" + + +class CarrieLogic(Toggle): + """Adds the 2 checks inside Underground Waterway's crawlspace to the pool. + If you (and everyone else if racing the same seed) are planning to only ever play Reinhardt, don't enable this. + Can be combined with Hard Logic to include Carrie-only tricks.""" + display_name = "Carrie Logic" + + +class HardLogic(Toggle): + """Properly considers sequence break tricks in logic (i.e. maze skip). Can be combined with Carrie Logic to include + Carrie-only tricks. + See the Game Page for a full list of tricks and glitches that may be logically required.""" + display_name = "Hard Logic" + + +class MultiHitBreakables(Toggle): + """Adds the items that drop from the objects that break in three hits to the pool. There are 17 of these throughout + the game, adding up to 74 checks in total with all stages. + The game will be modified to + remember exactly which of their items you've picked up instead of simply whether they were broken or not.""" + display_name = "Multi-hit Breakables" + + +class EmptyBreakables(Toggle): + """Adds 9 check locations in the form of breakables that normally have nothing (all empty Forest coffins, etc.) + and some additional Red Jewels and/or moneybags into the item pool to compensate.""" + display_name = "Empty Breakables" + + +class LizardLockerItems(Toggle): + """Adds the 6 items inside Castle Center 2F's Lizard-man generators to the pool. + Picking up all of these can be a very tedious luck-based process, so they are off by default.""" + display_name = "Lizard Locker Items" + + +class Shopsanity(Toggle): + """Adds 7 one-time purchases from Renon's shop into the location pool. After buying an item from a slot, it will + revert to whatever it is in the vanilla game.""" + display_name = "Shopsanity" + + +class ShopPrices(Choice): + """Randomizes the amount of gold each item costs in Renon's shop. + Use the below options to control how much or little an item can cost.""" + display_name = "Shop Prices" + option_vanilla = 0 + option_randomized = 1 + default = 0 + + +class MinimumGoldPrice(Range): + """The lowest amount of gold an item can cost in Renon's shop, divided by 100.""" + display_name = "Minimum Gold Price" + range_start = 1 + range_end = 50 + default = 2 + + +class MaximumGoldPrice(Range): + """The highest amount of gold an item can cost in Renon's shop, divided by 100.""" + display_name = "Maximum Gold Price" + range_start = 1 + range_end = 50 + default = 30 + + +class PostBehemothBoss(Choice): + """Sets which boss is fought in the vampire triplets' room in Castle Center by which characters after defeating + Behemoth.""" + display_name = "Post-Behemoth Boss" + option_vanilla = 0 + option_inverted = 1 + option_always_rosa = 2 + option_always_camilla = 3 + default = 0 + + +class RoomOfClocksBoss(Choice): + """Sets which boss is fought at Room of Clocks by which characters.""" + display_name = "Room of Clocks Boss" + option_vanilla = 0 + option_inverted = 1 + option_always_death = 2 + option_always_actrise = 3 + default = 0 + + +class RenonFightCondition(Choice): + """Sets the condition on which the Renon fight will trigger.""" + display_name = "Renon Fight Condition" + option_never = 0 + option_spend_30k = 1 + option_always = 2 + default = 1 + + +class VincentFightCondition(Choice): + """Sets the condition on which the vampire Vincent fight will trigger.""" + display_name = "Vincent Fight Condition" + option_never = 0 + option_wait_16_days = 1 + option_always = 2 + default = 1 + + +class BadEndingCondition(Choice): + """Sets the condition on which the currently-controlled character's Bad Ending will trigger.""" + display_name = "Bad Ending Condition" + option_never = 0 + option_kill_vincent = 1 + option_always = 2 + default = 1 + + +class IncreaseItemLimit(DefaultOnToggle): + """Increases the holding limit of usable items from 10 to 99 of each item.""" + display_name = "Increase Item Limit" + + +class NerfHealingItems(Toggle): + """Decreases the amount of health healed by Roast Chickens to 25%, Roast Beefs to 50%, and Healing Kits to 80%.""" + display_name = "Nerf Healing Items" + + +class LoadingZoneHeals(DefaultOnToggle): + """Whether end-of-level loading zones restore health and cure status aliments or not. + Recommended off for those looking for more of a survival horror experience!""" + display_name = "Loading Zone Heals" + + +class InvisibleItems(Choice): + """Sets which items are visible in their locations and which are invisible until picked up. + 'Chance' gives each item a 50/50 chance of being visible or invisible.""" + display_name = "Invisible Items" + option_vanilla = 0 + option_reveal_all = 1 + option_hide_all = 2 + option_chance = 3 + default = 0 + + +class DropPreviousSubWeapon(Toggle): + """When receiving a sub-weapon, the one you had before will drop behind you, so it can be taken back if desired.""" + display_name = "Drop Previous Sub-weapon" + + +class PermanentPowerUps(Toggle): + """Replaces PowerUps with PermaUps, which upgrade your B weapon level permanently and will stay even after + dying and/or continuing. + To compensate, only two will be in the pool overall, and they will not drop from any enemy or projectile.""" + display_name = "Permanent PowerUps" + + +class IceTrapPercentage(Range): + """Replaces a percentage of junk items with Ice Traps. + These will be visibly disguised as other items, and receiving one will freeze you + as if you were hit by Camilla's ice cloud attack.""" + display_name = "Ice Trap Percentage" + range_start = 0 + range_end = 100 + default = 0 + + +class IceTrapAppearance(Choice): + """What items Ice Traps can possibly be disguised as.""" + display_name = "Ice Trap Appearance" + option_major_only = 0 + option_junk_only = 1 + option_anything = 2 + default = 0 + + +class DisableTimeRestrictions(Toggle): + """Disables the restriction on every event and door that requires the current time + to be within a specific range, so they can be triggered at any time. + This includes all sun/moon doors and, in the Villa, the meeting with Rosa and the fountain pillar. + The Villa coffin is not affected by this.""" + display_name = "Disable Time Requirements" + + +class SkipGondolas(Toggle): + """Makes jumping on and activating a gondola in Tunnel instantly teleport you + to the other station, thereby skipping the entire three-minute ride. + The item normally at the gondola transfer point is moved to instead be + near the red gondola at its station.""" + display_name = "Skip Gondolas" + + +class SkipWaterwayBlocks(Toggle): + """Opens the door to the third switch in Underground Waterway from the start so that the jumping across floating + brick platforms won't have to be done. Shopping at the Contract on the other side of them may still be logically + required if Shopsanity is on.""" + display_name = "Skip Waterway Blocks" + + +class Countdown(Choice): + """Displays, near the HUD clock and below the health bar, the number of unobtained progression-marked items + or the total check locations remaining in the stage you are currently in.""" + display_name = "Countdown" + option_none = 0 + option_majors = 1 + option_all_locations = 2 + default = 0 + + +class BigToss(Toggle): + """Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. + Press A while tossed to cancel the launch momentum and avoid being thrown off ledges. + Hold Z to have all incoming damage be treated as it normally would. + Any tricks that might be possible with it are NOT considered in logic on any setting.""" + display_name = "Big Toss" + + +class PantherDash(Choice): + """Hold C-right at any time to sprint way faster. Any tricks that might be + possible with it are NOT considered in logic on any setting and any boss + fights with boss health meters, if started, are expected to be finished + before leaving their arenas if Dracula's Condition is bosses. Jumpless will + prevent jumping while moving at the increased speed to ensure logic cannot be broken with it.""" + display_name = "Panther Dash" + option_off = 0 + option_on = 1 + option_jumpless = 2 + default = 0 + + +class IncreaseShimmySpeed(Toggle): + """Increases the speed at which characters shimmy left and right while hanging on ledges.""" + display_name = "Increase Shimmy Speed" + + +class FallGuard(Toggle): + """Removes fall damage from landing too hard. Note that falling for too long will still result in instant death.""" + display_name = "Fall Guard" + + +class BackgroundMusic(Choice): + """Randomizes or disables the music heard throughout the game. + Randomized music is split into two pools: songs that loop and songs that don't. + The "lead-in" versions of some songs will be paired accordingly.""" + display_name = "Background Music" + option_normal = 0 + option_disabled = 1 + option_randomized = 2 + default = 0 + + +class MapLighting(Choice): + """Randomizes the lighting color RGB values on every map during every time of day to be literally anything. + The colors and/or shading of the following things are affected: fog, maps, player, enemies, and some objects.""" + display_name = "Map Lighting" + option_normal = 0 + option_randomized = 1 + default = 0 + + +class CinematicExperience(Toggle): + """Enables an unused film reel effect on every cutscene in the game. Purely cosmetic.""" + display_name = "Cinematic Experience" + + +class WindowColorR(Range): + """The red value for the background color of the text windows during gameplay.""" + display_name = "Window Color R" + range_start = 0 + range_end = 15 + default = 1 + + +class WindowColorG(Range): + """The green value for the background color of the text windows during gameplay.""" + display_name = "Window Color G" + range_start = 0 + range_end = 15 + default = 5 + + +class WindowColorB(Range): + """The blue value for the background color of the text windows during gameplay.""" + display_name = "Window Color B" + range_start = 0 + range_end = 15 + default = 15 + + +class WindowColorA(Range): + """The alpha value for the background color of the text windows during gameplay.""" + display_name = "Window Color A" + range_start = 0 + range_end = 15 + default = 8 + + +class DeathLink(Choice): + """When you die, everyone dies. Of course the reverse is true too. + Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion + instead of the normal death animation.""" + display_name = "DeathLink" + option_off = 0 + alias_no = 0 + alias_true = 1 + alias_yes = 1 + option_on = 1 + option_explosive = 2 + + +@dataclass +class CV64Options(PerGameCommonOptions): + character_stages: CharacterStages + stage_shuffle: StageShuffle + starting_stage: StartingStage + warp_order: WarpOrder + sub_weapon_shuffle: SubWeaponShuffle + spare_keys: SpareKeys + hard_item_pool: HardItemPool + special1s_per_warp: Special1sPerWarp + total_special1s: TotalSpecial1s + draculas_condition: DraculasCondition + percent_special2s_required: PercentSpecial2sRequired + total_special2s: TotalSpecial2s + bosses_required: BossesRequired + carrie_logic: CarrieLogic + hard_logic: HardLogic + multi_hit_breakables: MultiHitBreakables + empty_breakables: EmptyBreakables + lizard_locker_items: LizardLockerItems + shopsanity: Shopsanity + shop_prices: ShopPrices + minimum_gold_price: MinimumGoldPrice + maximum_gold_price: MaximumGoldPrice + post_behemoth_boss: PostBehemothBoss + room_of_clocks_boss: RoomOfClocksBoss + renon_fight_condition: RenonFightCondition + vincent_fight_condition: VincentFightCondition + bad_ending_condition: BadEndingCondition + increase_item_limit: IncreaseItemLimit + nerf_healing_items: NerfHealingItems + loading_zone_heals: LoadingZoneHeals + invisible_items: InvisibleItems + drop_previous_sub_weapon: DropPreviousSubWeapon + permanent_powerups: PermanentPowerUps + ice_trap_percentage: IceTrapPercentage + ice_trap_appearance: IceTrapAppearance + disable_time_restrictions: DisableTimeRestrictions + skip_gondolas: SkipGondolas + skip_waterway_blocks: SkipWaterwayBlocks + countdown: Countdown + big_toss: BigToss + panther_dash: PantherDash + increase_shimmy_speed: IncreaseShimmySpeed + background_music: BackgroundMusic + map_lighting: MapLighting + fall_guard: FallGuard + cinematic_experience: CinematicExperience + window_color_r: WindowColorR + window_color_g: WindowColorG + window_color_b: WindowColorB + window_color_a: WindowColorA + death_link: DeathLink + start_inventory_from_pool: StartInventoryPool diff --git a/worlds/cv64/regions.py b/worlds/cv64/regions.py new file mode 100644 index 000000000000..2194828a19ae --- /dev/null +++ b/worlds/cv64/regions.py @@ -0,0 +1,517 @@ +from .data import lname, rname, ename +from typing import List, Union + + +# # # KEY # # # +# "stage" = What stage the Region is a part of. The Region and its corresponding Locations and Entrances will only be +# put in if its stage is active. +# "locations" = The Locations to add to that Region when putting in said Region (provided their add conditions pass). +# "entrances" = The Entrances to add to that Region when putting in said Region (provided their add conditions pass). +region_info = { + "Menu": {}, + + rname.forest_start: {"stage": rname.forest_of_silence, + "locations": [lname.forest_pillars_right, + lname.forest_pillars_left, + lname.forest_pillars_top, + lname.forest_king_skeleton, + lname.forest_boss_one, + lname.forest_lgaz_in, + lname.forest_lgaz_top, + lname.forest_hgaz_in, + lname.forest_hgaz_top, + lname.forest_weretiger_sw, + lname.forest_boss_two, + lname.forest_weretiger_gate, + lname.forest_dirge_tomb_l, + lname.forest_dirge_tomb_u, + lname.forest_dirge_plaque, + lname.forest_dirge_ped, + lname.forest_dirge_rock1, + lname.forest_dirge_rock2, + lname.forest_dirge_rock3, + lname.forest_dirge_rock4, + lname.forest_dirge_rock5, + lname.forest_corpse_save, + lname.forest_dbridge_wall, + lname.forest_dbridge_sw], + "entrances": [ename.forest_dbridge_gate]}, + + rname.forest_mid: {"stage": rname.forest_of_silence, + "locations": [lname.forest_dbridge_gate_l, + lname.forest_dbridge_gate_r, + lname.forest_dbridge_tomb_l, + lname.forest_dbridge_tomb_ur, + lname.forest_dbridge_tomb_uf, + lname.forest_bface_tomb_lf, + lname.forest_bface_tomb_lr, + lname.forest_bface_tomb_u, + lname.forest_ibridge, + lname.forest_bridge_rock1, + lname.forest_bridge_rock2, + lname.forest_bridge_rock3, + lname.forest_bridge_rock4, + lname.forest_werewolf_tomb_lf, + lname.forest_werewolf_tomb_lr, + lname.forest_werewolf_tomb_r, + lname.forest_werewolf_plaque, + lname.forest_werewolf_tree, + lname.forest_werewolf_island, + lname.forest_final_sw], + "entrances": [ename.forest_werewolf_gate]}, + + rname.forest_end: {"stage": rname.forest_of_silence, + "locations": [lname.forest_boss_three], + "entrances": [ename.forest_end]}, + + rname.cw_start: {"stage": rname.castle_wall, + "locations": [lname.cwr_bottom, + lname.cw_dragon_sw, + lname.cw_boss, + lname.cw_save_slab1, + lname.cw_save_slab2, + lname.cw_save_slab3, + lname.cw_save_slab4, + lname.cw_save_slab5, + lname.cw_rrampart, + lname.cw_lrampart, + lname.cw_pillar, + lname.cw_shelf_visible, + lname.cw_shelf_sandbags, + lname.cw_shelf_torch], + "entrances": [ename.cw_portcullis_c, + ename.cw_lt_skip, + ename.cw_lt_door]}, + + rname.cw_exit: {"stage": rname.castle_wall, + "locations": [lname.cw_ground_left, + lname.cw_ground_middle, + lname.cw_ground_right]}, + + rname.cw_ltower: {"stage": rname.castle_wall, + "locations": [lname.cwl_bottom, + lname.cwl_bridge, + lname.cw_drac_sw, + lname.cw_drac_slab1, + lname.cw_drac_slab2, + lname.cw_drac_slab3, + lname.cw_drac_slab4, + lname.cw_drac_slab5], + "entrances": [ename.cw_end]}, + + rname.villa_start: {"stage": rname.villa, + "locations": [lname.villafy_outer_gate_l, + lname.villafy_outer_gate_r, + lname.villafy_dog_platform, + lname.villafy_inner_gate], + "entrances": [ename.villa_dog_gates]}, + + rname.villa_main: {"stage": rname.villa, + "locations": [lname.villafy_gate_marker, + lname.villafy_villa_marker, + lname.villafy_tombstone, + lname.villafy_fountain_fl, + lname.villafy_fountain_fr, + lname.villafy_fountain_ml, + lname.villafy_fountain_mr, + lname.villafy_fountain_rl, + lname.villafy_fountain_rr, + lname.villafo_front_r, + lname.villafo_front_l, + lname.villafo_mid_l, + lname.villafo_mid_r, + lname.villafo_rear_r, + lname.villafo_rear_l, + lname.villafo_pot_r, + lname.villafo_pot_l, + lname.villafo_sofa, + lname.villafo_chandelier1, + lname.villafo_chandelier2, + lname.villafo_chandelier3, + lname.villafo_chandelier4, + lname.villafo_chandelier5, + lname.villala_hallway_stairs, + lname.villala_hallway_l, + lname.villala_hallway_r, + lname.villala_bedroom_chairs, + lname.villala_bedroom_bed, + lname.villala_vincent, + lname.villala_slivingroom_table, + lname.villala_slivingroom_mirror, + lname.villala_diningroom_roses, + lname.villala_llivingroom_pot_r, + lname.villala_llivingroom_pot_l, + lname.villala_llivingroom_painting, + lname.villala_llivingroom_light, + lname.villala_llivingroom_lion, + lname.villala_exit_knight], + "entrances": [ename.villa_snipe_dogs, + ename.villa_renon, + ename.villa_to_storeroom, + ename.villa_to_archives, + ename.villa_to_maze]}, + + rname.villa_storeroom: {"stage": rname.villa, + "locations": [lname.villala_storeroom_l, + lname.villala_storeroom_r, + lname.villala_storeroom_s], + "entrances": [ename.villa_from_storeroom]}, + + rname.villa_archives: {"stage": rname.villa, + "locations": [lname.villala_archives_entrance, + lname.villala_archives_table, + lname.villala_archives_rear]}, + + rname.villa_maze: {"stage": rname.villa, + "locations": [lname.villam_malus_torch, + lname.villam_malus_bush, + lname.villam_fplatform, + lname.villam_frankieturf_l, + lname.villam_frankieturf_r, + lname.villam_frankieturf_ru, + lname.villam_fgarden_f, + lname.villam_fgarden_mf, + lname.villam_fgarden_mr, + lname.villam_fgarden_r, + lname.villam_rplatform, + lname.villam_rplatform_de, + lname.villam_exit_de, + lname.villam_serv_path], + "entrances": [ename.villa_from_maze, + ename.villa_copper_door, + ename.villa_copper_skip]}, + + rname.villa_servants: {"stage": rname.villa, + "locations": [lname.villafo_serv_ent], + "entrances": [ename.villa_servant_door]}, + + rname.villa_crypt: {"stage": rname.villa, + "locations": [lname.villam_crypt_ent, + lname.villam_crypt_upstream, + lname.villac_ent_l, + lname.villac_ent_r, + lname.villac_wall_l, + lname.villac_wall_r, + lname.villac_coffin_l, + lname.villac_coffin_r, + lname.villa_boss_one, + lname.villa_boss_two], + "entrances": [ename.villa_bridge_door, + ename.villa_end_r, + ename.villa_end_c]}, + + rname.tunnel_start: {"stage": rname.tunnel, + "locations": [lname.tunnel_landing, + lname.tunnel_landing_rc, + lname.tunnel_stone_alcove_r, + lname.tunnel_stone_alcove_l, + lname.tunnel_twin_arrows, + lname.tunnel_arrows_rock1, + lname.tunnel_arrows_rock2, + lname.tunnel_arrows_rock3, + lname.tunnel_arrows_rock4, + lname.tunnel_arrows_rock5, + lname.tunnel_lonesome_bucket, + lname.tunnel_lbucket_mdoor_l, + lname.tunnel_lbucket_quag, + lname.tunnel_bucket_quag_rock1, + lname.tunnel_bucket_quag_rock2, + lname.tunnel_bucket_quag_rock3, + lname.tunnel_lbucket_albert, + lname.tunnel_albert_camp, + lname.tunnel_albert_quag, + lname.tunnel_gondola_rc_sdoor_l, + lname.tunnel_gondola_rc_sdoor_m, + lname.tunnel_gondola_rc_sdoor_r, + lname.tunnel_gondola_rc, + lname.tunnel_rgondola_station, + lname.tunnel_gondola_transfer], + "entrances": [ename.tunnel_start_renon, + ename.tunnel_gondolas]}, + + rname.tunnel_end: {"stage": rname.tunnel, + "locations": [lname.tunnel_corpse_bucket_quag, + lname.tunnel_corpse_bucket_mdoor_l, + lname.tunnel_corpse_bucket_mdoor_r, + lname.tunnel_shovel_quag_start, + lname.tunnel_exit_quag_start, + lname.tunnel_shovel_quag_end, + lname.tunnel_exit_quag_end, + lname.tunnel_shovel, + lname.tunnel_shovel_save, + lname.tunnel_shovel_mdoor_l, + lname.tunnel_shovel_mdoor_r, + lname.tunnel_shovel_sdoor_l, + lname.tunnel_shovel_sdoor_m, + lname.tunnel_shovel_sdoor_r], + "entrances": [ename.tunnel_end_renon, + ename.tunnel_end]}, + + rname.uw_main: {"stage": rname.underground_waterway, + "locations": [lname.uw_near_ent, + lname.uw_across_ent, + lname.uw_first_ledge1, + lname.uw_first_ledge2, + lname.uw_first_ledge3, + lname.uw_first_ledge4, + lname.uw_first_ledge5, + lname.uw_first_ledge6, + lname.uw_poison_parkour, + lname.uw_boss, + lname.uw_waterfall_alcove, + lname.uw_carrie1, + lname.uw_carrie2, + lname.uw_bricks_save, + lname.uw_above_skel_ledge, + lname.uw_in_skel_ledge1, + lname.uw_in_skel_ledge2, + lname.uw_in_skel_ledge3], + "entrances": [ename.uw_final_waterfall, + ename.uw_renon]}, + + rname.uw_end: {"stage": rname.underground_waterway, + "entrances": [ename.uw_waterfall_skip, + ename.uw_end]}, + + rname.cc_main: {"stage": rname.castle_center, + "locations": [lname.ccb_skel_hallway_ent, + lname.ccb_skel_hallway_jun, + lname.ccb_skel_hallway_tc, + lname.ccb_skel_hallway_ba, + lname.ccb_behemoth_l_ff, + lname.ccb_behemoth_l_mf, + lname.ccb_behemoth_l_mr, + lname.ccb_behemoth_l_fr, + lname.ccb_behemoth_r_ff, + lname.ccb_behemoth_r_mf, + lname.ccb_behemoth_r_mr, + lname.ccb_behemoth_r_fr, + lname.ccb_behemoth_crate1, + lname.ccb_behemoth_crate2, + lname.ccb_behemoth_crate3, + lname.ccb_behemoth_crate4, + lname.ccb_behemoth_crate5, + lname.ccelv_near_machine, + lname.ccelv_atop_machine, + lname.ccelv_stand1, + lname.ccelv_stand2, + lname.ccelv_stand3, + lname.ccelv_pipes, + lname.ccelv_switch, + lname.ccelv_staircase, + lname.ccff_redcarpet_knight, + lname.ccff_gears_side, + lname.ccff_gears_mid, + lname.ccff_gears_corner, + lname.ccff_lizard_knight, + lname.ccff_lizard_near_knight, + lname.ccff_lizard_pit, + lname.ccff_lizard_corner, + lname.ccff_lizard_locker_nfr, + lname.ccff_lizard_locker_nmr, + lname.ccff_lizard_locker_nml, + lname.ccff_lizard_locker_nfl, + lname.ccff_lizard_locker_fl, + lname.ccff_lizard_locker_fr, + lname.ccff_lizard_slab1, + lname.ccff_lizard_slab2, + lname.ccff_lizard_slab3, + lname.ccff_lizard_slab4, + lname.ccll_brokenstairs_floor, + lname.ccll_brokenstairs_knight, + lname.ccll_brokenstairs_save, + lname.ccll_glassknight_l, + lname.ccll_glassknight_r, + lname.ccll_butlers_door, + lname.ccll_butlers_side, + lname.ccll_cwhall_butlerflames_past, + lname.ccll_cwhall_flamethrower, + lname.ccll_cwhall_cwflames, + lname.ccll_heinrich, + lname.ccia_nitro_crates, + lname.ccia_nitro_shelf_h, + lname.ccia_stairs_knight, + lname.ccia_maids_vase, + lname.ccia_maids_outer, + lname.ccia_maids_inner, + lname.ccia_inventions_maids, + lname.ccia_inventions_crusher, + lname.ccia_inventions_famicart, + lname.ccia_inventions_zeppelin, + lname.ccia_inventions_round, + lname.ccia_nitrohall_flamethrower, + lname.ccia_nitrohall_torch, + lname.ccia_nitro_shelf_i], + "entrances": [ename.cc_tc_door, + ename.cc_lower_wall, + ename.cc_renon, + ename.cc_upper_wall]}, + + rname.cc_torture_chamber: {"stage": rname.castle_center, + "locations": [lname.ccb_mandrag_shelf_l, + lname.ccb_mandrag_shelf_r, + lname.ccb_torture_rack, + lname.ccb_torture_rafters]}, + + rname.cc_library: {"stage": rname.castle_center, + "locations": [lname.ccll_cwhall_wall, + lname.ccl_bookcase]}, + + rname.cc_crystal: {"stage": rname.castle_center, + "locations": [lname.cc_behind_the_seal, + lname.cc_boss_one, + lname.cc_boss_two], + "entrances": [ename.cc_elevator]}, + + rname.cc_elev_top: {"stage": rname.castle_center, + "entrances": [ename.cc_exit_r, + ename.cc_exit_c]}, + + rname.dt_main: {"stage": rname.duel_tower, + "locations": [lname.dt_boss_one, + lname.dt_boss_two, + lname.dt_ibridge_l, + lname.dt_ibridge_r, + lname.dt_stones_start, + lname.dt_stones_end, + lname.dt_werebull_arena, + lname.dt_boss_three, + lname.dt_boss_four], + "entrances": [ename.dt_start, + ename.dt_end]}, + + rname.toe_main: {"stage": rname.tower_of_execution, + "locations": [lname.toe_ledge1, + lname.toe_ledge2, + lname.toe_ledge3, + lname.toe_ledge4, + lname.toe_ledge5, + lname.toe_midsavespikes_r, + lname.toe_midsavespikes_l, + lname.toe_elec_grate, + lname.toe_ibridge, + lname.toe_top], + "entrances": [ename.toe_start, + ename.toe_gate, + ename.toe_gate_skip, + ename.toe_end]}, + + rname.toe_ledge: {"stage": rname.tower_of_execution, + "locations": [lname.toe_keygate_l, + lname.toe_keygate_r]}, + + rname.tosci_start: {"stage": rname.tower_of_science, + "locations": [lname.tosci_elevator, + lname.tosci_plain_sr, + lname.tosci_stairs_sr], + "entrances": [ename.tosci_start, + ename.tosci_key1_door, + ename.tosci_to_key2_door]}, + + rname.tosci_three_doors: {"stage": rname.tower_of_science, + "locations": [lname.tosci_three_door_hall]}, + + rname.tosci_conveyors: {"stage": rname.tower_of_science, + "locations": [lname.tosci_ibridge_t, + lname.tosci_ibridge_b1, + lname.tosci_ibridge_b2, + lname.tosci_ibridge_b3, + lname.tosci_ibridge_b4, + lname.tosci_ibridge_b5, + lname.tosci_ibridge_b6, + lname.tosci_conveyor_sr, + lname.tosci_exit], + "entrances": [ename.tosci_from_key2_door, + ename.tosci_key3_door, + ename.tosci_end]}, + + rname.tosci_key3: {"stage": rname.tower_of_science, + "locations": [lname.tosci_key3_r, + lname.tosci_key3_m, + lname.tosci_key3_l]}, + + rname.tosor_main: {"stage": rname.tower_of_sorcery, + "locations": [lname.tosor_stained_tower, + lname.tosor_savepoint, + lname.tosor_trickshot, + lname.tosor_yellow_bubble, + lname.tosor_blue_platforms, + lname.tosor_side_isle, + lname.tosor_ibridge], + "entrances": [ename.tosor_start, + ename.tosor_end]}, + + rname.roc_main: {"stage": rname.room_of_clocks, + "locations": [lname.roc_ent_l, + lname.roc_ent_r, + lname.roc_elev_r, + lname.roc_elev_l, + lname.roc_cont_r, + lname.roc_cont_l, + lname.roc_exit, + lname.roc_boss], + "entrances": [ename.roc_gate]}, + + rname.ct_start: {"stage": rname.clock_tower, + "locations": [lname.ct_gearclimb_battery_slab1, + lname.ct_gearclimb_battery_slab2, + lname.ct_gearclimb_battery_slab3, + lname.ct_gearclimb_side, + lname.ct_gearclimb_corner, + lname.ct_gearclimb_door_slab1, + lname.ct_gearclimb_door_slab2, + lname.ct_gearclimb_door_slab3], + "entrances": [ename.ct_to_door1]}, + + rname.ct_middle: {"stage": rname.clock_tower, + "locations": [lname.ct_bp_chasm_fl, + lname.ct_bp_chasm_fr, + lname.ct_bp_chasm_rl, + lname.ct_bp_chasm_k], + "entrances": [ename.ct_from_door1, + ename.ct_to_door2]}, + + rname.ct_end: {"stage": rname.clock_tower, + "locations": [lname.ct_finalroom_door_slab1, + lname.ct_finalroom_door_slab2, + lname.ct_finalroom_fl, + lname.ct_finalroom_fr, + lname.ct_finalroom_rl, + lname.ct_finalroom_rr, + lname.ct_finalroom_platform, + lname.ct_finalroom_renon_slab1, + lname.ct_finalroom_renon_slab2, + lname.ct_finalroom_renon_slab3, + lname.ct_finalroom_renon_slab4, + lname.ct_finalroom_renon_slab5, + lname.ct_finalroom_renon_slab6, + lname.ct_finalroom_renon_slab7, + lname.ct_finalroom_renon_slab8], + "entrances": [ename.ct_from_door2, + ename.ct_renon, + ename.ct_door_3]}, + + rname.ck_main: {"stage": rname.castle_keep, + "locations": [lname.ck_boss_one, + lname.ck_boss_two, + lname.ck_flame_l, + lname.ck_flame_r, + lname.ck_behind_drac, + lname.ck_cube], + "entrances": [ename.ck_slope_jump, + ename.ck_drac_door]}, + + rname.renon: {"locations": [lname.renon1, + lname.renon2, + lname.renon3, + lname.renon4, + lname.renon5, + lname.renon6, + lname.renon7]}, + + rname.ck_drac_chamber: {"locations": [lname.the_end]} +} + + +def get_region_info(region: str, info: str) -> Union[str, List[str], None]: + return region_info[region].get(info, None) diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py new file mode 100644 index 000000000000..ab8c7030aa4e --- /dev/null +++ b/worlds/cv64/rom.py @@ -0,0 +1,959 @@ + +import Utils + +from BaseClasses import Location +from worlds.Files import APDeltaPatch +from typing import List, Dict, Union, Iterable, Collection, TYPE_CHECKING + +import hashlib +import os +import pkgutil + +from . import lzkn64 +from .data import patches +from .stages import get_stage_info +from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap +from .aesthetics import renon_item_dialogue, get_item_text_color +from .locations import get_location_info +from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \ + BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash +from settings import get_settings + +if TYPE_CHECKING: + from . import CV64World + +CV64US10HASH = "1cc5cf3b4d29d8c3ade957648b529dc1" +ROM_PLAYER_LIMIT = 65535 + +warp_map_offsets = [0xADF67, 0xADF77, 0xADF87, 0xADF97, 0xADFA7, 0xADFBB, 0xADFCB, 0xADFDF] + + +class LocalRom: + orig_buffer: None + buffer: bytearray + + def __init__(self, file: str) -> None: + self.orig_buffer = None + + with open(file, "rb") as stream: + self.buffer = bytearray(stream.read()) + + 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.buffer[address] + + def read_bytes(self, start_address: int, length: int) -> bytearray: + return self.buffer[start_address:start_address + length] + + def write_byte(self, address: int, value: int) -> None: + self.buffer[address] = value + + def write_bytes(self, start_address: int, values: Collection[int]) -> None: + self.buffer[start_address:start_address + len(values)] = values + + def write_int16(self, address: int, value: int) -> None: + value = value & 0xFFFF + self.write_bytes(address, [(value >> 8) & 0xFF, value & 0xFF]) + + def write_int16s(self, start_address: int, values: List[int]) -> None: + for i, value in enumerate(values): + self.write_int16(start_address + (i * 2), value) + + def write_int24(self, address: int, value: int) -> None: + value = value & 0xFFFFFF + self.write_bytes(address, [(value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]) + + def write_int24s(self, start_address: int, values: List[int]) -> None: + for i, value in enumerate(values): + self.write_int24(start_address + (i * 3), value) + + def write_int32(self, address, value: int) -> None: + value = value & 0xFFFFFFFF + self.write_bytes(address, [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]) + + def write_int32s(self, start_address: int, values: list) -> None: + for i, value in enumerate(values): + self.write_int32(start_address + (i * 4), value) + + def write_to_file(self, filepath: str) -> None: + with open(filepath, "wb") as outfile: + outfile.write(self.buffer) + + +def patch_rom(world: "CV64World", rom: LocalRom, offset_data: Dict[int, int], shop_name_list: List[str], + shop_desc_list: List[List[Union[int, str, None]]], shop_colors_list: List[bytearray], + active_locations: Iterable[Location]) -> None: + + multiworld = world.multiworld + options = world.options + player = world.player + active_stage_exits = world.active_stage_exits + s1s_per_warp = world.s1s_per_warp + active_warp_list = world.active_warp_list + required_s2s = world.required_s2s + total_s2s = world.total_s2s + + # NOP out the CRC BNEs + rom.write_int32(0x66C, 0x00000000) + rom.write_int32(0x678, 0x00000000) + + # Always offer Hard Mode on file creation + rom.write_int32(0xC8810, 0x240A0100) # ADDIU T2, R0, 0x0100 + + # Disable Easy Mode cutoff point at Castle Center elevator + rom.write_int32(0xD9E18, 0x240D0000) # ADDIU T5, R0, 0x0000 + + # Disable the Forest, Castle Wall, and Villa intro cutscenes and make it possible to change the starting level + rom.write_byte(0xB73308, 0x00) + rom.write_byte(0xB7331A, 0x40) + rom.write_byte(0xB7332B, 0x4C) + rom.write_byte(0xB6302B, 0x00) + rom.write_byte(0x109F8F, 0x00) + + # Prevent Forest end cutscene flag from setting so it can be triggered infinitely + rom.write_byte(0xEEA51, 0x01) + + # Hack to make the Forest, CW and Villa intro cutscenes play at the start of their levels no matter what map came + # before them + rom.write_int32(0x97244, 0x803FDD60) + rom.write_int32s(0xBFDD60, patches.forest_cw_villa_intro_cs_player) + + # Make changing the map ID to 0xFF reset the map. Helpful to work around a bug wherein the camera gets stuck when + # entering a loading zone that doesn't change the map. + rom.write_int32s(0x197B0, [0x0C0FF7E6, # JAL 0x803FDF98 + 0x24840008]) # ADDIU A0, A0, 0x0008 + rom.write_int32s(0xBFDF98, patches.map_id_refresher) + + # Enable swapping characters when loading into a map by holding L. + rom.write_int32(0x97294, 0x803FDFC4) + rom.write_int32(0x19710, 0x080FF80E) # J 0x803FE038 + rom.write_int32s(0xBFDFC4, patches.character_changer) + + # Villa coffin time-of-day hack + rom.write_byte(0xD9D83, 0x74) + rom.write_int32(0xD9D84, 0x080FF14D) # J 0x803FC534 + rom.write_int32s(0xBFC534, patches.coffin_time_checker) + + # Fix both Castle Center elevator bridges for both characters unless enabling only one character's stages. At which + # point one bridge will be always broken and one always repaired instead. + if options.character_stages == CharacterStages.option_reinhardt_only: + rom.write_int32(0x6CEAA0, 0x240B0000) # ADDIU T3, R0, 0x0000 + elif options.character_stages == CharacterStages.option_carrie_only: + rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001 + else: + rom.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001 + rom.write_int32(0x6CEAA4, 0x240D0001) # ADDIU T5, R0, 0x0001 + + # Were-bull arena flag hack + rom.write_int32(0x6E38F0, 0x0C0FF157) # JAL 0x803FC55C + rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter) + rom.write_int32(0xA949C, 0x0C0FF380) # JAL 0x803FCE00 + rom.write_int32s(0xBFCE00, patches.werebull_flag_pickup_setter) + + # Enable being able to carry multiple Special jewels, Nitros, and Mandragoras simultaneously + rom.write_int32(0xBF1F4, 0x3C038039) # LUI V1, 0x8039 + # Special1 + rom.write_int32(0xBF210, 0x80659C4B) # LB A1, 0x9C4B (V1) + rom.write_int32(0xBF214, 0x24A50001) # ADDIU A1, A1, 0x0001 + rom.write_int32(0xBF21C, 0xA0659C4B) # SB A1, 0x9C4B (V1) + # Special2 + rom.write_int32(0xBF230, 0x80659C4C) # LB A1, 0x9C4C (V1) + rom.write_int32(0xBF234, 0x24A50001) # ADDIU A1, A1, 0x0001 + rom.write_int32(0xbf23C, 0xA0659C4C) # SB A1, 0x9C4C (V1) + # Magical Nitro + rom.write_int32(0xBF360, 0x10000004) # B 0x8013C184 + rom.write_int32(0xBF378, 0x25E50001) # ADDIU A1, T7, 0x0001 + rom.write_int32(0xBF37C, 0x10000003) # B 0x8013C19C + # Mandragora + rom.write_int32(0xBF3A8, 0x10000004) # B 0x8013C1CC + rom.write_int32(0xBF3C0, 0x25050001) # ADDIU A1, T0, 0x0001 + rom.write_int32(0xBF3C4, 0x10000003) # B 0x8013C1E4 + + # Give PowerUps their Legacy of Darkness behavior when attempting to pick up more than two + rom.write_int16(0xA9624, 0x1000) + rom.write_int32(0xA9730, 0x24090000) # ADDIU T1, R0, 0x0000 + rom.write_int32(0xBF2FC, 0x080FF16D) # J 0x803FC5B4 + rom.write_int32(0xBF300, 0x00000000) # NOP + rom.write_int32s(0xBFC5B4, patches.give_powerup_stopper) + + # Rename the Wooden Stake and Rose to "You are a FOOL!" + rom.write_bytes(0xEFE34, + bytearray([0xFF, 0xFF, 0xA2, 0x0B]) + cv64_string_to_bytearray("You are a FOOL!", append_end=False)) + # Capitalize the "k" in "Archives key" to be consistent with...literally every other key name! + rom.write_byte(0xEFF21, 0x2D) + + # Skip the "There is a white jewel" text so checking one saves the game instantly. + rom.write_int32s(0xEFC72, [0x00020002 for _ in range(37)]) + rom.write_int32(0xA8FC0, 0x24020001) # ADDIU V0, R0, 0x0001 + # Skip the yes/no prompts when activating things. + rom.write_int32s(0xBFDACC, patches.map_text_redirector) + rom.write_int32(0xA9084, 0x24020001) # ADDIU V0, R0, 0x0001 + rom.write_int32(0xBEBE8, 0x0C0FF6B4) # JAL 0x803FDAD0 + # Skip Vincent and Heinrich's mandatory-for-a-check dialogue + rom.write_int32(0xBED9C, 0x0C0FF6DA) # JAL 0x803FDB68 + # Skip the long yes/no prompt in the CC planetarium to set the pieces. + rom.write_int32(0xB5C5DF, 0x24030001) # ADDIU V1, R0, 0x0001 + # Skip the yes/no prompt to activate the CC elevator. + rom.write_int32(0xB5E3FB, 0x24020001) # ADDIU V0, R0, 0x0001 + # Skip the yes/no prompts to set Nitro/Mandragora at both walls. + rom.write_int32(0xB5DF3E, 0x24030001) # ADDIU V1, R0, 0x0001 + + # Custom message if you try checking the downstairs CC crack before removing the seal. + rom.write_bytes(0xBFDBAC, cv64_string_to_bytearray("The Furious Nerd Curse\n" + "prevents you from setting\n" + "anything until the seal\n" + "is removed!", True)) + + rom.write_int32s(0xBFDD20, patches.special_descriptions_redirector) + + # Change the Stage Select menu options + rom.write_int32s(0xADF64, patches.warp_menu_rewrite) + rom.write_int32s(0x10E0C8, patches.warp_pointer_table) + for i in range(len(active_warp_list)): + if i == 0: + rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "start map id")) + rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "start spawn id")) + else: + rom.write_byte(warp_map_offsets[i], get_stage_info(active_warp_list[i], "mid map id")) + rom.write_byte(warp_map_offsets[i] + 4, get_stage_info(active_warp_list[i], "mid spawn id")) + + # Play the "teleportation" sound effect when teleporting + rom.write_int32s(0xAE088, [0x08004FAB, # J 0x80013EAC + 0x2404019E]) # ADDIU A0, R0, 0x019E + + # Change the Stage Select menu's text to reflect its new purpose + rom.write_bytes(0xEFAD0, cv64_string_to_bytearray(f"Where to...?\t{active_warp_list[0]}\t" + f"`{str(s1s_per_warp).zfill(2)} {active_warp_list[1]}\t" + f"`{str(s1s_per_warp * 2).zfill(2)} {active_warp_list[2]}\t" + f"`{str(s1s_per_warp * 3).zfill(2)} {active_warp_list[3]}\t" + f"`{str(s1s_per_warp * 4).zfill(2)} {active_warp_list[4]}\t" + f"`{str(s1s_per_warp * 5).zfill(2)} {active_warp_list[5]}\t" + f"`{str(s1s_per_warp * 6).zfill(2)} {active_warp_list[6]}\t" + f"`{str(s1s_per_warp * 7).zfill(2)} {active_warp_list[7]}")) + + # Lizard-man save proofing + rom.write_int32(0xA99AC, 0x080FF0B8) # J 0x803FC2E0 + rom.write_int32s(0xBFC2E0, patches.boss_save_stopper) + + # Disable or guarantee vampire Vincent's fight + if options.vincent_fight_condition == VincentFightCondition.option_never: + rom.write_int32(0xAACC0, 0x24010001) # ADDIU AT, R0, 0x0001 + rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000 + elif options.vincent_fight_condition == VincentFightCondition.option_always: + rom.write_int32(0xAACE0, 0x24180010) # ADDIU T8, R0, 0x0010 + else: + rom.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000 + + # Disable or guarantee Renon's fight + rom.write_int32(0xAACB4, 0x080FF1A4) # J 0x803FC690 + if options.renon_fight_condition == RenonFightCondition.option_never: + rom.write_byte(0xB804F0, 0x00) + rom.write_byte(0xB80632, 0x00) + rom.write_byte(0xB807E3, 0x00) + rom.write_byte(0xB80988, 0xB8) + rom.write_byte(0xB816BD, 0xB8) + rom.write_byte(0xB817CF, 0x00) + rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr) + elif options.renon_fight_condition == RenonFightCondition.option_always: + rom.write_byte(0xB804F0, 0x0C) + rom.write_byte(0xB80632, 0x0C) + rom.write_byte(0xB807E3, 0x0C) + rom.write_byte(0xB80988, 0xC4) + rom.write_byte(0xB816BD, 0xC4) + rom.write_byte(0xB817CF, 0x0C) + rom.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr) + else: + rom.write_int32s(0xBFC690, patches.renon_cutscene_checker) + + # NOP the Easy Mode check when buying a thing from Renon, so he can be triggered even on this mode. + rom.write_int32(0xBD8B4, 0x00000000) + + # Disable or guarantee the Bad Ending + if options.bad_ending_condition == BadEndingCondition.option_never: + rom.write_int32(0xAEE5C6, 0x3C0A0000) # LUI T2, 0x0000 + elif options.bad_ending_condition == BadEndingCondition.option_always: + rom.write_int32(0xAEE5C6, 0x3C0A0040) # LUI T2, 0x0040 + + # Play Castle Keep's song if teleporting in front of Dracula's door outside the escape sequence + rom.write_int32(0x6E937C, 0x080FF12E) # J 0x803FC4B8 + rom.write_int32s(0xBFC4B8, patches.ck_door_music_player) + + # Increase item capacity to 100 if "Increase Item Limit" is turned on + if options.increase_item_limit: + rom.write_byte(0xBF30B, 0x63) # Most items + rom.write_byte(0xBF3F7, 0x63) # Sun/Moon cards + rom.write_byte(0xBF353, 0x64) # Keys (increase regardless) + + # Change the item healing values if "Nerf Healing" is turned on + if options.nerf_healing_items: + rom.write_byte(0xB56371, 0x50) # Healing kit (100 -> 80) + rom.write_byte(0xB56374, 0x32) # Roast beef ( 80 -> 50) + rom.write_byte(0xB56377, 0x19) # Roast chicken ( 50 -> 25) + + # Disable loading zone healing if turned off + if not options.loading_zone_heals: + rom.write_byte(0xD99A5, 0x00) # Skip all loading zone checks + rom.write_byte(0xA9DFFB, 0x40) # Disable free heal from King Skeleton by reading the unused magic meter value + + # Disable spinning on the Special1 and 2 pickup models so colorblind people can more easily identify them + rom.write_byte(0xEE4F5, 0x00) # Special1 + rom.write_byte(0xEE505, 0x00) # Special2 + # Make the Special2 the same size as a Red jewel(L) to further distinguish them + rom.write_int32(0xEE4FC, 0x3FA66666) + + # Prevent the vanilla Magical Nitro transport's "can explode" flag from setting + rom.write_int32(0xB5D7AA, 0x00000000) # NOP + + # Ensure the vampire Nitro check will always pass, so they'll never not spawn and crash the Villa cutscenes + rom.write_byte(0xA6253D, 0x03) + + # Enable the Game Over's "Continue" menu starting the cursor on whichever checkpoint is most recent + rom.write_int32(0xB4DDC, 0x0C060D58) # JAL 0x80183560 + rom.write_int32s(0x106750, patches.continue_cursor_start_checker) + rom.write_int32(0x1C444, 0x080FF08A) # J 0x803FC228 + rom.write_int32(0x1C2A0, 0x080FF08A) # J 0x803FC228 + rom.write_int32s(0xBFC228, patches.savepoint_cursor_updater) + rom.write_int32(0x1C2D0, 0x080FF094) # J 0x803FC250 + rom.write_int32s(0xBFC250, patches.stage_start_cursor_updater) + rom.write_byte(0xB585C8, 0xFF) + + # Make the Special1 and 2 play sounds when you reach milestones with them. + rom.write_int32s(0xBFDA50, patches.special_sound_notifs) + rom.write_int32(0xBF240, 0x080FF694) # J 0x803FDA50 + rom.write_int32(0xBF220, 0x080FF69E) # J 0x803FDA78 + + # Add data for White Jewel #22 (the new Duel Tower savepoint) at the end of the White Jewel ID data list + rom.write_int16s(0x104AC8, [0x0000, 0x0006, + 0x0013, 0x0015]) + + # Take the contract in Waterway off of its 00400000 bitflag. + rom.write_byte(0x87E3DA, 0x00) + + # Spawn coordinates list extension + rom.write_int32(0xD5BF4, 0x080FF103) # J 0x803FC40C + rom.write_int32s(0xBFC40C, patches.spawn_coordinates_extension) + rom.write_int32s(0x108A5E, patches.waterway_end_coordinates) + + # Change the File Select stage numbers to match the new stage order. Also fix a vanilla issue wherein saving in a + # character-exclusive stage as the other character would incorrectly display the name of that character's equivalent + # stage on the save file instead of the one they're actually in. + rom.write_byte(0xC9FE3, 0xD4) + rom.write_byte(0xCA055, 0x08) + rom.write_byte(0xCA066, 0x40) + rom.write_int32(0xCA068, 0x860C17D0) # LH T4, 0x17D0 (S0) + rom.write_byte(0xCA06D, 0x08) + rom.write_byte(0x104A31, 0x01) + rom.write_byte(0x104A39, 0x01) + rom.write_byte(0x104A89, 0x01) + rom.write_byte(0x104A91, 0x01) + rom.write_byte(0x104A99, 0x01) + rom.write_byte(0x104AA1, 0x01) + + for stage in active_stage_exits: + for offset in get_stage_info(stage, "save number offsets"): + rom.write_byte(offset, active_stage_exits[stage]["position"]) + + # CC top elevator switch check + rom.write_int32(0x6CF0A0, 0x0C0FF0B0) # JAL 0x803FC2C0 + rom.write_int32s(0xBFC2C0, patches.elevator_flag_checker) + + # Disable time restrictions + if options.disable_time_restrictions: + # Fountain + rom.write_int32(0x6C2340, 0x00000000) # NOP + rom.write_int32(0x6C257C, 0x10000023) # B [forward 0x23] + # Rosa + rom.write_byte(0xEEAAB, 0x00) + rom.write_byte(0xEEAAD, 0x18) + # Moon doors + rom.write_int32(0xDC3E0, 0x00000000) # NOP + rom.write_int32(0xDC3E8, 0x00000000) # NOP + # Sun doors + rom.write_int32(0xDC410, 0x00000000) # NOP + rom.write_int32(0xDC418, 0x00000000) # NOP + + # Custom data-loading code + rom.write_int32(0x6B5028, 0x08060D70) # J 0x801835D0 + rom.write_int32s(0x1067B0, patches.custom_code_loader) + + # Custom remote item rewarding and DeathLink receiving code + rom.write_int32(0x19B98, 0x080FF000) # J 0x803FC000 + rom.write_int32s(0xBFC000, patches.remote_item_giver) + rom.write_int32s(0xBFE190, patches.subweapon_surface_checker) + + # Make received DeathLinks blow you to smithereens instead of kill you normally. + if options.death_link == DeathLink.option_explosive: + rom.write_int32(0x27A70, 0x10000008) # B [forward 0x08] + rom.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition) + + # Set the DeathLink ROM flag if it's on at all. + if options.death_link != DeathLink.option_off: + rom.write_byte(0xBFBFDE, 0x01) + + # DeathLink counter decrementer code + rom.write_int32(0x1C340, 0x080FF8F0) # J 0x803FE3C0 + rom.write_int32s(0xBFE3C0, patches.deathlink_counter_decrementer) + rom.write_int32(0x25B6C, 0x0080FF052) # J 0x803FC148 + rom.write_int32s(0xBFC148, patches.nitro_fall_killer) + + # Death flag un-setter on "Beginning of stage" state overwrite code + rom.write_int32(0x1C2B0, 0x080FF047) # J 0x803FC11C + rom.write_int32s(0xBFC11C, patches.death_flag_unsetter) + + # Warp menu-opening code + rom.write_int32(0xB9BA8, 0x080FF099) # J 0x803FC264 + rom.write_int32s(0xBFC264, patches.warp_menu_opener) + + # NPC item textbox hack + rom.write_int32(0xBF1DC, 0x080FF904) # J 0x803FE410 + rom.write_int32(0xBF1E0, 0x27BDFFE0) # ADDIU SP, SP, -0x20 + rom.write_int32s(0xBFE410, patches.npc_item_hack) + + # Sub-weapon check function hook + rom.write_int32(0xBF32C, 0x00000000) # NOP + rom.write_int32(0xBF330, 0x080FF05E) # J 0x803FC178 + rom.write_int32s(0xBFC178, patches.give_subweapon_stopper) + + # Warp menu Special1 restriction + rom.write_int32(0xADD68, 0x0C04AB12) # JAL 0x8012AC48 + rom.write_int32s(0xADE28, patches.stage_select_overwrite) + rom.write_byte(0xADE47, s1s_per_warp) + + # Dracula's door text pointer hijack + rom.write_int32(0xD69F0, 0x080FF141) # J 0x803FC504 + rom.write_int32s(0xBFC504, patches.dracula_door_text_redirector) + + # Dracula's chamber condition + rom.write_int32(0xE2FDC, 0x0804AB25) # J 0x8012AC78 + rom.write_int32s(0xADE84, patches.special_goal_checker) + rom.write_bytes(0xBFCC48, [0xA0, 0x00, 0xFF, 0xFF, 0xA0, 0x01, 0xFF, 0xFF, 0xA0, 0x02, 0xFF, 0xFF, 0xA0, 0x03, 0xFF, + 0xFF, 0xA0, 0x04, 0xFF, 0xFF, 0xA0, 0x05, 0xFF, 0xFF, 0xA0, 0x06, 0xFF, 0xFF, 0xA0, 0x07, + 0xFF, 0xFF, 0xA0, 0x08, 0xFF, 0xFF, 0xA0, 0x09]) + if options.draculas_condition == DraculasCondition.option_crystal: + rom.write_int32(0x6C8A54, 0x0C0FF0C1) # JAL 0x803FC304 + rom.write_int32s(0xBFC304, patches.crystal_special2_giver) + rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n" + f"You'll need the power\n" + f"of the basement crystal\n" + f"to undo the seal.", True)) + special2_name = "Crystal " + special2_text = "The crystal is on!\n" \ + "Time to teach the old man\n" \ + "a lesson!" + elif options.draculas_condition == DraculasCondition.option_bosses: + rom.write_int32(0xBBD50, 0x080FF18C) # J 0x803FC630 + rom.write_int32s(0xBFC630, patches.boss_special2_giver) + rom.write_int32s(0xBFC55C, patches.werebull_flag_unsetter_special2_electric_boogaloo) + rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n" + f"You'll need to defeat\n" + f"{required_s2s} powerful monsters\n" + f"to undo the seal.", True)) + special2_name = "Trophy " + special2_text = f"Proof you killed a powerful\n" \ + f"Night Creature. Earn {required_s2s}/{total_s2s}\n" \ + f"to battle Dracula." + elif options.draculas_condition == DraculasCondition.option_specials: + special2_name = "Special2" + rom.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n" + f"You'll need to find\n" + f"{required_s2s} Special2 jewels\n" + f"to undo the seal.", True)) + special2_text = f"Need {required_s2s}/{total_s2s} to kill Dracula.\n" \ + f"Looking closely, you see...\n" \ + f"a piece of him within?" + else: + rom.write_byte(0xADE8F, 0x00) + special2_name = "Special2" + special2_text = "If you're reading this,\n" \ + "how did you get a Special2!?" + rom.write_byte(0xADE8F, required_s2s) + # Change the Special2 name depending on the setting. + rom.write_bytes(0xEFD4E, cv64_string_to_bytearray(special2_name)) + # Change the Special1 and 2 menu descriptions to tell you how many you need to unlock a warp and fight Dracula + # respectively. + special_text_bytes = cv64_string_to_bytearray(f"{s1s_per_warp} per warp unlock.\n" + f"{options.total_special1s.value} exist in total.\n" + f"Z + R + START to warp.") + cv64_string_to_bytearray(special2_text) + rom.write_bytes(0xBFE53C, special_text_bytes) + + # On-the-fly TLB script modifier + rom.write_int32s(0xBFC338, patches.double_component_checker) + rom.write_int32s(0xBFC3D4, patches.downstairs_seal_checker) + rom.write_int32s(0xBFE074, patches.mandragora_with_nitro_setter) + rom.write_int32s(0xBFC700, patches.overlay_modifiers) + + # On-the-fly actor data modifier hook + rom.write_int32(0xEAB04, 0x080FF21E) # J 0x803FC878 + rom.write_int32s(0xBFC870, patches.map_data_modifiers) + + # Fix to make flags apply to freestanding invisible items properly + rom.write_int32(0xA84F8, 0x90CC0039) # LBU T4, 0x0039 (A2) + + # Fix locked doors to check the key counters instead of their vanilla key locations' bitflags + # Pickup flag check modifications: + rom.write_int32(0x10B2D8, 0x00000002) # Left Tower Door + rom.write_int32(0x10B2F0, 0x00000003) # Storeroom Door + rom.write_int32(0x10B2FC, 0x00000001) # Archives Door + rom.write_int32(0x10B314, 0x00000004) # Maze Gate + rom.write_int32(0x10B350, 0x00000005) # Copper Door + rom.write_int32(0x10B3A4, 0x00000006) # Torture Chamber Door + rom.write_int32(0x10B3B0, 0x00000007) # ToE Gate + rom.write_int32(0x10B3BC, 0x00000008) # Science Door1 + rom.write_int32(0x10B3C8, 0x00000009) # Science Door2 + rom.write_int32(0x10B3D4, 0x0000000A) # Science Door3 + rom.write_int32(0x6F0094, 0x0000000B) # CT Door 1 + rom.write_int32(0x6F00A4, 0x0000000C) # CT Door 2 + rom.write_int32(0x6F00B4, 0x0000000D) # CT Door 3 + # Item counter decrement check modifications: + rom.write_int32(0xEDA84, 0x00000001) # Archives Door + rom.write_int32(0xEDA8C, 0x00000002) # Left Tower Door + rom.write_int32(0xEDA94, 0x00000003) # Storeroom Door + rom.write_int32(0xEDA9C, 0x00000004) # Maze Gate + rom.write_int32(0xEDAA4, 0x00000005) # Copper Door + rom.write_int32(0xEDAAC, 0x00000006) # Torture Chamber Door + rom.write_int32(0xEDAB4, 0x00000007) # ToE Gate + rom.write_int32(0xEDABC, 0x00000008) # Science Door1 + rom.write_int32(0xEDAC4, 0x00000009) # Science Door2 + rom.write_int32(0xEDACC, 0x0000000A) # Science Door3 + rom.write_int32(0xEDAD4, 0x0000000B) # CT Door 1 + rom.write_int32(0xEDADC, 0x0000000C) # CT Door 2 + rom.write_int32(0xEDAE4, 0x0000000D) # CT Door 3 + + # Fix ToE gate's "unlocked" flag in the locked door flags table + rom.write_int16(0x10B3B6, 0x0001) + + rom.write_int32(0x10AB2C, 0x8015FBD4) # Maze Gates' check code pointer adjustments + rom.write_int32(0x10AB40, 0x8015FBD4) + rom.write_int32s(0x10AB50, [0x0D0C0000, + 0x8015FBD4]) + rom.write_int32s(0x10AB64, [0x0D0C0000, + 0x8015FBD4]) + rom.write_int32s(0xE2E14, patches.normal_door_hook) + rom.write_int32s(0xBFC5D0, patches.normal_door_code) + rom.write_int32s(0x6EF298, patches.ct_door_hook) + rom.write_int32s(0xBFC608, patches.ct_door_code) + # Fix key counter not decrementing if 2 or above + rom.write_int32(0xAA0E0, 0x24020000) # ADDIU V0, R0, 0x0000 + + # Make the Easy-only candle drops in Room of Clocks appear on any difficulty + rom.write_byte(0x9B518F, 0x01) + + # Slightly move some once-invisible freestanding items to be more visible + if options.invisible_items == InvisibleItems.option_reveal_all: + rom.write_byte(0x7C7F95, 0xEF) # Forest dirge maiden statue + rom.write_byte(0x7C7FA8, 0xAB) # Forest werewolf statue + rom.write_byte(0x8099C4, 0x8C) # Villa courtyard tombstone + rom.write_byte(0x83A626, 0xC2) # Villa living room painting + # rom.write_byte(0x83A62F, 0x64) # Villa Mary's room table + rom.write_byte(0xBFCB97, 0xF5) # CC torture instrument rack + rom.write_byte(0x8C44D5, 0x22) # CC red carpet hallway knight + rom.write_byte(0x8DF57C, 0xF1) # CC cracked wall hallway flamethrower + rom.write_byte(0x90FCD6, 0xA5) # CC nitro hallway flamethrower + rom.write_byte(0x90FB9F, 0x9A) # CC invention room round machine + rom.write_byte(0x90FBAF, 0x03) # CC invention room giant famicart + rom.write_byte(0x90FE54, 0x97) # CC staircase knight (x) + rom.write_byte(0x90FE58, 0xFB) # CC staircase knight (z) + + # Change bitflag on item in upper coffin in Forest final switch gate tomb to one that's not used by something else + rom.write_int32(0x10C77C, 0x00000002) + + # Make the torch directly behind Dracula's chamber that normally doesn't set a flag set bitflag 0x08 in 0x80389BFA + rom.write_byte(0x10CE9F, 0x01) + + # Change the CC post-Behemoth boss depending on the option for Post-Behemoth Boss + if options.post_behemoth_boss == PostBehemothBoss.option_inverted: + rom.write_byte(0xEEDAD, 0x02) + rom.write_byte(0xEEDD9, 0x01) + elif options.post_behemoth_boss == PostBehemothBoss.option_always_rosa: + rom.write_byte(0xEEDAD, 0x00) + rom.write_byte(0xEEDD9, 0x03) + # Put both on the same flag so changing character won't trigger a rematch with the same boss. + rom.write_byte(0xEED8B, 0x40) + elif options.post_behemoth_boss == PostBehemothBoss.option_always_camilla: + rom.write_byte(0xEEDAD, 0x03) + rom.write_byte(0xEEDD9, 0x00) + rom.write_byte(0xEED8B, 0x40) + + # Change the RoC boss depending on the option for Room of Clocks Boss + if options.room_of_clocks_boss == RoomOfClocksBoss.option_inverted: + rom.write_byte(0x109FB3, 0x56) + rom.write_byte(0x109FBF, 0x44) + rom.write_byte(0xD9D44, 0x14) + rom.write_byte(0xD9D4C, 0x14) + elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_death: + rom.write_byte(0x109FBF, 0x44) + rom.write_byte(0xD9D45, 0x00) + # Put both on the same flag so changing character won't trigger a rematch with the same boss. + rom.write_byte(0x109FB7, 0x90) + rom.write_byte(0x109FC3, 0x90) + elif options.room_of_clocks_boss == RoomOfClocksBoss.option_always_actrise: + rom.write_byte(0x109FB3, 0x56) + rom.write_int32(0xD9D44, 0x00000000) + rom.write_byte(0xD9D4D, 0x00) + rom.write_byte(0x109FB7, 0x90) + rom.write_byte(0x109FC3, 0x90) + + # Un-nerf Actrise when playing as Reinhardt. + # This is likely a leftover TGS demo feature in which players could battle Actrise as Reinhardt. + rom.write_int32(0xB318B4, 0x240E0001) # ADDIU T6, R0, 0x0001 + + # Tunnel gondola skip + if options.skip_gondolas: + rom.write_int32(0x6C5F58, 0x080FF7D0) # J 0x803FDF40 + rom.write_int32s(0xBFDF40, patches.gondola_skipper) + # New gondola transfer point candle coordinates + rom.write_byte(0xBFC9A3, 0x04) + rom.write_bytes(0x86D824, [0x27, 0x01, 0x10, 0xF7, 0xA0]) + + # Waterway brick platforms skip + if options.skip_waterway_blocks: + rom.write_int32(0x6C7E2C, 0x00000000) # NOP + + # Ambience silencing fix + rom.write_int32(0xD9270, 0x080FF840) # J 0x803FE100 + rom.write_int32s(0xBFE100, patches.ambience_silencer) + # Fix for the door sliding sound playing infinitely if leaving the fan meeting room before the door closes entirely. + # Hooking this in the ambience silencer code does nothing for some reason. + rom.write_int32s(0xAE10C, [0x08004FAB, # J 0x80013EAC + 0x3404829B]) # ORI A0, R0, 0x829B + rom.write_int32s(0xD9E8C, [0x08004FAB, # J 0x80013EAC + 0x3404829B]) # ORI A0, R0, 0x829B + # Fan meeting room ambience fix + rom.write_int32(0x109964, 0x803FE13C) + + # Make the Villa coffin cutscene skippable + rom.write_int32(0xAA530, 0x080FF880) # J 0x803FE200 + rom.write_int32s(0xBFE200, patches.coffin_cutscene_skipper) + + # Increase shimmy speed + if options.increase_shimmy_speed: + rom.write_byte(0xA4241, 0x5A) + + # Disable landing fall damage + if options.fall_guard: + rom.write_byte(0x27B23, 0x00) + + # Enable the unused film reel effect on all cutscenes + if options.cinematic_experience: + rom.write_int32(0xAA33C, 0x240A0001) # ADDIU T2, R0, 0x0001 + rom.write_byte(0xAA34B, 0x0C) + rom.write_int32(0xAA4C4, 0x24090001) # ADDIU T1, R0, 0x0001 + + # Permanent PowerUp stuff + if options.permanent_powerups: + # Make receiving PowerUps increase the unused menu PowerUp counter instead of the one outside the save struct + rom.write_int32(0xBF2EC, 0x806B619B) # LB T3, 0x619B (V1) + rom.write_int32(0xBFC5BC, 0xA06C619B) # SB T4, 0x619B (V1) + # Make Reinhardt's whip check the menu PowerUp counter + rom.write_int32(0x69FA08, 0x80CC619B) # LB T4, 0x619B (A2) + rom.write_int32(0x69FBFC, 0x80C3619B) # LB V1, 0x619B (A2) + rom.write_int32(0x69FFE0, 0x818C9C53) # LB T4, 0x9C53 (T4) + # Make Carrie's orb check the menu PowerUp counter + rom.write_int32(0x6AC86C, 0x8105619B) # LB A1, 0x619B (T0) + rom.write_int32(0x6AC950, 0x8105619B) # LB A1, 0x619B (T0) + rom.write_int32(0x6AC99C, 0x810E619B) # LB T6, 0x619B (T0) + rom.write_int32(0x5AFA0, 0x80639C53) # LB V1, 0x9C53 (V1) + rom.write_int32(0x5B0A0, 0x81089C53) # LB T0, 0x9C53 (T0) + rom.write_byte(0x391C7, 0x00) # Prevent PowerUps from dropping from regular enemies + rom.write_byte(0xEDEDF, 0x03) # Make any vanishing PowerUps that do show up L jewels instead + # Rename the PowerUp to "PermaUp" + rom.write_bytes(0xEFDEE, cv64_string_to_bytearray("PermaUp")) + # Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized + if not options.multi_hit_breakables: + rom.write_byte(0x10C7A1, 0x03) + # Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other + # game PermaUps are distinguishable. + rom.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00]) + + # Write the randomized (or disabled) music ID list and its associated code + if options.background_music: + rom.write_int32(0x14588, 0x08060D60) # J 0x80183580 + rom.write_int32(0x14590, 0x00000000) # NOP + rom.write_int32s(0x106770, patches.music_modifier) + rom.write_int32(0x15780, 0x0C0FF36E) # JAL 0x803FCDB8 + rom.write_int32s(0xBFCDB8, patches.music_comparer_modifier) + + # Enable storing item flags anywhere and changing the item model/visibility on any item instance. + rom.write_int32s(0xA857C, [0x080FF38F, # J 0x803FCE3C + 0x94D90038]) # LHU T9, 0x0038 (A2) + rom.write_int32s(0xBFCE3C, patches.item_customizer) + rom.write_int32s(0xA86A0, [0x0C0FF3AF, # JAL 0x803FCEBC + 0x95C40002]) # LHU A0, 0x0002 (T6) + rom.write_int32s(0xBFCEBC, patches.item_appearance_switcher) + rom.write_int32s(0xA8728, [0x0C0FF3B8, # JAL 0x803FCEE4 + 0x01396021]) # ADDU T4, T1, T9 + rom.write_int32s(0xBFCEE4, patches.item_model_visibility_switcher) + rom.write_int32s(0xA8A04, [0x0C0FF3C2, # JAL 0x803FCF08 + 0x018B6021]) # ADDU T4, T4, T3 + rom.write_int32s(0xBFCF08, patches.item_shine_visibility_switcher) + + # Make Axes and Crosses in AP Locations drop to their correct height, and make items with changed appearances spin + # their correct speed. + rom.write_int32s(0xE649C, [0x0C0FFA03, # JAL 0x803FE80C + 0x956C0002]) # LHU T4, 0x0002 (T3) + rom.write_int32s(0xA8B08, [0x080FFA0C, # J 0x803FE830 + 0x960A0038]) # LHU T2, 0x0038 (S0) + rom.write_int32s(0xE8584, [0x0C0FFA21, # JAL 0x803FE884 + 0x95D80000]) # LHU T8, 0x0000 (T6) + rom.write_int32s(0xE7AF0, [0x0C0FFA2A, # JAL 0x803FE8A8 + 0x958D0000]) # LHU T5, 0x0000 (T4) + rom.write_int32s(0xBFE7DC, patches.item_drop_spin_corrector) + + # Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and + # setting flags instead. + if options.multi_hit_breakables: + rom.write_int32(0xE87F8, 0x00000000) # NOP + rom.write_int16(0xE836C, 0x1000) + rom.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34 + rom.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter) + # Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one) + rom.write_int32(0xE7D54, 0x00000000) # NOP + rom.write_int16(0xE7908, 0x1000) + rom.write_byte(0xE7A5C, 0x10) + rom.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C + rom.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter) + + # New flag values to put in each 3HB vanilla flag's spot + rom.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock + rom.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock + rom.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub + rom.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab + rom.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab + rom.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock + rom.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge + rom.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge + rom.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate + rom.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal + rom.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab + rom.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge + rom.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate + rom.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab + rom.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab + rom.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab + rom.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab + rom.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier + rom.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data + + # Once-per-frame gameplay checks + rom.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034 + rom.write_int32(0xBFD058, 0x0801AEB5) # J 0x8006BAD4 + + # Everything related to dropping the previous sub-weapon + if options.drop_previous_sub_weapon: + rom.write_int32(0xBFD034, 0x080FF3FF) # J 0x803FCFFC + rom.write_int32(0xBFC190, 0x080FF3F2) # J 0x803FCFC8 + rom.write_int32s(0xBFCFC4, patches.prev_subweapon_spawn_checker) + rom.write_int32s(0xBFCFFC, patches.prev_subweapon_fall_checker) + rom.write_int32s(0xBFD060, patches.prev_subweapon_dropper) + + # Everything related to the Countdown counter + if options.countdown: + rom.write_int32(0xBFD03C, 0x080FF9DC) # J 0x803FE770 + rom.write_int32(0xD5D48, 0x080FF4EC) # J 0x803FD3B0 + rom.write_int32s(0xBFD3B0, patches.countdown_number_displayer) + rom.write_int32s(0xBFD6DC, patches.countdown_number_manager) + rom.write_int32s(0xBFE770, patches.countdown_demo_hider) + rom.write_int32(0xBFCE2C, 0x080FF5D2) # J 0x803FD748 + rom.write_int32s(0xBB168, [0x080FF5F4, # J 0x803FD7D0 + 0x8E020028]) # LW V0, 0x0028 (S0) + rom.write_int32s(0xBB1D0, [0x080FF5FB, # J 0x803FD7EC + 0x8E020028]) # LW V0, 0x0028 (S0) + rom.write_int32(0xBC4A0, 0x080FF5E6) # J 0x803FD798 + rom.write_int32(0xBC4C4, 0x080FF5E6) # J 0x803FD798 + rom.write_int32(0x19844, 0x080FF602) # J 0x803FD808 + # If the option is set to "all locations", count it down no matter what the item is. + if options.countdown == Countdown.option_all_locations: + rom.write_int32s(0xBFD71C, [0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101, + 0x01010101, 0x01010101, 0x01010101, 0x01010101, 0x01010101]) + else: + # If it's majors, then insert this last minute check I threw together for the weird edge case of a CV64 ice + # trap for another CV64 player taking the form of a major. + rom.write_int32s(0xBFD788, [0x080FF717, # J 0x803FDC5C + 0x2529FFFF]) # ADDIU T1, T1, 0xFFFF + rom.write_int32s(0xBFDC5C, patches.countdown_extra_safety_check) + rom.write_int32(0xA9ECC, 0x00000000) # NOP the pointless overwrite of the item actor appearance custom value. + + # Ice Trap stuff + rom.write_int32(0x697C60, 0x080FF06B) # J 0x803FC18C + rom.write_int32(0x6A5160, 0x080FF06B) # J 0x803FC18C + rom.write_int32s(0xBFC1AC, patches.ice_trap_initializer) + rom.write_int32s(0xBFE700, patches.the_deep_freezer) + rom.write_int32s(0xB2F354, [0x3739E4C0, # ORI T9, T9, 0xE4C0 + 0x03200008, # JR T9 + 0x00000000]) # NOP + rom.write_int32s(0xBFE4C0, patches.freeze_verifier) + + # Initial Countdown numbers + rom.write_int32(0xAD6A8, 0x080FF60A) # J 0x803FD828 + rom.write_int32s(0xBFD828, patches.new_game_extras) + + # Everything related to shopsanity + if options.shopsanity: + rom.write_byte(0xBFBFDF, 0x01) + rom.write_bytes(0x103868, cv64_string_to_bytearray("Not obtained. ")) + rom.write_int32s(0xBFD8D0, patches.shopsanity_stuff) + rom.write_int32(0xBD828, 0x0C0FF643) # JAL 0x803FD90C + rom.write_int32(0xBD5B8, 0x0C0FF651) # JAL 0x803FD944 + rom.write_int32(0xB0610, 0x0C0FF665) # JAL 0x803FD994 + rom.write_int32s(0xBD24C, [0x0C0FF677, # J 0x803FD9DC + 0x00000000]) # NOP + rom.write_int32(0xBD618, 0x0C0FF684) # JAL 0x803FDA10 + + shopsanity_name_text = [] + shopsanity_desc_text = [] + for i in range(len(shop_name_list)): + shopsanity_name_text += bytearray([0xA0, i]) + shop_colors_list[i] + \ + cv64_string_to_bytearray(cv64_text_truncate(shop_name_list[i], 74)) + + shopsanity_desc_text += [0xA0, i] + if shop_desc_list[i][1] is not None: + shopsanity_desc_text += cv64_string_to_bytearray("For " + shop_desc_list[i][1] + ".\n", + append_end=False) + shopsanity_desc_text += cv64_string_to_bytearray(renon_item_dialogue[shop_desc_list[i][0]]) + rom.write_bytes(0x1AD00, shopsanity_name_text) + rom.write_bytes(0x1A800, shopsanity_desc_text) + + # Panther Dash running + if options.panther_dash: + rom.write_int32(0x69C8C4, 0x0C0FF77E) # JAL 0x803FDDF8 + rom.write_int32(0x6AA228, 0x0C0FF77E) # JAL 0x803FDDF8 + rom.write_int32s(0x69C86C, [0x0C0FF78E, # JAL 0x803FDE38 + 0x3C01803E]) # LUI AT, 0x803E + rom.write_int32s(0x6AA1D0, [0x0C0FF78E, # JAL 0x803FDE38 + 0x3C01803E]) # LUI AT, 0x803E + rom.write_int32(0x69D37C, 0x0C0FF79E) # JAL 0x803FDE78 + rom.write_int32(0x6AACE0, 0x0C0FF79E) # JAL 0x803FDE78 + rom.write_int32s(0xBFDDF8, patches.panther_dash) + # Jump prevention + if options.panther_dash == PantherDash.option_jumpless: + rom.write_int32(0xBFDE2C, 0x080FF7BB) # J 0x803FDEEC + rom.write_int32(0xBFD044, 0x080FF7B1) # J 0x803FDEC4 + rom.write_int32s(0x69B630, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8CCD0000]) # LW T5, 0x0000 (A2) + rom.write_int32s(0x6A8EC0, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8CCC0000]) # LW T4, 0x0000 (A2) + # Fun fact: KCEK put separate code to handle coyote time jumping + rom.write_int32s(0x69910C, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8C4E0000]) # LW T6, 0x0000 (V0) + rom.write_int32s(0x6A6718, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8C4E0000]) # LW T6, 0x0000 (V0) + rom.write_int32s(0xBFDEC4, patches.panther_jump_preventer) + + # Everything related to Big Toss. + if options.big_toss: + rom.write_int32s(0x27E90, [0x0C0FFA38, # JAL 0x803FE8E0 + 0xAFB80074]) # SW T8, 0x0074 (SP) + rom.write_int32(0x26F54, 0x0C0FFA4D) # JAL 0x803FE934 + rom.write_int32s(0xBFE8E0, patches.big_tosser) + + # Write all the new randomized bytes. + for offset, item_id in offset_data.items(): + if item_id <= 0xFF: + rom.write_byte(offset, item_id) + elif item_id <= 0xFFFF: + rom.write_int16(offset, item_id) + elif item_id <= 0xFFFFFF: + rom.write_int24(offset, item_id) + else: + rom.write_int32(offset, item_id) + + # Write the secondary name the client will use to distinguish a vanilla ROM from an AP one. + rom.write_bytes(0xBFBFD0, "ARCHIPELAGO1".encode("utf-8")) + # Write the slot authentication + rom.write_bytes(0xBFBFE0, world.auth) + + # Write the specified window colors + rom.write_byte(0xAEC23, options.window_color_r.value << 4) + rom.write_byte(0xAEC33, options.window_color_g.value << 4) + rom.write_byte(0xAEC47, options.window_color_b.value << 4) + rom.write_byte(0xAEC43, options.window_color_a.value << 4) + + # Write the item/player names for other game items + for loc in active_locations: + if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == player: + continue + if len(loc.item.name) > 67: + item_name = loc.item.name[0x00:0x68] + else: + item_name = loc.item.name + inject_address = 0xBB7164 + (256 * (loc.address & 0xFFF)) + wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + multiworld.get_player_name(loc.item.player), 96) + rom.write_bytes(inject_address, get_item_text_color(loc) + cv64_string_to_bytearray(wrapped_name)) + rom.write_byte(inject_address + 255, num_lines) + + # Everything relating to loading the other game items text + rom.write_int32(0xA8D8C, 0x080FF88F) # J 0x803FE23C + rom.write_int32(0xBEA98, 0x0C0FF8B4) # JAL 0x803FE2D0 + rom.write_int32(0xBEAB0, 0x0C0FF8BD) # JAL 0x803FE2F8 + rom.write_int32(0xBEACC, 0x0C0FF8C5) # JAL 0x803FE314 + rom.write_int32s(0xBFE23C, patches.multiworld_item_name_loader) + rom.write_bytes(0x10F188, [0x00 for _ in range(264)]) + rom.write_bytes(0x10F298, [0x00 for _ in range(264)]) + + # When the game normally JALs to the item prepare textbox function after the player picks up an item, set the + # "no receiving" timer to ensure the item textbox doesn't freak out if you pick something up while there's a queue + # of unreceived items. + rom.write_int32(0xA8D94, 0x0C0FF9F0) # JAL 0x803FE7C0 + rom.write_int32s(0xBFE7C0, [0x3C088039, # LUI T0, 0x8039 + 0x24090020, # ADDIU T1, R0, 0x0020 + 0x0804EDCE, # J 0x8013B738 + 0xA1099BE0]) # SB T1, 0x9BE0 (T0) + + +class CV64DeltaPatch(APDeltaPatch): + hash = CV64US10HASH + patch_file_ending: str = ".apcv64" + result_file_ending: str = ".z64" + + game = "Castlevania 64" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + def patch(self, target: str): + super().patch(target) + rom = LocalRom(target) + + # Extract the item models file, decompress it, append the AP icons, compress it back, re-insert it. + items_file = lzkn64.decompress_buffer(rom.read_bytes(0x9C5310, 0x3D28)) + compressed_file = lzkn64.compress_buffer(items_file[0:0x69B6] + pkgutil.get_data(__name__, "data/ap_icons.bin")) + rom.write_bytes(0xBB2D88, compressed_file) + # Update the items' Nisitenma-Ichigo table entry to point to the new file's start and end addresses in the ROM. + rom.write_int32s(0x95F04, [0x80BB2D88, 0x00BB2D88 + len(compressed_file)]) + # Update the items' decompressed file size tables with the new file's decompressed file size. + rom.write_int16(0x95706, 0x7BF0) + rom.write_int16(0x104CCE, 0x7BF0) + # Update the Wooden Stake and Roses' item appearance settings table to point to the Archipelago item graphics. + rom.write_int16(0xEE5BA, 0x7B38) + rom.write_int16(0xEE5CA, 0x7280) + # Change the items' sizes. The progression one will be larger than the non-progression one. + rom.write_int32(0xEE5BC, 0x3FF00000) + rom.write_int32(0xEE5CC, 0x3FA00000) + rom.write_to_file(target) + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(open(file_name, "rb").read()) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if CV64US10HASH != basemd5.hexdigest(): + raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0." + "Get the correct game and version, then dump it.") + setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes) + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + if not file_name: + file_name = get_settings()["cv64_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/cv64/rules.py b/worlds/cv64/rules.py new file mode 100644 index 000000000000..9642073341e4 --- /dev/null +++ b/worlds/cv64/rules.py @@ -0,0 +1,103 @@ +from typing import Dict, TYPE_CHECKING + +from BaseClasses import CollectionState +from worlds.generic.Rules import allow_self_locking_items, CollectionRule +from .options import DraculasCondition +from .entrances import get_entrance_info +from .data import iname, rname + +if TYPE_CHECKING: + from . import CV64World + + +class CV64Rules: + player: int + world: "CV64World" + rules: Dict[str, CollectionRule] + s1s_per_warp: int + required_s2s: int + drac_condition: int + + def __init__(self, world: "CV64World") -> None: + self.player = world.player + self.world = world + self.s1s_per_warp = world.s1s_per_warp + self.required_s2s = world.required_s2s + self.drac_condition = world.drac_condition + + self.rules = { + iname.left_tower_key: lambda state: state.has(iname.left_tower_key, self.player), + iname.storeroom_key: lambda state: state.has(iname.storeroom_key, self.player), + iname.archives_key: lambda state: state.has(iname.archives_key, self.player), + iname.garden_key: lambda state: state.has(iname.garden_key, self.player), + iname.copper_key: lambda state: state.has(iname.copper_key, self.player), + iname.chamber_key: lambda state: state.has(iname.chamber_key, self.player), + "Bomb 1": lambda state: state.has_all({iname.magical_nitro, iname.mandragora}, self.player), + "Bomb 2": lambda state: state.has(iname.magical_nitro, self.player, 2) + and state.has(iname.mandragora, self.player, 2), + iname.execution_key: lambda state: state.has(iname.execution_key, self.player), + iname.science_key1: lambda state: state.has(iname.science_key1, self.player), + iname.science_key2: lambda state: state.has(iname.science_key2, self.player), + iname.science_key3: lambda state: state.has(iname.science_key3, self.player), + iname.clocktower_key1: lambda state: state.has(iname.clocktower_key1, self.player), + iname.clocktower_key2: lambda state: state.has(iname.clocktower_key2, self.player), + iname.clocktower_key3: lambda state: state.has(iname.clocktower_key3, self.player), + "Dracula": self.can_enter_dracs_chamber + } + + def can_enter_dracs_chamber(self, state: CollectionState) -> bool: + drac_object_name = None + if self.drac_condition == DraculasCondition.option_crystal: + drac_object_name = "Crystal" + elif self.drac_condition == DraculasCondition.option_bosses: + drac_object_name = "Trophy" + elif self.drac_condition == DraculasCondition.option_specials: + drac_object_name = "Special2" + + if drac_object_name is not None: + return state.has(drac_object_name, self.player, self.required_s2s) + return True + + def set_cv64_rules(self) -> None: + multiworld = self.world.multiworld + + for region in multiworld.get_regions(self.player): + # Set each entrance's rule if it should have one. + # Warp entrances have their own special handling. + for entrance in region.entrances: + if entrance.parent_region.name == "Menu": + if entrance.name.startswith("Warp "): + entrance.access_rule = lambda state, warp_num=int(entrance.name[5]): \ + state.has(iname.special_one, self.player, self.s1s_per_warp * warp_num) + else: + ent_rule = get_entrance_info(entrance.name, "rule") + if ent_rule in self.rules: + entrance.access_rule = self.rules[ent_rule] + + multiworld.completion_condition[self.player] = lambda state: state.has(iname.victory, self.player) + if self.world.options.accessibility: # not locations accessibility + self.set_self_locking_items() + + def set_self_locking_items(self) -> None: + multiworld = self.world.multiworld + + # Do the regions that we know for a fact always exist, and we always do no matter what. + allow_self_locking_items(multiworld.get_region(rname.villa_archives, self.player), iname.archives_key) + allow_self_locking_items(multiworld.get_region(rname.cc_torture_chamber, self.player), iname.chamber_key) + + # Add this region if the world doesn't have the Villa Storeroom warp entrance. + if "Villa" not in self.world.active_warp_list[1:]: + allow_self_locking_items(multiworld.get_region(rname.villa_storeroom, self.player), iname.storeroom_key) + + # Add this region if Hard Logic is on and Multi Hit Breakables are off. + if self.world.options.hard_logic and not self.world.options.multi_hit_breakables: + allow_self_locking_items(multiworld.get_region(rname.cw_ltower, self.player), iname.left_tower_key) + + # Add these regions if Tower of Science is in the world. + if "Tower of Science" in self.world.active_stage_exits: + allow_self_locking_items(multiworld.get_region(rname.tosci_three_doors, self.player), iname.science_key1) + allow_self_locking_items(multiworld.get_region(rname.tosci_key3, self.player), iname.science_key3) + + # Add this region if Tower of Execution is in the world and Hard Logic is not on. + if "Tower of Execution" in self.world.active_stage_exits and self.world.options.hard_logic: + allow_self_locking_items(multiworld.get_region(rname.toe_ledge, self.player), iname.execution_key) diff --git a/worlds/cv64/src/drop_sub_weapon.c b/worlds/cv64/src/drop_sub_weapon.c new file mode 100644 index 000000000000..d1a4b52269c6 --- /dev/null +++ b/worlds/cv64/src/drop_sub_weapon.c @@ -0,0 +1,69 @@ +// Written by Moisés +#include "include/game/module.h" +#include "include/game/math.h" +#include "cv64.h" + +extern vec3f player_pos; +extern vec3s player_angle; // player_angle.y = Player's facing angle (yaw) +extern f32 player_height_with_respect_of_floor; // Stored negative in-game + +#define SHT_MAX 32767.0f +#define SHT_MINV (1.0f / SHT_MAX) + +void spawn_item_behind_player(s32 item) { + interactuablesModule* pickable_item = NULL; + const f32 spawnDistance = 8.0f; + vec3f player_backwards_dir; + + pickable_item = (interactuablesModule*)module_createAndSetChild(moduleList_findFirstModuleByID(ACTOR_CREATOR), ACTOR_ITEM); + if (pickable_item != NULL) { + // Convert facing angle to a vec3f + // SHT_MINV needs to be negative here for the item to be spawned properly on the character's back + player_backwards_dir.x = coss(-player_angle.y) * -SHT_MINV; + player_backwards_dir.z = sins(-player_angle.y) * -SHT_MINV; + // Multiply facing vector with distance away from the player + vec3f_multiplyScalar(&player_backwards_dir, &player_backwards_dir, spawnDistance); + // Assign the position of the item relative to the player's current position. + vec3f_add(&pickable_item->position, &player_pos, &player_backwards_dir); + // The Y position of the item will be the same as the floor right under the player + // The player's height with respect of the flower under them is already stored negative in-game, + // so no need to substract + pickable_item->position.y = player_pos.y + 5.0f; + pickable_item->height = pickable_item->position.y; + + // Assign item ID + pickable_item->item_ID = item; + } +} + + +const f32 droppingAccel = 0.05f; +const f32 maxDroppingSpeed = 1.5f; +f32 droppingSpeed = 0.0f; +f32 droppingTargetYPos = 0.0f; +u8 dropItemCalcFuncCalled = FALSE; + +s32 drop_item_calc(interactuablesModule* pickable_item) { + if (dropItemCalcFuncCalled == FALSE) { + droppingTargetYPos = player_pos.y + player_height_with_respect_of_floor + 1.0f; + if (pickable_item->item_ID == CROSS || pickable_item->item_ID == AXE || + pickable_item->item_ID == CROSS__VANISH || pickable_item->item_ID == AXE__VANISH) { + droppingTargetYPos += 3.0f; + } + dropItemCalcFuncCalled = TRUE; + return TRUE; + } + if (pickable_item->position.y <= droppingTargetYPos) { + droppingSpeed = 0.0f; + dropItemCalcFuncCalled = FALSE; + return FALSE; + } + else { + if (droppingSpeed < maxDroppingSpeed) { + droppingSpeed += droppingAccel; + } + pickable_item->position.y -= droppingSpeed; + pickable_item->height = pickable_item->position.y; + return TRUE; + } +} \ No newline at end of file diff --git a/worlds/cv64/src/print.c b/worlds/cv64/src/print.c new file mode 100644 index 000000000000..7f77afb00f0f --- /dev/null +++ b/worlds/cv64/src/print.c @@ -0,0 +1,116 @@ +// Written by Moisés. +// NOTE: This is an earlier version to-be-replaced. +#include +#include + +// Helper function +// https://decomp.me/scratch/9H1Uy +u32 convertUTF8StringToUTF16(char* src, u16* buffer) { + u32 string_length = 0; + + // If the source string starts with a null char (0), we assume the string empty. + if (*src != 0) { + // Copy the char from the source string into the bufferination. + // Then advance to the next char until we find the null char (0). + do { + *buffer = *src; + src++; + buffer++; + string_length++; + } while (*src != 0); + } + // Make sure to add the null char at the end of the bufferination string, + // and then return the length of the string. + *buffer = 0; + return string_length; +} + +// Begin printing ASCII text stored in a char* +textbox* print_text(const char* message, const s16 X_pos, const s16 Y_pos, const u8 number_of_lines, const s16 textbox_width, const u32 txtbox_flags, const void* module) { + textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create; + void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos; + void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions; + void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr; + u16* (*ptr_convertUTF16ToCustomTextFormat)(u16*) = convertUTF16ToCustomTextFormat; + void* (*ptr_malloc)(s32, u32) = malloc; + + textbox* txtbox = NULL; + + // Allocate memory for the text buffer + u16* text_buffer = (u16*) ptr_malloc(0, 100); + + // Create the textbox data structure + if (module != NULL) { + txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags); + } + + if (txtbox != NULL && text_buffer != NULL && message != NULL) { + // Set text position and dimensions + ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1); + ptr_textbox_setDimensions(txtbox, number_of_lines, textbox_width, 0, 0); + + // Convert the ASCII message to the CV64 custom format + convertUTF8StringToUTF16(message, text_buffer); + ptr_convertUTF16ToCustomTextFormat(text_buffer); + + // Set the text buffer pointer to the textbox data structure + ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0); + } + // We return the textbox so that we can modify its properties once it begins printing + // (say to show, hide the text) + return txtbox; +} + +// Begin printing signed integer +textbox* print_number(const s32 number, u16* text_buffer, const s16 X_pos, const s16 Y_pos, const u8 number_of_digits, const u32 txtbox_flags, const u32 additional_text_flag, const void* module) { + textbox* (*ptr_textbox_create)(void*, void*, u32) = textbox_create; + void (*ptr_textbox_setPos)(textbox*, u16, u16, s32) = textbox_setPos; + void (*ptr_textbox_setDimensions)(textbox*, s8, s16, u8, u8) = textbox_setDimensions; + void (*ptr_textbox_setMessagePtr)(textbox*, u16*, s32, s16) = textbox_setMessagePtr; + void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText; + + textbox* txtbox = NULL; + + // Create the textbox data structure + if (module != NULL) { + txtbox = ptr_textbox_create(module, HUD_camera, txtbox_flags); + } + + if (txtbox != NULL && text_buffer != NULL) { + // Set text position and dimensions + ptr_textbox_setPos(txtbox, X_pos, Y_pos, 1); + ptr_textbox_setDimensions(txtbox, 1, 100, 0, 0); + + // Convert the number to the CV64 custom format + ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag); + + // Set the text buffer pointer to the textbox data structure + ptr_textbox_setMessagePtr(txtbox, text_buffer, 0, 0); + } + // We return the textbox so that we can modify its properties once it begins printing + // (say to show, hide the text) + return txtbox; +} + +// Update the value of a number that began printing after calling "print_number()" +void update_printed_number(textbox* txtbox, const s32 number, u16* text_buffer, const u8 number_of_digits, const u32 additional_text_flag) { + void (*ptr_text_convertIntNumberToText)(u32, u16*, u8, u32) = text_convertIntNumberToText; + + if (text_buffer != NULL) { + ptr_text_convertIntNumberToText(number, text_buffer, number_of_digits, additional_text_flag); + txtbox->flags |= 0x1000000; // Needed to make sure the number updates properly + } +} + +void display_text(textbox* txtbox, const u8 display_textbox) { + if (txtbox != NULL) { + if (display_textbox == TRUE) { + // Show text + txtbox->flags &= ~HIDE_TEXTBOX; + } + else { + // Hide text + txtbox->flags |= HIDE_TEXTBOX; + } + } +} diff --git a/worlds/cv64/src/print_text_ovl.c b/worlds/cv64/src/print_text_ovl.c new file mode 100644 index 000000000000..7ca8e6f35e2d --- /dev/null +++ b/worlds/cv64/src/print_text_ovl.c @@ -0,0 +1,26 @@ +// Written by Moisés +#include "print.h" +#include +#include + +#define counter_X_pos 30 +#define counter_Y_pos 40 +#define counter_number_of_digits 2 +#define GOLD_JEWEL_FONT 0x14 + +extern u8 bytes[13]; + +u16* number_text_buffer = NULL; +textbox* txtbox = NULL; + +void begin_print() { + // Allocate memory for the number text + number_text_buffer = (u16*) malloc(0, 12); + + // Assuming that 0x80342814 = HUD Module + txtbox = print_number(0, number_text_buffer, counter_X_pos, counter_Y_pos, counter_number_of_digits, 0x08600000, GOLD_JEWEL_FONT, (void*) 0x80342814); +} + +void update_print(u8 i) { + update_printed_number(txtbox, (s32) bytes[i], number_text_buffer, counter_number_of_digits, GOLD_JEWEL_FONT); +} diff --git a/worlds/cv64/stages.py b/worlds/cv64/stages.py new file mode 100644 index 000000000000..a6fa6679214c --- /dev/null +++ b/worlds/cv64/stages.py @@ -0,0 +1,490 @@ +import logging + +from .data import rname +from .regions import get_region_info +from .locations import get_location_info +from .options import WarpOrder + +from typing import TYPE_CHECKING, Dict, List, Tuple, Union + +if TYPE_CHECKING: + from . import CV64World + + +# # # KEY # # # +# "start region" = The Region that the start of the stage is in. Used for connecting the previous stage's end and +# alternate end (if it exists) Entrances to the start of the next one. +# "start map id" = The map ID that the start of the stage is in. +# "start spawn id" = The player spawn location ID for the start of the stage. This and "start map id" are both written +# to the previous stage's end loading zone to make it send the player to the next stage in the +# world's determined stage order. +# "mid region" = The Region that the stage's middle warp point is in. Used for connecting the warp Entrances after the +# starting stage to where they should be connecting to. +# "mid map id" = The map ID that the stage's middle warp point is in. +# "mid spawn id" = The player spawn location ID for the stage's middle warp point. This and "mid map id" are both +# written to the warp menu code to make it send the player to where it should be sending them. +# "end region" = The Region that the end of the stage is in. Used for connecting the next stage's beginning Entrance +# (if it exists) to the end of the previous one. +# "end map id" = The map ID that the end of the stage is in. +# "end spawn id" = The player spawn location ID for the end of the stage. This and "end map id" are both written to the +# next stage's beginning loading zone (if it exists) to make it send the player to the previous stage +# in the world's determined stage order. +# startzone map offset = The offset in the ROM to overwrite to change where the start of the stage leads. +# startzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the previous map the +# start of the stage puts the player at. +# endzone map offset = The offset in the ROM to overwrite to change where the end of the stage leads. +# endzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the end of +# the stage puts the player at. +# altzone map offset = The offset in the ROM to overwrite to change where the alternate end of the stage leads +# (if it exists). +# altzone spawn offset = The offset in the ROM to overwrite to change what spawn location in the next map the alternate +# end of the stage puts the player at. +# character = What character that stage is exclusively meant for normally. Used in determining what stages to leave out +# depending on what character stage setting was chosen in the player options. +# save number offsets = The offsets to overwrite to change what stage number is displayed on the save file when saving +# at the stage's White Jewels. +# regions = All Regions that make up the stage. If the stage is in the world's active stages, its Regions and their +# corresponding Locations and Entrances will all be created. +stage_info = { + "Forest of Silence": { + "start region": rname.forest_start, "start map id": 0x00, "start spawn id": 0x00, + "mid region": rname.forest_mid, "mid map id": 0x00, "mid spawn id": 0x04, + "end region": rname.forest_end, "end map id": 0x00, "end spawn id": 0x01, + "endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B, + "save number offsets": [0x1049C5, 0x1049CD, 0x1049D5], + "regions": [rname.forest_start, + rname.forest_mid, + rname.forest_end] + }, + + "Castle Wall": { + "start region": rname.cw_start, "start map id": 0x02, "start spawn id": 0x00, + "mid region": rname.cw_start, "mid map id": 0x02, "mid spawn id": 0x07, + "end region": rname.cw_exit, "end map id": 0x02, "end spawn id": 0x10, + "endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61, + "save number offsets": [0x1049DD, 0x1049E5, 0x1049ED], + "regions": [rname.cw_start, + rname.cw_exit, + rname.cw_ltower] + }, + + "Villa": { + "start region": rname.villa_start, "start map id": 0x03, "start spawn id": 0x00, + "mid region": rname.villa_storeroom, "mid map id": 0x05, "mid spawn id": 0x04, + "end region": rname.villa_crypt, "end map id": 0x1A, "end spawn id": 0x03, + "endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81, + "altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81, + "save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D], + "regions": [rname.villa_start, + rname.villa_main, + rname.villa_storeroom, + rname.villa_archives, + rname.villa_maze, + rname.villa_servants, + rname.villa_crypt] + }, + + "Tunnel": { + "start region": rname.tunnel_start, "start map id": 0x07, "start spawn id": 0x00, + "mid region": rname.tunnel_end, "mid map id": 0x07, "mid spawn id": 0x03, + "end region": rname.tunnel_end, "end map id": 0x07, "end spawn id": 0x11, + "endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt", + "save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D], + "regions": [rname.tunnel_start, + rname.tunnel_end] + }, + + "Underground Waterway": { + "start region": rname.uw_main, "start map id": 0x08, "start spawn id": 0x00, + "mid region": rname.uw_main, "mid map id": 0x08, "mid spawn id": 0x03, + "end region": rname.uw_end, "end map id": 0x08, "end spawn id": 0x01, + "endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie", + "save number offsets": [0x104A35, 0x104A3D], + "regions": [rname.uw_main, + rname.uw_end] + }, + + "Castle Center": { + "start region": rname.cc_main, "start map id": 0x19, "start spawn id": 0x00, + "mid region": rname.cc_main, "mid map id": 0x0E, "mid spawn id": 0x03, + "end region": rname.cc_elev_top, "end map id": 0x0F, "end spawn id": 0x02, + "endzone map offset": 0x109CB7, "endzone spawn offset": 0x109CB9, + "altzone map offset": 0x109CCF, "altzone spawn offset": 0x109CD1, + "save number offsets": [0x104A45, 0x104A4D, 0x104A55, 0x104A5D, 0x104A65, 0x104A6D, 0x104A75], + "regions": [rname.cc_main, + rname.cc_torture_chamber, + rname.cc_library, + rname.cc_crystal, + rname.cc_elev_top] + }, + + "Duel Tower": { + "start region": rname.dt_main, "start map id": 0x13, "start spawn id": 0x00, + "startzone map offset": 0x109DA7, "startzone spawn offset": 0x109DA9, + "mid region": rname.dt_main, "mid map id": 0x13, "mid spawn id": 0x15, + "end region": rname.dt_main, "end map id": 0x13, "end spawn id": 0x01, + "endzone map offset": 0x109D8F, "endzone spawn offset": 0x109D91, "character": "Reinhardt", + "save number offsets": [0x104ACD], + "regions": [rname.dt_main] + }, + + "Tower of Execution": { + "start region": rname.toe_main, "start map id": 0x10, "start spawn id": 0x00, + "startzone map offset": 0x109D17, "startzone spawn offset": 0x109D19, + "mid region": rname.toe_main, "mid map id": 0x10, "mid spawn id": 0x02, + "end region": rname.toe_main, "end map id": 0x10, "end spawn id": 0x12, + "endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt", + "save number offsets": [0x104A7D, 0x104A85], + "regions": [rname.toe_main, + rname.toe_ledge] + }, + + "Tower of Science": { + "start region": rname.tosci_start, "start map id": 0x12, "start spawn id": 0x00, + "startzone map offset": 0x109D77, "startzone spawn offset": 0x109D79, + "mid region": rname.tosci_conveyors, "mid map id": 0x12, "mid spawn id": 0x03, + "end region": rname.tosci_conveyors, "end map id": 0x12, "end spawn id": 0x04, + "endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie", + "save number offsets": [0x104A95, 0x104A9D, 0x104AA5], + "regions": [rname.tosci_start, + rname.tosci_three_doors, + rname.tosci_conveyors, + rname.tosci_key3] + }, + + "Tower of Sorcery": { + "start region": rname.tosor_main, "start map id": 0x11, "start spawn id": 0x00, + "startzone map offset": 0x109D47, "startzone spawn offset": 0x109D49, + "mid region": rname.tosor_main, "mid map id": 0x11, "mid spawn id": 0x01, + "end region": rname.tosor_main, "end map id": 0x11, "end spawn id": 0x13, + "endzone map offset": 0x109D2F, "endzone spawn offset": 0x109D31, "character": "Carrie", + "save number offsets": [0x104A8D], + "regions": [rname.tosor_main] + }, + + "Room of Clocks": { + "start region": rname.roc_main, "start map id": 0x1B, "start spawn id": 0x00, + "mid region": rname.roc_main, "mid map id": 0x1B, "mid spawn id": 0x02, + "end region": rname.roc_main, "end map id": 0x1B, "end spawn id": 0x14, + "endzone map offset": 0x109EAF, "endzone spawn offset": 0x109EB1, + "save number offsets": [0x104AC5], + "regions": [rname.roc_main] + }, + + "Clock Tower": { + "start region": rname.ct_start, "start map id": 0x17, "start spawn id": 0x00, + "mid region": rname.ct_middle, "mid map id": 0x17, "mid spawn id": 0x02, + "end region": rname.ct_end, "end map id": 0x17, "end spawn id": 0x03, + "endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39, + "save number offsets": [0x104AB5, 0x104ABD], + "regions": [rname.ct_start, + rname.ct_middle, + rname.ct_end] + }, + + "Castle Keep": { + "start region": rname.ck_main, "start map id": 0x14, "start spawn id": 0x02, + "mid region": rname.ck_main, "mid map id": 0x14, "mid spawn id": 0x03, + "end region": rname.ck_drac_chamber, + "save number offsets": [0x104AAD], + "regions": [rname.ck_main] + }, +} + +vanilla_stage_order = ("Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center", + "Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks", + "Clock Tower", "Castle Keep") + +# # # KEY # # # +# "prev" = The previous stage in the line. +# "next" = The next stage in the line. +# "alt" = The alternate next stage in the line (if one exists). +# "position" = The stage's number in the order of stages. +# "path" = Character indicating whether the stage is on the main path or an alternate path, similar to Rondo of Blood. +# Used in writing the randomized stage order to the spoiler. +vanilla_stage_exits = {rname.forest_of_silence: {"prev": None, "next": rname.castle_wall, + "alt": None, "position": 1, "path": " "}, + rname.castle_wall: {"prev": None, "next": rname.villa, + "alt": None, "position": 2, "path": " "}, + rname.villa: {"prev": None, "next": rname.tunnel, + "alt": rname.underground_waterway, "position": 3, "path": " "}, + rname.tunnel: {"prev": None, "next": rname.castle_center, + "alt": None, "position": 4, "path": " "}, + rname.underground_waterway: {"prev": None, "next": rname.castle_center, + "alt": None, "position": 4, "path": "'"}, + rname.castle_center: {"prev": None, "next": rname.duel_tower, + "alt": rname.tower_of_science, "position": 5, "path": " "}, + rname.duel_tower: {"prev": rname.castle_center, "next": rname.tower_of_execution, + "alt": None, "position": 6, "path": " "}, + rname.tower_of_execution: {"prev": rname.duel_tower, "next": rname.room_of_clocks, + "alt": None, "position": 7, "path": " "}, + rname.tower_of_science: {"prev": rname.castle_center, "next": rname.tower_of_sorcery, + "alt": None, "position": 6, "path": "'"}, + rname.tower_of_sorcery: {"prev": rname.tower_of_science, "next": rname.room_of_clocks, + "alt": None, "position": 7, "path": "'"}, + rname.room_of_clocks: {"prev": None, "next": rname.clock_tower, + "alt": None, "position": 8, "path": " "}, + rname.clock_tower: {"prev": None, "next": rname.castle_keep, + "alt": None, "position": 9, "path": " "}, + rname.castle_keep: {"prev": None, "next": None, + "alt": None, "position": 10, "path": " "}} + + +def get_stage_info(stage: str, info: str) -> Union[str, int, Union[List[int], List[str]], None]: + return stage_info[stage].get(info, None) + + +def get_locations_from_stage(stage: str) -> List[str]: + overall_locations = [] + for region in get_stage_info(stage, "regions"): + stage_locations = get_region_info(region, "locations") + if stage_locations is not None: + overall_locations += stage_locations + + final_locations = [] + for loc in overall_locations: + if get_location_info(loc, "code") is not None: + final_locations.append(loc) + return final_locations + + +def verify_character_stage(world: "CV64World", stage: str) -> bool: + # Verify a character stage is in the world if the given stage is a character stage. + stage_char = get_stage_info(stage, "character") + return stage_char is None or (world.reinhardt_stages and stage_char == "Reinhardt") or \ + (world.carrie_stages and stage_char == "Carrie") + + +def get_normal_stage_exits(world: "CV64World") -> Dict[str, dict]: + exits = {name: vanilla_stage_exits[name].copy() for name in vanilla_stage_exits} + non_branching_pos = 1 + + for stage in stage_info: + # Remove character stages that are not enabled. + if not verify_character_stage(world, stage): + del exits[stage] + continue + + # If branching pathways are not enabled, update the exit info to converge said stages on a single path. + if world.branching_stages: + continue + if world.carrie_stages and not world.reinhardt_stages and exits[stage]["alt"] is not None: + exits[stage]["next"] = exits[stage]["alt"] + elif world.carrie_stages and world.reinhardt_stages and stage != rname.castle_keep: + exits[stage]["next"] = vanilla_stage_order[vanilla_stage_order.index(stage) + 1] + exits[stage]["alt"] = None + exits[stage]["position"] = non_branching_pos + exits[stage]["path"] = " " + non_branching_pos += 1 + + return exits + + +def shuffle_stages(world: "CV64World", stage_1_blacklist: List[str]) \ + -> Tuple[Dict[str, Dict[str, Union[str, int, None]]], str, List[str]]: + """Woah, this is a lot! I should probably summarize what's happening in here, huh? + + So, in the vanilla game, all the stages are basically laid out on a linear "timeline" with some stages being + different depending on who you are playing as. The different character stages, in question, are the one following + Villa and the two following Castle Center. The ends of these two stages are considered the route divergences and, in + this rando, the game's behavior has been changed in such that both characters can access each other's exclusive + stages (thereby making the entire game playable in just one character run). With this in mind, when shuffling the + stages around, there is one particularly big rule that must be kept in mind to ensure things don't get too wacky. + That being: + + Villa and Castle Center cannot appear in branching path stage slots; they can only be on "main" path slots. + + So for this reason, generating a new stage layout is not as simple as just scrambling a list of stages around. It + must be done in such a way that whatever stages directly follow Villa or CC is not the other stage. The exception is + if branching stages are not a thing at all due to the player settings, in which case everything I said above does + not matter. Consider the following representation of a stage "timeline", wherein each "-" represents a main stage + and a "=" represents a pair of branching stages: + + -==---=--- + + In the above example, CC is the first "-" and Villa is the fourth. CC and Villa can only be "-"s whereas every other + stage can be literally anywhere, including on one of the "=" dashes. Villa will always be followed by one pair of + branching stages and CC will be followed by two pairs. + + This code starts by first generating a singular list of stages that fit the criteria of Castle Center not being in + the next two entries following Villa and Villa not being in the next four entries after Castle Center. Once that has + been figured out, it will then generate a dictionary of stages with the appropriate information regarding what + stages come before and after them to then be used for Entrance creation as well as what position in the list they + are in for the purposes of the spoiler log and extended hint information. + + I opted to use the Rondo of Blood "'" stage notation to represent Carrie stage slots specifically. If a main stage + with a backwards connection connects backwards into a pair of branching stages, it will be the non-"'" stage + (Reinhardt's) that it connects to. The Carrie stage slot cannot be accessed this way. + + If anyone has any ideas or suggestions on how to improve this, I'd love to hear them! Because it's only going to get + uglier come Legacy of Darkness and Cornell's funny side route later on. + """ + + starting_stage_value = world.options.starting_stage.value + + # Verify the starting stage is valid. If it isn't, pick a stage at random. + if vanilla_stage_order[starting_stage_value] not in stage_1_blacklist and \ + verify_character_stage(world, vanilla_stage_order[starting_stage_value]): + starting_stage = vanilla_stage_order[starting_stage_value] + else: + logging.warning(f"[{world.multiworld.player_name[world.player]}] {vanilla_stage_order[starting_stage_value]} " + f"cannot be the starting stage with the chosen settings. Picking a different stage instead...") + possible_stages = [] + for stage in vanilla_stage_order: + if stage in world.active_stage_exits and stage != rname.castle_keep: + possible_stages.append(stage) + starting_stage = world.random.choice(possible_stages) + world.options.starting_stage.value = vanilla_stage_order.index(starting_stage) + + remaining_stage_pool = [stage for stage in world.active_stage_exits] + remaining_stage_pool.remove(rname.castle_keep) + + total_stages = len(remaining_stage_pool) + + new_stage_order = [] + villa_cc_ids = [2, 3] + alt_villa_stage = [] + alt_cc_stages = [] + + # If there are branching stages, remove Villa and CC from the list and determine their placements first. + if world.branching_stages: + villa_cc_ids = world.random.sample(range(1, 5), 2) + remaining_stage_pool.remove(rname.villa) + remaining_stage_pool.remove(rname.castle_center) + + # Remove the starting stage from the remaining pool if it's in there at this point. + if starting_stage in remaining_stage_pool: + remaining_stage_pool.remove(starting_stage) + + # If Villa or CC is our starting stage, force its respective ID to be 0 and re-randomize the other. + if starting_stage == rname.villa: + villa_cc_ids[0] = 0 + villa_cc_ids[1] = world.random.randint(1, 5) + elif starting_stage == rname.castle_center: + villa_cc_ids[1] = 0 + villa_cc_ids[0] = world.random.randint(1, 5) + + for i in range(total_stages): + # If we're on Villa or CC's ID while in branching stage mode, put the respective stage in the slot. + if world.branching_stages and i == villa_cc_ids[0] and rname.villa not in new_stage_order: + new_stage_order.append(rname.villa) + villa_cc_ids[1] += 2 + elif world.branching_stages and i == villa_cc_ids[1] and rname.castle_center not in new_stage_order: + new_stage_order.append(rname.castle_center) + villa_cc_ids[0] += 4 + else: + # If neither of the above are true, if we're looking at Stage 1, append the starting stage. + # Otherwise, draw a random stage from the active list and delete it from there. + if i == 0: + new_stage_order.append(starting_stage) + else: + new_stage_order.append(world.random.choice(remaining_stage_pool)) + remaining_stage_pool.remove(new_stage_order[i]) + + # If we're looking at an alternate stage slot, put the stage in one of these lists to indicate it as such + if not world.branching_stages: + continue + if i - 2 >= 0: + if new_stage_order[i - 2] == rname.villa: + alt_villa_stage.append(new_stage_order[i]) + if i - 3 >= 0: + if new_stage_order[i - 3] == rname.castle_center: + alt_cc_stages.append(new_stage_order[i]) + if i - 4 >= 0: + if new_stage_order[i - 4] == rname.castle_center: + alt_cc_stages.append(new_stage_order[i]) + + new_stage_order.append(rname.castle_keep) + + # Update the dictionary of stage exits + current_stage_number = 1 + for i in range(len(new_stage_order)): + # Stage position number and alternate path indicator + world.active_stage_exits[new_stage_order[i]]["position"] = current_stage_number + if new_stage_order[i] in alt_villa_stage + alt_cc_stages: + world.active_stage_exits[new_stage_order[i]]["path"] = "'" + else: + world.active_stage_exits[new_stage_order[i]]["path"] = " " + + # Previous stage + if world.active_stage_exits[new_stage_order[i]]["prev"]: + if i - 1 < 0: + world.active_stage_exits[new_stage_order[i]]["prev"] = "Menu" + elif world.branching_stages: + if new_stage_order[i - 1] == alt_villa_stage[0] or new_stage_order[i] == alt_villa_stage[0]: + world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 2] + elif new_stage_order[i - 1] == alt_cc_stages[1] or new_stage_order[i] == alt_cc_stages[0]: + world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 3] + else: + world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1] + else: + world.active_stage_exits[new_stage_order[i]]["prev"] = new_stage_order[i - 1] + + # Next stage + if world.active_stage_exits[new_stage_order[i]]["next"]: + if world.branching_stages: + if new_stage_order[i + 1] == alt_villa_stage[0]: + world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 2] + current_stage_number -= 1 + elif new_stage_order[i + 1] == alt_cc_stages[0]: + world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 3] + current_stage_number -= 2 + else: + world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1] + else: + world.active_stage_exits[new_stage_order[i]]["next"] = new_stage_order[i + 1] + + # Alternate next stage + if world.active_stage_exits[new_stage_order[i]]["alt"]: + if world.branching_stages: + if new_stage_order[i] == rname.villa: + world.active_stage_exits[new_stage_order[i]]["alt"] = alt_villa_stage[0] + else: + world.active_stage_exits[new_stage_order[i]]["alt"] = alt_cc_stages[0] + else: + world.active_stage_exits[new_stage_order[i]]["alt"] = None + + current_stage_number += 1 + + return world.active_stage_exits, starting_stage, new_stage_order + + +def generate_warps(world: "CV64World") -> List[str]: + # Create a list of warps from the active stage list. They are in a random order by default and will never + # include the starting stage. + possible_warps = [stage for stage in world.active_stage_list] + + # Remove the starting stage from the possible warps. + del (possible_warps[0]) + + active_warp_list = world.random.sample(possible_warps, 7) + + if world.options.warp_order == WarpOrder.option_seed_stage_order: + # Arrange the warps to be in the seed's stage order + new_list = world.active_stage_list.copy() + for warp in world.active_stage_list: + if warp not in active_warp_list: + new_list.remove(warp) + active_warp_list = new_list + elif world.options.warp_order == WarpOrder.option_vanilla_stage_order: + # Arrange the warps to be in the vanilla game's stage order + new_list = list(vanilla_stage_order) + for warp in vanilla_stage_order: + if warp not in active_warp_list: + new_list.remove(warp) + active_warp_list = new_list + + # Insert the starting stage at the start of the warp list + active_warp_list.insert(0, world.active_stage_list[0]) + + return active_warp_list + + +def get_region_names(active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> List[str]: + region_names = [] + for stage in active_stage_exits: + stage_regions = get_stage_info(stage, "regions") + for region in stage_regions: + region_names.append(region) + + return region_names diff --git a/worlds/cv64/test/__init__.py b/worlds/cv64/test/__init__.py new file mode 100644 index 000000000000..2d09e27cb316 --- /dev/null +++ b/worlds/cv64/test/__init__.py @@ -0,0 +1,6 @@ +from test.bases import WorldTestBase + + +class CV64TestBase(WorldTestBase): + game = "Castlevania 64" + player: int = 1 diff --git a/worlds/cv64/test/test_access.py b/worlds/cv64/test/test_access.py new file mode 100644 index 000000000000..79b1e14e11ea --- /dev/null +++ b/worlds/cv64/test/test_access.py @@ -0,0 +1,250 @@ +from . import CV64TestBase + + +class WarpTest(CV64TestBase): + options = { + "special1s_per_warp": 3, + "total_special1s": 21 + } + + def test_warps(self) -> None: + for i in range(1, 8): + self.assertFalse(self.can_reach_entrance(f"Warp {i}")) + self.collect([self.get_item_by_name("Special1")] * 2) + self.assertFalse(self.can_reach_entrance(f"Warp {i}")) + self.collect([self.get_item_by_name("Special1")] * 1) + self.assertTrue(self.can_reach_entrance(f"Warp {i}")) + + +class CastleWallTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 1 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance(f"Left Tower door")) + self.collect([self.get_item_by_name("Left Tower Key")] * 1) + self.assertTrue(self.can_reach_entrance(f"Left Tower door")) + + +class VillaTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 2 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance("To Storeroom door")) + self.collect([self.get_item_by_name("Storeroom Key")] * 1) + self.assertTrue(self.can_reach_entrance("To Storeroom door")) + self.assertFalse(self.can_reach_entrance("To Archives door")) + self.collect([self.get_item_by_name("Archives Key")] * 1) + self.assertTrue(self.can_reach_entrance("To Archives door")) + self.assertFalse(self.can_reach_entrance("To maze gate")) + self.assertFalse(self.can_reach_entrance("Copper door")) + self.collect([self.get_item_by_name("Garden Key")] * 1) + self.assertTrue(self.can_reach_entrance("To maze gate")) + self.assertFalse(self.can_reach_entrance("Copper door")) + self.collect([self.get_item_by_name("Copper Key")] * 1) + self.assertTrue(self.can_reach_entrance("Copper door")) + + +class CastleCenterTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 5 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance("Torture Chamber door")) + self.collect([self.get_item_by_name("Chamber Key")] * 1) + self.assertTrue(self.can_reach_entrance("Torture Chamber door")) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.assertFalse(self.can_reach_entrance("Upper cracked wall")) + self.collect([self.get_item_by_name("Magical Nitro")] * 1) + self.assertFalse(self.can_reach_entrance("Upper cracked wall")) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.collect([self.get_item_by_name("Mandragora")] * 1) + self.assertTrue(self.can_reach_entrance("Upper cracked wall")) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.collect([self.get_item_by_name("Magical Nitro")] * 1) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.collect([self.get_item_by_name("Mandragora")] * 1) + self.assertTrue(self.can_reach_entrance("Upper cracked wall")) + + +class ExecutionTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 7 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance("Execution gate")) + self.collect([self.get_item_by_name("Execution Key")] * 1) + self.assertTrue(self.can_reach_entrance("Execution gate")) + + +class ScienceTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 8 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance("Science Door 1")) + self.collect([self.get_item_by_name("Science Key1")] * 1) + self.assertTrue(self.can_reach_entrance("Science Door 1")) + self.assertFalse(self.can_reach_entrance("To Science Door 2")) + self.assertFalse(self.can_reach_entrance("Science Door 3")) + self.collect([self.get_item_by_name("Science Key2")] * 1) + self.assertTrue(self.can_reach_entrance("To Science Door 2")) + self.assertFalse(self.can_reach_entrance("Science Door 3")) + self.collect([self.get_item_by_name("Science Key3")] * 1) + self.assertTrue(self.can_reach_entrance("Science Door 3")) + + +class ClocktowerTest(CV64TestBase): + options = { + "stage_shuffle": True, + "starting_stage": 11 + } + + def test_doors(self) -> None: + self.assertFalse(self.can_reach_entrance("To Clocktower Door 1")) + self.assertFalse(self.can_reach_entrance("To Clocktower Door 2")) + self.assertFalse(self.can_reach_entrance("Clocktower Door 3")) + self.collect([self.get_item_by_name("Clocktower Key1")] * 1) + self.assertTrue(self.can_reach_entrance("To Clocktower Door 1")) + self.assertFalse(self.can_reach_entrance("To Clocktower Door 2")) + self.assertFalse(self.can_reach_entrance("Clocktower Door 3")) + self.collect([self.get_item_by_name("Clocktower Key2")] * 1) + self.assertTrue(self.can_reach_entrance("To Clocktower Door 2")) + self.assertFalse(self.can_reach_entrance("Clocktower Door 3")) + self.collect([self.get_item_by_name("Clocktower Key3")] * 1) + self.assertTrue(self.can_reach_entrance("Clocktower Door 3")) + + +class DraculaNoneTest(CV64TestBase): + options = { + "draculas_condition": 0, + "stage_shuffle": True, + "starting_stage": 5, + } + + def test_dracula_none_condition(self) -> None: + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Left Tower Key"), + self.get_item_by_name("Garden Key"), + self.get_item_by_name("Copper Key"), + self.get_item_by_name("Science Key1"), + self.get_item_by_name("Science Key2"), + self.get_item_by_name("Science Key3"), + self.get_item_by_name("Clocktower Key1"), + self.get_item_by_name("Clocktower Key2"), + self.get_item_by_name("Clocktower Key3")] * 1) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Special1")] * 7) + self.assertTrue(self.can_reach_entrance("Dracula's door")) + + +class DraculaSpecialTest(CV64TestBase): + options = { + "draculas_condition": 3 + } + + def test_dracula_special_condition(self) -> None: + self.assertFalse(self.can_reach_entrance("Clocktower Door 3")) + self.collect([self.get_item_by_name("Left Tower Key"), + self.get_item_by_name("Garden Key"), + self.get_item_by_name("Copper Key"), + self.get_item_by_name("Magical Nitro"), + self.get_item_by_name("Mandragora"), + self.get_item_by_name("Clocktower Key1"), + self.get_item_by_name("Clocktower Key2"), + self.get_item_by_name("Clocktower Key3")] * 2) + self.assertTrue(self.can_reach_entrance("Clocktower Door 3")) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Special2")] * 19) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Special2")] * 1) + self.assertTrue(self.can_reach_entrance("Dracula's door")) + + +class DraculaCrystalTest(CV64TestBase): + options = { + "draculas_condition": 1, + "stage_shuffle": True, + "starting_stage": 5, + "hard_logic": True + } + + def test_dracula_crystal_condition(self) -> None: + self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower")) + self.collect([self.get_item_by_name("Left Tower Key"), + self.get_item_by_name("Garden Key"), + self.get_item_by_name("Copper Key"), + self.get_item_by_name("Science Key1"), + self.get_item_by_name("Science Key2"), + self.get_item_by_name("Science Key3"), + self.get_item_by_name("Clocktower Key1"), + self.get_item_by_name("Clocktower Key2"), + self.get_item_by_name("Clocktower Key3")] * 1) + self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower")) + self.collect([self.get_item_by_name("Special1")] * 7) + self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower")) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Magical Nitro"), + self.get_item_by_name("Mandragora")] * 1) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.collect([self.get_item_by_name("Magical Nitro"), + self.get_item_by_name("Mandragora")] * 1) + self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall")) + self.assertTrue(self.can_reach_entrance("Dracula's door")) + + +class DraculaBossTest(CV64TestBase): + options = { + "draculas_condition": 2, + "stage_shuffle": True, + "starting_stage": 5, + "hard_logic": True, + "bosses_required": 16 + } + + def test_dracula_boss_condition(self) -> None: + self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower")) + self.collect([self.get_item_by_name("Left Tower Key"), + self.get_item_by_name("Garden Key"), + self.get_item_by_name("Copper Key"), + self.get_item_by_name("Science Key1"), + self.get_item_by_name("Science Key2"), + self.get_item_by_name("Science Key3"), + self.get_item_by_name("Clocktower Key1"), + self.get_item_by_name("Clocktower Key2"), + self.get_item_by_name("Clocktower Key3")] * 1) + self.assertFalse(self.can_reach_entrance("Slope Jump to boss tower")) + self.collect([self.get_item_by_name("Special1")] * 7) + self.assertTrue(self.can_reach_entrance("Slope Jump to boss tower")) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.collect([self.get_item_by_name("Magical Nitro"), + self.get_item_by_name("Mandragora")] * 1) + self.assertFalse(self.can_reach_entrance("Dracula's door")) + self.assertFalse(self.can_reach_entrance("Lower sealed cracked wall")) + self.collect([self.get_item_by_name("Magical Nitro"), + self.get_item_by_name("Mandragora")] * 1) + self.assertTrue(self.can_reach_entrance("Lower sealed cracked wall")) + self.assertTrue(self.can_reach_entrance("Dracula's door")) + + +class LizardTest(CV64TestBase): + options = { + "stage_shuffle": True, + "draculas_condition": 2, + "starting_stage": 4 + } + + def test_lizard_man_trio(self) -> None: + self.assertTrue(self.can_reach_location("Underground Waterway: Lizard-man trio")) diff --git a/worlds/cv64/text.py b/worlds/cv64/text.py new file mode 100644 index 000000000000..76ffaf1f7d39 --- /dev/null +++ b/worlds/cv64/text.py @@ -0,0 +1,95 @@ +from typing import Tuple + +cv64_char_dict = {"\n": (0x01, 0), " ": (0x02, 4), "!": (0x03, 2), '"': (0x04, 5), "#": (0x05, 6), "$": (0x06, 5), + "%": (0x07, 8), "&": (0x08, 7), "'": (0x09, 4), "(": (0x0A, 3), ")": (0x0B, 3), "*": (0x0C, 4), + "+": (0x0D, 5), ",": (0x0E, 3), "-": (0x0F, 4), ".": (0x10, 3), "/": (0x11, 6), "0": (0x12, 5), + "1": (0x13, 3), "2": (0x14, 5), "3": (0x15, 4), "4": (0x16, 5), "5": (0x17, 5), "6": (0x18, 5), + "7": (0x19, 5), "8": (0x1A, 5), "9": (0x1B, 5), ":": (0x1C, 3), ";": (0x1D, 3), "<": (0x1E, 3), + "=": (0x1F, 4), ">": (0x20, 3), "?": (0x21, 5), "@": (0x22, 8), "A": (0x23, 7), "B": (0x24, 6), + "C": (0x25, 5), "D": (0x26, 7), "E": (0x27, 5), "F": (0x28, 6), "G": (0x29, 6), "H": (0x2A, 7), + "I": (0x2B, 3), "J": (0x2C, 3), "K": (0x2D, 6), "L": (0x2E, 6), "M": (0x2F, 8), "N": (0x30, 7), + "O": (0x31, 6), "P": (0x32, 6), "Q": (0x33, 8), "R": (0x34, 6), "S": (0x35, 5), "T": (0x36, 6), + "U": (0x37, 6), "V": (0x38, 7), "W": (0x39, 8), "X": (0x3A, 6), "Y": (0x3B, 7), "Z": (0x3C, 6), + "[": (0x3D, 3), "\\": (0x3E, 6), "]": (0x3F, 3), "^": (0x40, 6), "_": (0x41, 5), "a": (0x43, 5), + "b": (0x44, 6), "c": (0x45, 4), "d": (0x46, 6), "e": (0x47, 5), "f": (0x48, 5), "g": (0x49, 5), + "h": (0x4A, 6), "i": (0x4B, 3), "j": (0x4C, 3), "k": (0x4D, 6), "l": (0x4E, 3), "m": (0x4F, 8), + "n": (0x50, 6), "o": (0x51, 5), "p": (0x52, 5), "q": (0x53, 5), "r": (0x54, 4), "s": (0x55, 4), + "t": (0x56, 4), "u": (0x57, 5), "v": (0x58, 6), "w": (0x59, 8), "x": (0x5A, 5), "y": (0x5B, 5), + "z": (0x5C, 4), "{": (0x5D, 4), "|": (0x5E, 2), "}": (0x5F, 3), "`": (0x61, 4), "「": (0x62, 3), + "」": (0x63, 3), "~": (0x65, 3), "″": (0x72, 3), "°": (0x73, 3), "∞": (0x74, 8)} +# [0] = CV64's in-game ID for that text character. +# [1] = How much space towards the in-game line length limit it contributes. + + +def cv64_string_to_bytearray(cv64text: str, a_advance: bool = False, append_end: bool = True) -> bytearray: + """Converts a string into a bytearray following CV64's string format.""" + text_bytes = bytearray(0) + for i, char in enumerate(cv64text): + if char == "\t": + text_bytes.extend([0xFF, 0xFF]) + else: + if char in cv64_char_dict: + text_bytes.extend([0x00, cv64_char_dict[char][0]]) + else: + text_bytes.extend([0x00, 0x41]) + + if a_advance: + text_bytes.extend([0xA3, 0x00]) + if append_end: + text_bytes.extend([0xFF, 0xFF]) + return text_bytes + + +def cv64_text_truncate(cv64text: str, textbox_len_limit: int) -> str: + """Truncates a string at a given in-game text line length.""" + line_len = 0 + + for i in range(len(cv64text)): + line_len += cv64_char_dict[cv64text[i]][1] + + if line_len > textbox_len_limit: + return cv64text[0x00:i] + + return cv64text + + +def cv64_text_wrap(cv64text: str, textbox_len_limit: int) -> Tuple[str, int]: + """Rebuilds a string with some of its spaces replaced with newlines to ensure the text wraps properly in an in-game + textbox of a given length.""" + words = cv64text.split(" ") + new_text = "" + line_len = 0 + num_lines = 1 + + for i in range(len(words)): + word_len = 0 + word_divider = " " + + if line_len != 0: + line_len += 4 + else: + word_divider = "" + + for char in words[i]: + if char in cv64_char_dict: + line_len += cv64_char_dict[char][1] + word_len += cv64_char_dict[char][1] + else: + line_len += 5 + word_len += 5 + + if word_len > textbox_len_limit or char in ["\n", "\t"]: + word_len = 0 + line_len = 0 + if num_lines < 4: + num_lines += 1 + + if line_len > textbox_len_limit: + word_divider = "\n" + line_len = word_len + if num_lines < 4: + num_lines += 1 + + new_text += word_divider + words[i] + + return new_text, num_lines From 12864f7b24028fa56135e599f0fe1642c9d2d377 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Wed, 20 Mar 2024 22:44:09 +0100 Subject: [PATCH 10/74] A Short Hike: Implement New Game (#2577) --- README.md | 1 + docs/CODEOWNERS | 3 + worlds/shorthike/Items.py | 62 ++ worlds/shorthike/Locations.py | 709 +++++++++++++++++++++++ worlds/shorthike/Options.py | 73 +++ worlds/shorthike/Rules.py | 73 +++ worlds/shorthike/__init__.py | 136 +++++ worlds/shorthike/docs/en_A Short Hike.md | 30 + worlds/shorthike/docs/setup_en.md | 32 + 9 files changed, 1119 insertions(+) create mode 100644 worlds/shorthike/Items.py create mode 100644 worlds/shorthike/Locations.py create mode 100644 worlds/shorthike/Options.py create mode 100644 worlds/shorthike/Rules.py create mode 100644 worlds/shorthike/__init__.py create mode 100644 worlds/shorthike/docs/en_A Short Hike.md create mode 100644 worlds/shorthike/docs/setup_en.md diff --git a/README.md b/README.md index 2c0c164b53c4..18b1651bb039 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Currently, the following games are supported: * Celeste 64 * Zork Grand Inquisitor * Castlevania 64 +* A Short Hike For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 9c801f04af04..64407356625b 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -134,6 +134,9 @@ # Shivers /worlds/shivers/ @GodlFire +# A Short Hike +/worlds/shorthike/ @chandler05 + # Sonic Adventure 2 Battle /worlds/sa2b/ @PoryGone @RaspberrySpace diff --git a/worlds/shorthike/Items.py b/worlds/shorthike/Items.py new file mode 100644 index 000000000000..a240dcbc6a1f --- /dev/null +++ b/worlds/shorthike/Items.py @@ -0,0 +1,62 @@ +from BaseClasses import ItemClassification +from typing import TypedDict, Dict, List, Set + +class ItemDict(TypedDict): + name: str + id: int + count: int + classification: ItemClassification + +base_id = 82000 + +item_table: List[ItemDict] = [ + {"name": "Stick", "id": base_id + 1, "count": 8, "classification": ItemClassification.progression_skip_balancing}, + {"name": "Seashell", "id": base_id + 2, "count": 23, "classification": ItemClassification.progression_skip_balancing}, + {"name": "Golden Feather", "id": base_id + 3, "count": 0, "classification": ItemClassification.progression}, + {"name": "Silver Feather", "id": base_id + 4, "count": 0, "classification": ItemClassification.useful}, + {"name": "Bucket", "id": base_id + 5, "count": 0, "classification": ItemClassification.progression}, + {"name": "Bait", "id": base_id + 6, "count": 2, "classification": ItemClassification.filler}, + {"name": "Fishing Rod", "id": base_id + 7, "count": 2, "classification": ItemClassification.progression}, + {"name": "Shovel", "id": base_id + 8, "count": 1, "classification": ItemClassification.progression}, + {"name": "Toy Shovel", "id": base_id + 9, "count": 5, "classification": ItemClassification.progression_skip_balancing}, + {"name": "Compass", "id": base_id + 10, "count": 1, "classification": ItemClassification.useful}, + {"name": "Medal", "id": base_id + 11, "count": 3, "classification": ItemClassification.filler}, + {"name": "Shell Necklace", "id": base_id + 12, "count": 1, "classification": ItemClassification.progression}, + {"name": "Wristwatch", "id": base_id + 13, "count": 1, "classification": ItemClassification.progression}, + {"name": "Motorboat Key", "id": base_id + 14, "count": 1, "classification": ItemClassification.progression}, + {"name": "Pickaxe", "id": base_id + 15, "count": 3, "classification": ItemClassification.useful}, + {"name": "Fishing Journal", "id": base_id + 16, "count": 1, "classification": ItemClassification.useful}, + {"name": "A Stormy View Map", "id": base_id + 17, "count": 1, "classification": ItemClassification.filler}, + {"name": "The King Map", "id": base_id + 18, "count": 1, "classification": ItemClassification.filler}, + {"name": "The Treasure of Sid Beach Map", "id": base_id + 19, "count": 1, "classification": ItemClassification.filler}, + {"name": "In Her Shadow Map", "id": base_id + 20, "count": 1, "classification": ItemClassification.filler}, + {"name": "Sunhat", "id": base_id + 21, "count": 1, "classification": ItemClassification.filler}, + {"name": "Baseball Cap", "id": base_id + 22, "count": 1, "classification": ItemClassification.filler}, + {"name": "Provincial Park Hat", "id": base_id + 23, "count": 1, "classification": ItemClassification.filler}, + {"name": "Headband", "id": base_id + 24, "count": 1, "classification": ItemClassification.progression}, + {"name": "Running Shoes", "id": base_id + 25, "count": 1, "classification": ItemClassification.useful}, + {"name": "Camping Permit", "id": base_id + 26, "count": 1, "classification": ItemClassification.progression}, + {"name": "Walkie Talkie", "id": base_id + 27, "count": 1, "classification": ItemClassification.useful}, + + # Not in the item pool for now + #{"name": "Boating Manual", "id": base_id + ~, "count": 1, "classification": ItemClassification.filler}, + + # Different Coin Amounts (Fillers) + {"name": "7 Coins", "id": base_id + 28, "count": 3, "classification": ItemClassification.filler}, + {"name": "15 Coins", "id": base_id + 29, "count": 1, "classification": ItemClassification.filler}, + {"name": "18 Coins", "id": base_id + 30, "count": 1, "classification": ItemClassification.filler}, + {"name": "21 Coins", "id": base_id + 31, "count": 2, "classification": ItemClassification.filler}, + {"name": "25 Coins", "id": base_id + 32, "count": 7, "classification": ItemClassification.filler}, + {"name": "27 Coins", "id": base_id + 33, "count": 1, "classification": ItemClassification.filler}, + {"name": "32 Coins", "id": base_id + 34, "count": 1, "classification": ItemClassification.filler}, + {"name": "33 Coins", "id": base_id + 35, "count": 6, "classification": ItemClassification.filler}, + {"name": "50 Coins", "id": base_id + 36, "count": 1, "classification": ItemClassification.filler}, + + # Filler item determined by settings + {"name": "13 Coins", "id": base_id + 37, "count": 0, "classification": ItemClassification.filler}, +] + +group_table: Dict[str, Set[str]] = { + "Coins": {"7 Coins", "13 Coins", "15 Coins", "18 Coins", "21 Coins", "25 Coins", "27 Coins", "32 Coins", "33 Coins", "50 Coins"}, + "Maps": {"A Stormy View Map", "The King Map", "The Treasure of Sid Beach Map", "In Her Shadow Map"}, +} diff --git a/worlds/shorthike/Locations.py b/worlds/shorthike/Locations.py new file mode 100644 index 000000000000..c2d316c68675 --- /dev/null +++ b/worlds/shorthike/Locations.py @@ -0,0 +1,709 @@ +from typing import List, TypedDict + +class LocationInfo(TypedDict): + name: str + id: int + inGameId: str + needsShovel: bool + purchase: bool + minGoldenFeathers: int + minGoldenFeathersEasy: int + minGoldenFeathersBucket: int + +base_id = 83000 + +location_table: List[LocationInfo] = [ + # Original Seashell Locations + {"name": "Start Beach Seashell", + "id": base_id + 1, + "inGameId": "PickUps.3", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Beach Hut Seashell", + "id": base_id + 2, + "inGameId": "PickUps.2", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Beach Umbrella Seashell", + "id": base_id + 3, + "inGameId": "PickUps.8", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sid Beach Mound Seashell", + "id": base_id + 4, + "inGameId": "PickUps.12", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sid Beach Seashell", + "id": base_id + 5, + "inGameId": "PickUps.11", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Shirley's Point Beach Seashell", + "id": base_id + 6, + "inGameId": "PickUps.18", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Shirley's Point Rock Seashell", + "id": base_id + 7, + "inGameId": "PickUps.17", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Beach Seashell", + "id": base_id + 8, + "inGameId": "PickUps.19", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "West River Seashell", + "id": base_id + 9, + "inGameId": "PickUps.10", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "West Riverbank Seashell", + "id": base_id + 10, + "inGameId": "PickUps.4", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Stone Tower Riverbank Seashell", + "id": base_id + 11, + "inGameId": "PickUps.23", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "North Beach Seashell", + "id": base_id + 12, + "inGameId": "PickUps.6", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "North Coast Seashell", + "id": base_id + 13, + "inGameId": "PickUps.7", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Boat Cliff Seashell", + "id": base_id + 14, + "inGameId": "PickUps.14", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Boat Isle Mound Seashell", + "id": base_id + 15, + "inGameId": "PickUps.22", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "East Coast Seashell", + "id": base_id + 16, + "inGameId": "PickUps.21", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "House North Beach Seashell", + "id": base_id + 17, + "inGameId": "PickUps.16", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Airstream Island North Seashell", + "id": base_id + 18, + "inGameId": "PickUps.13", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Airstream Island South Seashell", + "id": base_id + 19, + "inGameId": "PickUps.15", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Secret Island Beach Seashell", + "id": base_id + 20, + "inGameId": "PickUps.1", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Meteor Lake Seashell", + "id": base_id + 126, + "inGameId": "PickUps.20", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Good Creek Path Seashell", + "id": base_id + 127, + "inGameId": "PickUps.9", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + + # Visitor's Center Shop + {"name": "Visitor's Center Shop Golden Feather 1", + "id": base_id + 21, + "inGameId": "CampRangerNPC[0]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Shop Golden Feather 2", + "id": base_id + 22, + "inGameId": "CampRangerNPC[1]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Shop Hat", + "id": base_id + 23, + "inGameId": "CampRangerNPC[9]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Tough Bird Salesman + {"name": "Tough Bird Salesman Golden Feather 1", + "id": base_id + 24, + "inGameId": "ToughBirdNPC (1)[0]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Tough Bird Salesman Golden Feather 2", + "id": base_id + 25, + "inGameId": "ToughBirdNPC (1)[1]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Tough Bird Salesman Golden Feather 3", + "id": base_id + 26, + "inGameId": "ToughBirdNPC (1)[2]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Tough Bird Salesman Golden Feather 4", + "id": base_id + 27, + "inGameId": "ToughBirdNPC (1)[3]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Tough Bird Salesman (400 Coins)", + "id": base_id + 28, + "inGameId": "ToughBirdNPC (1)[9]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + + # Beachstickball + {"name": "Beachstickball (10 Hits)", + "id": base_id + 29, + "inGameId": "VolleyballOpponent[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Beachstickball (20 Hits)", + "id": base_id + 30, + "inGameId": "VolleyballOpponent[1]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Beachstickball (30 Hits)", + "id": base_id + 31, + "inGameId": "VolleyballOpponent[2]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Misc Item Locations + {"name": "Shovel Kid Trade", + "id": base_id + 32, + "inGameId": "Frog_StandingNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Compass Guy", + "id": base_id + 33, + "inGameId": "Fox_WalkingNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Hawk Peak Bucket Rock", + "id": base_id + 34, + "inGameId": "Tools.23", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands Bucket Rock", + "id": base_id + 35, + "inGameId": "Tools.42", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Bill the Walrus Fisherman", + "id": base_id + 36, + "inGameId": "SittingNPC (1)[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Catch 3 Fish Reward", + "id": base_id + 37, + "inGameId": "FishBuyer[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Catch All Fish Reward", + "id": base_id + 38, + "inGameId": "FishBuyer[1]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7}, + {"name": "Permit Guy Bribe", + "id": base_id + 39, + "inGameId": "CamperNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Catch Fish with Permit", + "id": base_id + 129, + "inGameId": "Player[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Return Camping Permit", + "id": base_id + 130, + "inGameId": "CamperNPC[1]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + + # Original Pickaxe Locations + {"name": "Blocked Mine Pickaxe 1", + "id": base_id + 40, + "inGameId": "Tools.31", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Blocked Mine Pickaxe 2", + "id": base_id + 41, + "inGameId": "Tools.32", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Blocked Mine Pickaxe 3", + "id": base_id + 42, + "inGameId": "Tools.33", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Original Toy Shovel Locations + {"name": "Blackwood Trail Lookout Toy Shovel", + "id": base_id + 43, + "inGameId": "PickUps.27", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Shirley's Point Beach Toy Shovel", + "id": base_id + 44, + "inGameId": "PickUps.30", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Beach Toy Shovel", + "id": base_id + 45, + "inGameId": "PickUps.29", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Blackwood Trail Rock Toy Shovel", + "id": base_id + 46, + "inGameId": "PickUps.26", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Beach Hut Cliff Toy Shovel", + "id": base_id + 128, + "inGameId": "PickUps.28", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Original Stick Locations + {"name": "Secret Island Beach Trail Stick", + "id": base_id + 47, + "inGameId": "PickUps.25", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Below Lighthouse Walkway Stick", + "id": base_id + 48, + "inGameId": "Tools.3", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Beach Hut Rocky Pool Sand Stick", + "id": base_id + 49, + "inGameId": "Tools.0", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Cliff Overlooking West River Waterfall Stick", + "id": base_id + 50, + "inGameId": "Tools.2", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0}, + {"name": "Trail to Tough Bird Salesman Stick", + "id": base_id + 51, + "inGameId": "Tools.8", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "North Beach Stick", + "id": base_id + 52, + "inGameId": "Tools.4", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Beachstickball Court Stick", + "id": base_id + 53, + "inGameId": "VolleyballMinigame.4", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Stick Under Sid Beach Umbrella", + "id": base_id + 54, + "inGameId": "Tools.1", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Boating + {"name": "Boat Rental", + "id": base_id + 55, + "inGameId": "DadDeer[0]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Boat Challenge Reward", + "id": base_id + 56, + "inGameId": "DeerKidBoat[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Not a location for now, corresponding with the Boating Manual + # {"name": "Receive Boating Manual", + # "id": base_id + 133, + # "inGameId": "DadDeer[1]", + # "needsShovel": False, "purchase": False, + # "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Original Map Locations + {"name": "Outlook Point Dog Gift", + "id": base_id + 57, + "inGameId": "Dog_WalkingNPC_BlueEyed[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + + # Original Clothes Locations + {"name": "Collect 15 Seashells", + "id": base_id + 58, + "inGameId": "LittleKidNPCVariant (1)[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Return to Shell Kid", + "id": base_id + 132, + "inGameId": "LittleKidNPCVariant (1)[1]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Taylor the Turtle Headband Gift", + "id": base_id + 59, + "inGameId": "Turtle_WalkingNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Sue the Rabbit Shoes Reward", + "id": base_id + 60, + "inGameId": "Bunny_WalkingNPC (1)[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Purchase Sunhat", + "id": base_id + 61, + "inGameId": "SittingNPC[0]", + "needsShovel": False, "purchase": True, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Original Golden Feather Locations + {"name": "Blackwood Forest Golden Feather", + "id": base_id + 62, + "inGameId": "Feathers.3", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Ranger May Shell Necklace Golden Feather", + "id": base_id + 63, + "inGameId": "AuntMayNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sand Castle Golden Feather", + "id": base_id + 64, + "inGameId": "SandProvince.3", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Artist Golden Feather", + "id": base_id + 65, + "inGameId": "StandingNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Visitor Camp Rock Golden Feather", + "id": base_id + 66, + "inGameId": "Feathers.8", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Outlook Cliff Golden Feather", + "id": base_id + 67, + "inGameId": "Feathers.2", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Meteor Lake Cliff Golden Feather", + "id": base_id + 68, + "inGameId": "Feathers.7", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0}, + + # Original Silver Feather Locations + {"name": "Secret Island Peak", + "id": base_id + 69, + "inGameId": "PickUps.24", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 5, "minGoldenFeathersEasy": 7, "minGoldenFeathersBucket": 7}, + {"name": "Wristwatch Trade", + "id": base_id + 70, + "inGameId": "Goat_StandingNPC[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + + # Golden Chests + {"name": "Lighthouse Golden Chest", + "id": base_id + 71, + "inGameId": "Feathers.0", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 0}, + {"name": "Outlook Golden Chest", + "id": base_id + 72, + "inGameId": "Feathers.6", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Stone Tower Golden Chest", + "id": base_id + 73, + "inGameId": "Feathers.5", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "North Cliff Golden Chest", + "id": base_id + 74, + "inGameId": "Feathers.4", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 3, "minGoldenFeathersEasy": 10, "minGoldenFeathersBucket": 10}, + + # Chests + {"name": "Blackwood Cliff Chest", + "id": base_id + 75, + "inGameId": "Coins.22", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "White Coast Trail Chest", + "id": base_id + 76, + "inGameId": "Coins.6", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sid Beach Chest", + "id": base_id + 77, + "inGameId": "Coins.7", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sid Beach Buried Treasure Chest", + "id": base_id + 78, + "inGameId": "Coins.46", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Sid Beach Cliff Chest", + "id": base_id + 79, + "inGameId": "Coins.9", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Buried Chest", + "id": base_id + 80, + "inGameId": "Coins.94", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Visitor's Center Hidden Chest", + "id": base_id + 81, + "inGameId": "Coins.42", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Shirley's Point Chest", + "id": base_id + 82, + "inGameId": "Coins.10", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 1, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 2}, + {"name": "Caravan Cliff Chest", + "id": base_id + 83, + "inGameId": "Coins.12", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Caravan Arch Chest", + "id": base_id + 84, + "inGameId": "Coins.11", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "King Buried Treasure Chest", + "id": base_id + 85, + "inGameId": "Coins.41", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Good Creek Path Buried Chest", + "id": base_id + 86, + "inGameId": "Coins.48", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Good Creek Path West Chest", + "id": base_id + 87, + "inGameId": "Coins.33", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Good Creek Path East Chest", + "id": base_id + 88, + "inGameId": "Coins.62", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "West Waterfall Chest", + "id": base_id + 89, + "inGameId": "Coins.20", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Stone Tower West Cliff Chest", + "id": base_id + 90, + "inGameId": "PickUps.0", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Bucket Path Chest", + "id": base_id + 91, + "inGameId": "Coins.50", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Bucket Cliff Chest", + "id": base_id + 92, + "inGameId": "Coins.49", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5}, + {"name": "In Her Shadow Buried Treasure Chest", + "id": base_id + 93, + "inGameId": "Feathers.9", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Meteor Lake Buried Chest", + "id": base_id + 94, + "inGameId": "Coins.86", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Meteor Lake Chest", + "id": base_id + 95, + "inGameId": "Coins.64", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "House North Beach Chest", + "id": base_id + 96, + "inGameId": "Coins.65", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "East Coast Chest", + "id": base_id + 97, + "inGameId": "Coins.98", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Fisherman's Boat Chest 1", + "id": base_id + 99, + "inGameId": "Boat.0", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Fisherman's Boat Chest 2", + "id": base_id + 100, + "inGameId": "Boat.7", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Airstream Island Chest", + "id": base_id + 101, + "inGameId": "Coins.31", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "West River Waterfall Head Chest", + "id": base_id + 102, + "inGameId": "Coins.34", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Old Building Chest", + "id": base_id + 103, + "inGameId": "Coins.104", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Old Building West Chest", + "id": base_id + 104, + "inGameId": "Coins.109", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Old Building East Chest", + "id": base_id + 105, + "inGameId": "Coins.8", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Hawk Peak West Chest", + "id": base_id + 106, + "inGameId": "Coins.21", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5}, + {"name": "Hawk Peak East Buried Chest", + "id": base_id + 107, + "inGameId": "Coins.76", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5}, + {"name": "Hawk Peak Northeast Chest", + "id": base_id + 108, + "inGameId": "Coins.79", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 3, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 5}, + {"name": "Northern East Coast Chest", + "id": base_id + 109, + "inGameId": "Coins.45", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 2, "minGoldenFeathersBucket": 0}, + {"name": "North Coast Chest", + "id": base_id + 110, + "inGameId": "Coins.28", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "North Coast Buried Chest", + "id": base_id + 111, + "inGameId": "Coins.47", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Small South Island Buried Chest", + "id": base_id + 112, + "inGameId": "Coins.87", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Secret Island Bottom Chest", + "id": base_id + 113, + "inGameId": "Coins.88", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Secret Island Treehouse Chest", + "id": base_id + 114, + "inGameId": "Coins.89", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 1}, + {"name": "Sunhat Island Buried Chest", + "id": base_id + 115, + "inGameId": "Coins.112", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands South Buried Chest", + "id": base_id + 116, + "inGameId": "Coins.119", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands West Chest", + "id": base_id + 117, + "inGameId": "Coins.121", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands North Buried Chest", + "id": base_id + 118, + "inGameId": "Coins.117", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 1, "minGoldenFeathersEasy": 1, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands East Chest", + "id": base_id + 119, + "inGameId": "Coins.120", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands South Hidden Chest", + "id": base_id + 120, + "inGameId": "Coins.124", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "A Stormy View Buried Treasure Chest", + "id": base_id + 121, + "inGameId": "Coins.113", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, + {"name": "Orange Islands Ruins Buried Chest", + "id": base_id + 122, + "inGameId": "Coins.118", + "needsShovel": True, "purchase": False, + "minGoldenFeathers": 2, "minGoldenFeathersEasy": 4, "minGoldenFeathersBucket": 0}, + + # Race Rewards + {"name": "Lighthouse Race Reward", + "id": base_id + 123, + "inGameId": "RaceOpponent[0]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 2, "minGoldenFeathersEasy": 3, "minGoldenFeathersBucket": 1}, + {"name": "Old Building Race Reward", + "id": base_id + 124, + "inGameId": "RaceOpponent[1]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 1, "minGoldenFeathersEasy": 5, "minGoldenFeathersBucket": 0}, + {"name": "Hawk Peak Race Reward", + "id": base_id + 125, + "inGameId": "RaceOpponent[2]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 7, "minGoldenFeathersEasy": 9, "minGoldenFeathersBucket": 7}, + {"name": "Lose Race Gift", + "id": base_id + 131, + "inGameId": "RaceOpponent[9]", + "needsShovel": False, "purchase": False, + "minGoldenFeathers": 0, "minGoldenFeathersEasy": 0, "minGoldenFeathersBucket": 0}, +] diff --git a/worlds/shorthike/Options.py b/worlds/shorthike/Options.py new file mode 100644 index 000000000000..2f378f18ff6e --- /dev/null +++ b/worlds/shorthike/Options.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +from Options import Choice, PerGameCommonOptions, Range, StartInventoryPool, Toggle + +class Goal(Choice): + """Choose the end goal. + Nap: Complete the climb to the top of Hawk Peak and take a nap + Photo: Get your picture taken at the top of Hawk Peak + Races: Complete all three races with Avery + Help Everyone: Travel around Hawk Peak and help every character with their troubles + Fishmonger: Catch one of every fish from around Hawk Peak""" + display_name = "Goal" + option_nap = 0 + option_photo = 1 + option_races = 2 + option_help_everyone = 3 + option_fishmonger = 4 + default = 3 + +class CoinsInShops(Toggle): + """When enabled, the randomizer can place coins into locations that are purchased, such as shops.""" + display_name = "Coins in Purchaseable Locations" + default = False + +class GoldenFeathers(Range): + """Number of Golden Feathers in the item pool. + (Note that for the Photo and Help Everyone goals, a minimum of 12 Golden Feathers is enforced)""" + display_name = "Golden Feathers" + range_start = 0 + range_end = 20 + default = 20 + +class SilverFeathers(Range): + """Number of Silver Feathers in the item pool.""" + display_name = "Silver Feathers" + range_start = 0 + range_end = 20 + default = 2 + +class Buckets(Range): + """Number of Buckets in the item pool.""" + display_name = "Buckets" + range_start = 0 + range_end = 2 + default = 2 + +class GoldenFeatherProgression(Choice): + """Determines which locations are considered in logic based on the required amount of golden feathers to reach them. + Easy: Locations will be considered inaccessible until the player has enough golden feathers to easily reach them. A minimum of 10 golden feathers is recommended for this setting. + Normal: Locations will be considered inaccessible until the player has the minimum possible number of golden feathers to reach them. A minimum of 7 golden feathers is recommended for this setting. + Hard: Removes the requirement of golden feathers for progression entirely and glitches may need to be used to progress""" + display_name = "Golden Feather Progression" + option_easy = 0 + option_normal = 1 + option_hard = 2 + default = 1 + +class CostMultiplier(Range): + """The percentage that all item shop costs will be of the vanilla values.""" + display_name = "Shop Cost Multiplier" + range_start = 25 + range_end = 200 + default = 100 + +@dataclass +class ShortHikeOptions(PerGameCommonOptions): + start_inventory_from_pool: StartInventoryPool + goal: Goal + coins_in_shops: CoinsInShops + golden_feathers: GoldenFeathers + silver_feathers: SilverFeathers + buckets: Buckets + golden_feather_progression: GoldenFeatherProgression + cost_multiplier: CostMultiplier diff --git a/worlds/shorthike/Rules.py b/worlds/shorthike/Rules.py new file mode 100644 index 000000000000..73a16434219e --- /dev/null +++ b/worlds/shorthike/Rules.py @@ -0,0 +1,73 @@ +from worlds.generic.Rules import forbid_items_for_player, add_rule + +def create_rules(self, location_table): + multiworld = self.multiworld + player = self.player + options = self.options + + # Shovel Rules + for loc in location_table: + if loc["needsShovel"]: + forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Maps'], player) + add_rule(multiworld.get_location(loc["name"], player), + lambda state: state.has("Shovel", player)) + if loc["purchase"] and not options.coins_in_shops: + forbid_items_for_player(multiworld.get_location(loc["name"], player), self.item_name_groups['Coins'], player) + + # Minimum Feather Rules + if options.golden_feather_progression != 2: + min_feathers = get_min_feathers(self, loc["minGoldenFeathers"], loc["minGoldenFeathersEasy"]) + + if options.buckets > 0 and loc["minGoldenFeathersBucket"] < min_feathers: + add_rule(multiworld.get_location(loc["name"], player), + lambda state, loc=loc, min_feathers=min_feathers: state.has("Golden Feather", player, min_feathers) + or (state.has("Bucket", player) and state.has("Golden Feather", player, loc["minGoldenFeathersBucket"]))) + elif min_feathers > 0: + add_rule(multiworld.get_location(loc["name"], player), + lambda state, min_feathers=min_feathers: state.has("Golden Feather", player, min_feathers)) + add_rule(multiworld.get_location("Shovel Kid Trade", player), + lambda state: state.has("Toy Shovel", player)) + add_rule(multiworld.get_location("Sand Castle Golden Feather", player), + lambda state: state.has("Toy Shovel", player)) + + # Fishing Rules + add_rule(multiworld.get_location("Catch 3 Fish Reward", player), + lambda state: state.has("Fishing Rod", player)) + add_rule(multiworld.get_location("Catch Fish with Permit", player), + lambda state: state.has("Fishing Rod", player)) + add_rule(multiworld.get_location("Catch All Fish Reward", player), + lambda state: state.has("Fishing Rod", player)) + + # Misc Rules + add_rule(multiworld.get_location("Return Camping Permit", player), + lambda state: state.has("Camping Permit", player)) + add_rule(multiworld.get_location("Boat Challenge Reward", player), + lambda state: state.has("Motorboat Key", player)) + add_rule(multiworld.get_location("Collect 15 Seashells", player), + lambda state: state.has("Seashell", player, 15)) + add_rule(multiworld.get_location("Wristwatch Trade", player), + lambda state: state.has("Wristwatch", player)) + add_rule(multiworld.get_location("Sue the Rabbit Shoes Reward", player), + lambda state: state.has("Headband", player)) + add_rule(multiworld.get_location("Return to Shell Kid", player), + lambda state: state.has("Shell Necklace", player) and state.has("Seashell", player, 15)) + add_rule(multiworld.get_location("Ranger May Shell Necklace Golden Feather", player), + lambda state: state.has("Shell Necklace", player)) + add_rule(multiworld.get_location("Beachstickball (10 Hits)", player), + lambda state: state.has("Stick", player)) + add_rule(multiworld.get_location("Beachstickball (20 Hits)", player), + lambda state: state.has("Stick", player)) + add_rule(multiworld.get_location("Beachstickball (30 Hits)", player), + lambda state: state.has("Stick", player)) + +def get_min_feathers(self, min_golden_feathers, min_golden_feathers_easy): + options = self.options + + min_feathers = min_golden_feathers + if options.golden_feather_progression == 0: + min_feathers = min_golden_feathers_easy + if min_feathers > options.golden_feathers: + if options.goal != 1 and options.goal != 3: + min_feathers = options.golden_feathers + + return min_feathers diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py new file mode 100644 index 000000000000..8a4ef932336d --- /dev/null +++ b/worlds/shorthike/__init__.py @@ -0,0 +1,136 @@ +from collections import Counter +from typing import ClassVar, Dict, Any, Type +from BaseClasses import Region, Location, Item, Tutorial +from Options import PerGameCommonOptions +from worlds.AutoWorld import World, WebWorld +from .Items import item_table, group_table, base_id +from .Locations import location_table +from .Rules import create_rules, get_min_feathers +from .Options import ShortHikeOptions + +class ShortHikeWeb(WebWorld): + theme = "ocean" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the A Short Hike randomizer connected to an Archipelago Multiworld", + "English", + "setup_en.md", + "setup/en", + ["Chandler"] + )] + +class ShortHikeWorld(World): + """ + A Short Hike is a relaxing adventure set on the islands of Hawk Peak. Fly and climb using Claire's wings and Golden Feathers + to make your way up to the summit. Along the way you'll meet other hikers, discover hidden treasures, + and take in the beautiful world around you. + """ + + game = "A Short Hike" + web = ShortHikeWeb() + data_version = 2 + + item_name_to_id = {item["name"]: item["id"] for item in item_table} + location_name_to_id = {loc["name"]: loc["id"] for loc in location_table} + location_name_to_game_id = {loc["name"]: loc["inGameId"] for loc in location_table} + + item_name_groups = group_table + + options_dataclass: ClassVar[Type[PerGameCommonOptions]] = ShortHikeOptions + options: ShortHikeOptions + + required_client_version = (0, 4, 4) + + def __init__(self, multiworld, player): + super(ShortHikeWorld, self).__init__(multiworld, player) + + def get_filler_item_name(self) -> str: + return "13 Coins" + + def create_item(self, name: str) -> "ShortHikeItem": + item_id: int = self.item_name_to_id[name] + id = item_id - base_id - 1 + + return ShortHikeItem(name, item_table[id]["classification"], item_id, player=self.player) + + def create_items(self) -> None: + for item in item_table: + count = item["count"] + + if count <= 0: + continue + else: + for i in range(count): + self.multiworld.itempool.append(self.create_item(item["name"])) + + feather_count = self.options.golden_feathers + if self.options.goal == 1 or self.options.goal == 3: + if feather_count < 12: + feather_count = 12 + + junk = 45 - self.options.silver_feathers - feather_count - self.options.buckets + self.multiworld.itempool += [self.create_item(self.get_filler_item_name()) for _ in range(junk)] + self.multiworld.itempool += [self.create_item("Golden Feather") for _ in range(feather_count)] + self.multiworld.itempool += [self.create_item("Silver Feather") for _ in range(self.options.silver_feathers)] + self.multiworld.itempool += [self.create_item("Bucket") for _ in range(self.options.buckets)] + + def create_regions(self) -> None: + menu_region = Region("Menu", self.player, self.multiworld) + self.multiworld.regions.append(menu_region) + + main_region = Region("Hawk Peak", self.player, self.multiworld) + + for loc in self.location_name_to_id.keys(): + main_region.locations.append(ShortHikeLocation(self.player, loc, self.location_name_to_id[loc], main_region)) + + self.multiworld.regions.append(main_region) + + menu_region.connect(main_region) + + if self.options.goal == "nap": + # Nap + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9)) + or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7))) + elif self.options.goal == "photo": + # Photo + self.multiworld.completion_condition[self.player] = lambda state: state.has("Golden Feather", self.player, 12) + elif self.options.goal == "races": + # Races + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9)) + or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7))) + elif self.options.goal == "help_everyone": + # Help Everyone + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, 12) + and state.has("Toy Shovel", self.player) and state.has("Camping Permit", self.player) + and state.has("Motorboat Key", self.player) and state.has("Headband", self.player) + and state.has("Wristwatch", self.player) and state.has("Seashell", self.player, 15) + and state.has("Shell Necklace", self.player)) + elif self.options.goal == "fishmonger": + # Fishmonger + self.multiworld.completion_condition[self.player] = lambda state: (state.has("Golden Feather", self.player, get_min_feathers(self, 7, 9)) + or (state.has("Bucket", self.player) and state.has("Golden Feather", self.player, 7)) + and state.has("Fishing Rod", self.player)) + + def set_rules(self): + create_rules(self, location_table) + + def fill_slot_data(self) -> Dict[str, Any]: + options = self.options + + settings = { + "goal": int(options.goal), + "logicLevel": int(options.golden_feather_progression), + "costMultiplier": int(options.cost_multiplier), + } + + slot_data = { + "settings": settings, + } + + return slot_data + +class ShortHikeItem(Item): + game: str = "A Short Hike" + +class ShortHikeLocation(Location): + game: str = "A Short Hike" diff --git a/worlds/shorthike/docs/en_A Short Hike.md b/worlds/shorthike/docs/en_A Short Hike.md new file mode 100644 index 000000000000..516bf28e47fc --- /dev/null +++ b/worlds/shorthike/docs/en_A Short Hike.md @@ -0,0 +1,30 @@ +# A Short Hike + +## What does randomization do to this game? + +All items that can be obtained from chests, the ground, and NPCs are randomized. + +## What does another world's item look like in A Short Hike? + +All items are replaced with chests that can contain items from other worlds. +Items will appear with the Archipelago logo next to them when obtained. + +## Which characters need to be helped for the Help Everyone goal? + +To achieve the Help Everyone goal, the following characters will need to be helped: +- Pay Tough Bird Salesman's Tuition Fee +- Give Frog a Toy Shovel +- Return the Camper's Camping Permit +- Complete the Deer Kid's Boating Challenge +- Find Sue's Headband +- Clean Up and Purchase the Sunhat from the Deer +- Return the Camper's Wristwatch +- Cheer Up the Artist +- Collect 15 Shells for the Kid +- Give the Shell Necklace to Aunt May +- Help the Fox Climb the Mountain + +## Can I have more than one save at a time? + +No, unfortunately only one save slot is available for use in A Short Hike. +Starting a new save will erase the old one _permanently_. \ No newline at end of file diff --git a/worlds/shorthike/docs/setup_en.md b/worlds/shorthike/docs/setup_en.md new file mode 100644 index 000000000000..e327d8bed93c --- /dev/null +++ b/worlds/shorthike/docs/setup_en.md @@ -0,0 +1,32 @@ +# A Short Hike Multiworld Setup Guide + +## Required Software + +- A Short Hike: [Steam](https://store.steampowered.com/app/1055540/A_Short_Hike/) + - The Epic Games Store or itch.io version of A Short Hike will also work. +- A Short Hike Modding Tools: [GitHub](https://github.com/BrandenEK/AShortHike.ModdingTools) +- A Short Hike Randomizer: [GitHub](https://github.com/BrandenEK/AShortHike.Randomizer) + +## Optional Software + +- [PopTracker](https://github.com/black-sliver/PopTracker/) + - [Chandler's A Short Hike PopTracker Pack](https://github.com/chandler05/shorthike-archipelago-poptracker/releases) + +## Installation + +1. Download the [Modding Tools](https://github.com/BrandenEK/AShortHike.ModdingTools/releases), and follow +the [installation instructions](https://github.com/BrandenEK/AShortHike.ModdingTools#a-short-hike-modding-tools) on the GitHub page. + +2. After the Modding Tools have been installed, download the +[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) and extract the contents of it +into the `Modding` folder. + +## Connecting + +A Short Hike will prompt you with the server details when a new game is started or a previous one is continued. +Enter in the Server Port, Name, and Password (optional) in the popup menu that appears and hit connect. + +## Tracking + +Install PopTracker from the link above and place the PopTracker pack into the packs folder. +Connect to Archipelago via the AP button in the top left. \ No newline at end of file From f4b7c28a33bb163768871616023a8cf3879840b4 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Wed, 20 Mar 2024 17:45:32 -0500 Subject: [PATCH 11/74] APProcedurePatch: hotfix changing class variables to instance variables (#2996) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/Files.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/worlds/Files.py b/worlds/Files.py index 6fee582c872d..6e9bf6b31b59 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -7,7 +7,7 @@ import os import threading -from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload +from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence import bsdiff4 @@ -41,7 +41,7 @@ def get_handler(file: str) -> Optional[AutoPatchRegister]: class AutoPatchExtensionRegister(abc.ABCMeta): extension_types: ClassVar[Dict[str, AutoPatchExtensionRegister]] = {} - required_extensions: List[str] = [] + required_extensions: Tuple[str, ...] = () def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchExtensionRegister: # construct class @@ -51,7 +51,9 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Aut return new_class @staticmethod - def get_handler(game: str) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]: + def get_handler(game: Optional[str]) -> Union[AutoPatchExtensionRegister, List[AutoPatchExtensionRegister]]: + if not game: + return APPatchExtension handler = AutoPatchExtensionRegister.extension_types.get(game, APPatchExtension) if handler.required_extensions: handlers = [handler] @@ -191,7 +193,7 @@ class APProcedurePatch(APAutoPatchInterface): hash: Optional[str] # base checksum of source file source_data: bytes patch_file_ending: str = "" - files: Dict[str, bytes] = {} + files: Dict[str, bytes] @classmethod def get_source_data(cls) -> bytes: @@ -206,6 +208,7 @@ def get_source_data_with_cache(cls) -> bytes: def __init__(self, *args: Any, **kwargs: Any): super(APProcedurePatch, self).__init__(*args, **kwargs) + self.files = {} def get_manifest(self) -> Dict[str, Any]: manifest = super(APProcedurePatch, self).get_manifest() @@ -277,7 +280,7 @@ def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None: super(APDeltaPatch, self).__init__(*args, **kwargs) self.patched_path = patched_path - def write_contents(self, opened_zipfile: zipfile.ZipFile): + def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None: self.write_file("delta.bsdiff4", bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read())) super(APDeltaPatch, self).write_contents(opened_zipfile) @@ -296,12 +299,12 @@ class APTokenMixin: """ A class that defines functions for generating a token binary, for use in patches. """ - tokens: List[ + _tokens: Sequence[ Tuple[APTokenTypes, int, Union[ bytes, # WRITE Tuple[int, int], # COPY, RLE int # AND_8, OR_8, XOR_8 - ]]] = [] + ]]] = () def get_token_binary(self) -> bytes: """ @@ -309,8 +312,8 @@ def get_token_binary(self) -> bytes: :return: A bytes object representing the token data. """ data = bytearray() - data.extend(len(self.tokens).to_bytes(4, "little")) - for token_type, offset, args in self.tokens: + data.extend(len(self._tokens).to_bytes(4, "little")) + for token_type, offset, args in self._tokens: data.append(token_type) data.extend(offset.to_bytes(4, "little")) if token_type in [APTokenTypes.AND_8, APTokenTypes.OR_8, APTokenTypes.XOR_8]: @@ -351,11 +354,14 @@ def write_token(self, data: bytes) -> None: ... - def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]): + def write_token(self, token_type: APTokenTypes, offset: int, data: Union[bytes, Tuple[int, int], int]) -> None: """ Stores a token to be used by patching. """ - self.tokens.append((token_type, offset, data)) + if not isinstance(self._tokens, list): + assert len(self._tokens) == 0, f"{type(self)}._tokens was tampered with." + self._tokens = [] + self._tokens.append((token_type, offset, data)) class APPatchExtension(metaclass=AutoPatchExtensionRegister): @@ -371,10 +377,10 @@ class APPatchExtension(metaclass=AutoPatchExtensionRegister): Patch extension functions must return the changed bytes. """ game: str - required_extensions: List[str] = [] + required_extensions: ClassVar[Tuple[str, ...]] = () @staticmethod - def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str): + def apply_bsdiff4(caller: APProcedurePatch, rom: bytes, patch: str) -> bytes: """Applies the given bsdiff4 from the patch onto the current file.""" return bsdiff4.patch(rom, caller.get_file(patch)) @@ -411,7 +417,7 @@ def apply_tokens(caller: APProcedurePatch, rom: bytes, token_file: str) -> bytes return bytes(rom_data) @staticmethod - def calc_snes_crc(caller: APProcedurePatch, rom: bytes): + def calc_snes_crc(caller: APProcedurePatch, rom: bytes) -> bytes: """Calculates and applies a valid CRC for the SNES rom header.""" rom_data = bytearray(rom) if len(rom) < 0x8000: From 30a0aa2c85a7015e2072b5781ed1078965f62f4b Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 21 Mar 2024 10:46:53 -0500 Subject: [PATCH 12/74] Lingo: Add item/location groups (#2789) --- worlds/lingo/__init__.py | 6 +- worlds/lingo/data/LL1.yaml | 194 ++++++++++++++--------- worlds/lingo/data/generated.dat | Bin 129731 -> 130791 bytes worlds/lingo/datatypes.py | 3 +- worlds/lingo/items.py | 18 ++- worlds/lingo/locations.py | 6 +- worlds/lingo/player_logic.py | 4 +- worlds/lingo/utils/pickle_static_data.py | 13 +- worlds/lingo/utils/validate_config.rb | 2 +- 9 files changed, 157 insertions(+), 89 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index e35a1026b731..c92e53069edc 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -6,8 +6,8 @@ from BaseClasses import Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance -from .items import ALL_ITEM_TABLE, LingoItem -from .locations import ALL_LOCATION_TABLE +from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, LingoItem +from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP from .options import LingoOptions from .player_logic import LingoPlayerLogic from .regions import create_regions @@ -46,6 +46,8 @@ class LingoWorld(World): location_name_to_id = { name: data.code for name, data in ALL_LOCATION_TABLE.items() } + item_name_groups = ITEMS_BY_GROUP + location_name_groups = LOCATIONS_BY_GROUP player_logic: LingoPlayerLogic diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index f72e63c1427e..75f688268f9d 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -63,12 +63,13 @@ # - item_name: Overrides the name of the item generated for this door. # If not specified, the item name will be generated from # the room name and the door name. + # - item_group: If set, this item will be in the specified item group. # - location_name: Overrides the name of the location generated for this # door. If not specified, the location name will be # generated using the names of the panels. # - skip_location: If true, no location is generated for this door. # - skip_item: If true, no item is generated for this door. - # - group: When simple doors is used, all doors with the same group + # - door_group: When simple doors is used, all doors with the same group # will be covered by a single item. # - include_reduce: Door checks are assumed to be EXCLUDED when reduce checks # is on. This option includes the check anyway. @@ -144,7 +145,7 @@ - Palindrome Room Area Doors/Door_racecar_racecar_2 - Palindrome Room Area Doors/Door_solos_solos_2 skip_location: True - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - room: The Tenacious panel: LEVEL (Black) @@ -231,7 +232,7 @@ Dead End Door: id: Appendix Room Area Doors/Door_rat_tar_2 skip_location: true - group: Dead End Area Access + door_group: Dead End Area Access panels: - room: Hub Room panel: RAT @@ -244,6 +245,7 @@ Seeker Entrance: id: Entry Room Area Doors/Door_entrance_entrance item_name: The Seeker - Entrance + item_group: Achievement Room Entrances panels: - OPEN Rhyme Room Entrance: @@ -251,7 +253,7 @@ - Appendix Room Area Doors/Door_rat_tar_3 - Double Room Area Doors/Door_room_entry_stairs skip_location: True - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - room: The Tenacious panel: LEVEL (Black) @@ -466,25 +468,27 @@ - ORDER Tenacious Entrance: id: Palindrome Room Area Doors/Door_slaughter_laughter - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious + item_group: Achievement Room Entrances panels: - SLAUGHTER Shortcut to Hedge Maze: id: Maze Area Doors/Door_trace_trace - group: Hedge Maze Doors + door_group: Hedge Maze Doors panels: - TRACE Near RAT Door: id: Appendix Room Area Doors/Door_deadend_deadened skip_location: True - group: Dead End Area Access + door_group: Dead End Area Access panels: - room: Hidden Room panel: DEAD END Traveled Entrance: id: Appendix Room Area Doors/Door_open_open item_name: The Traveled - Entrance - group: Entrance to The Traveled + door_group: Entrance to The Traveled + item_group: Achievement Room Entrances panels: - OPEN Lost Door: @@ -546,6 +550,7 @@ doors: Sun Painting: item_name: Pilgrim Room - Sun Painting + item_group: Paintings location_name: Pilgrim Room - HOT CRUST painting_id: pilgrim_painting2 panels: @@ -715,12 +720,14 @@ doors: Tenacious Entrance: id: Palindrome Room Area Doors/Door_decay_day - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious + item_group: Achievement Room Entrances panels: - DECAY Discerning Entrance: id: Shuffle Room Area Doors/Door_nope_open item_name: The Discerning - Entrance + item_group: Achievement Room Entrances panels: - NOPE Tower Entrance: @@ -729,13 +736,13 @@ - Shuffle Room Area Doors/Door_tower2 - Shuffle Room Area Doors/Door_tower3 - Shuffle Room Area Doors/Door_tower4 - group: Crossroads - Tower Entrances + door_group: Crossroads - Tower Entrances panels: - WE ROT Tower Back Entrance: id: Shuffle Room Area Doors/Door_runt location_name: Crossroads - TURN/RUNT - group: Crossroads - Tower Entrances + door_group: Crossroads - Tower Entrances panels: - TURN - room: Orange Tower Fourth Floor @@ -744,20 +751,20 @@ id: - Shuffle Room Area Doors/Door_words_shuffle_3 - Shuffle Room Area Doors/Door_words_shuffle_4 - group: Crossroads Doors + door_group: Crossroads Doors panels: - WORDS - SWORD Eye Wall: id: Shuffle Room Area Doors/Door_behind junk_item: True - group: Crossroads Doors + door_group: Crossroads Doors panels: - BEND HI Hollow Hallway: id: Shuffle Room Area Doors/Door_crossroads6 skip_location: True - group: Crossroads Doors + door_group: Crossroads Doors panels: - BEND HI Roof Access: @@ -934,7 +941,7 @@ - Palindrome Room Area Doors/Door_racecar_racecar_1 - Palindrome Room Area Doors/Door_solos_solos_1 location_name: The Tenacious - Palindromes - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious panels: - LEVEL (Black) - RACECAR (Black) @@ -965,7 +972,7 @@ id: - Symmetry Room Area Doors/Door_near_far - Symmetry Room Area Doors/Door_far_near - group: Symmetry Doors + door_group: Symmetry Doors item_name: Symmetry Room - Near Far Door location_name: Symmetry Room - NEAR, FAR panels: @@ -992,7 +999,7 @@ id: - Symmetry Room Area Doors/Door_warts_straw - Symmetry Room Area Doors/Door_straw_warts - group: Symmetry Doors + door_group: Symmetry Doors item_name: Symmetry Room - Warts Straw Door location_name: Symmetry Room - WARTS, STRAW panels: @@ -1019,7 +1026,7 @@ id: - Symmetry Room Area Doors/Door_leaf_feel - Symmetry Room Area Doors/Door_feel_leaf - group: Symmetry Doors + door_group: Symmetry Doors item_name: Symmetry Room - Leaf Feel Door location_name: Symmetry Room - LEAF, FEEL panels: @@ -1156,34 +1163,37 @@ doors: Tenacious Entrance: id: Palindrome Room Area Doors/Door_massacred_sacred - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious + item_group: Achievement Room Entrances panels: - MASSACRED Black Door: id: Symmetry Room Area Doors/Door_black_white - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious panels: - BLACK Agreeable Entrance: id: Symmetry Room Area Doors/Door_close_open item_name: The Agreeable - Entrance + item_group: Achievement Room Entrances panels: - CLOSE Painting Shortcut: item_name: Starting Room - Street Painting + item_group: Paintings painting_id: eyes_yellow_painting2 panels: - RIGHT Purple Barrier: id: Color Arrow Room Doors/Door_purple_3 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt panel: PURPLE Hallway Door: id: Red Blue Purple Room Area Doors/Door_room_2 - group: Hallway Room Doors + door_group: Hallway Room Doors location_name: Hallway Room - First Room panels: - WALL @@ -1229,7 +1239,8 @@ doors: Tenacious Entrance: id: Palindrome Room Area Doors/Door_dread_dead - group: Entrances to The Tenacious + door_group: Entrances to The Tenacious + item_group: Achievement Room Entrances panels: - DREAD The Agreeable: @@ -1298,7 +1309,8 @@ doors: Shortcut to Hedge Maze: id: Symmetry Room Area Doors/Door_bye_hi - group: Hedge Maze Doors + item_group: Achievement Room Entrances + door_group: Hedge Maze Doors panels: - BYE Hedge Maze: @@ -1391,12 +1403,14 @@ Perceptive Entrance: id: Maze Area Doors/Door_maze_maze item_name: The Perceptive - Entrance - group: Hedge Maze Doors + door_group: Hedge Maze Doors + item_group: Achievement Room Entrances panels: - DOWN Painting Shortcut: painting_id: garden_painting_tower2 item_name: Starting Room - Hedge Maze Painting + item_group: Paintings skip_location: True panels: - DOWN @@ -1407,7 +1421,8 @@ - Maze Area Doors/Door_look_room_3 skip_location: True item_name: The Observant - Entrance - group: Observant Doors + door_group: Observant Doors + item_group: Achievement Room Entrances panels: - room: The Perceptive panel: GAZE @@ -1473,7 +1488,7 @@ Second Floor: id: Naps Room Doors/Door_hider_5 location_name: The Fearless - First Floor Puzzles - group: Fearless Doors + door_group: Fearless Doors panels: - SPAN - TEAM @@ -1525,7 +1540,7 @@ - Naps Room Doors/Door_hider_1b2 - Naps Room Doors/Door_hider_new1 location_name: The Fearless - Second Floor Puzzles - group: Fearless Doors + door_group: Fearless Doors panels: - NONE - SUM @@ -1680,13 +1695,13 @@ doors: Backside Door: id: Maze Area Doors/Door_backside - group: Backside Doors + door_group: Backside Doors panels: - FOUR (1) - FOUR (2) Stairs: id: Maze Area Doors/Door_stairs - group: Observant Doors + door_group: Observant Doors panels: - SIX The Incomparable: @@ -1764,7 +1779,7 @@ Eight Door: id: Red Blue Purple Room Area Doors/Door_a_strands location_name: Giant Sevens - group: Observant Doors + door_group: Observant Doors panels: - I (Seven) - room: Courtyard @@ -1915,13 +1930,13 @@ doors: Shortcut to Hub Room: id: Shuffle Room Area Doors/Door_secret_secret - group: Orange Tower First Floor - Shortcuts + door_group: Orange Tower First Floor - Shortcuts panels: - SECRET Salt Pepper Door: id: Count Up Room Area Doors/Door_salt_pepper location_name: Orange Tower First Floor - Salt Pepper Door - group: Orange Tower First Floor - Shortcuts + door_group: Orange Tower First Floor - Shortcuts panels: - SALT - room: Directional Gallery @@ -1967,7 +1982,7 @@ doors: Red Barrier: id: Color Arrow Room Doors/Door_red_6 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt @@ -1975,7 +1990,7 @@ Rhyme Room Entrance: id: Double Room Area Doors/Door_room_entry_stairs2 skip_location: True - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - room: The Tenacious panel: LEVEL (Black) @@ -1989,7 +2004,7 @@ - Color Arrow Room Doors/Door_orange_hider_2 - Color Arrow Room Doors/Door_orange_hider_3 location_name: Color Barriers - RED and YELLOW - group: Color Hunt Barriers + door_group: Color Hunt Barriers item_name: Color Hunt - Orange Barrier panels: - RED @@ -2150,7 +2165,7 @@ doors: Welcome Back: id: Entry Room Area Doors/Door_sizes - group: Welcome Back Doors + door_group: Welcome Back Doors panels: - SIZE (Small) - SIZE (Big) @@ -2404,6 +2419,7 @@ Painting Shortcut: painting_id: flower_painting_8 item_name: Starting Room - Flower Painting + item_group: Paintings skip_location: True panels: - room: First Second Third Fourth @@ -2416,7 +2432,7 @@ panel: FOURTH Green Barrier: id: Color Arrow Room Doors/Door_green_5 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt @@ -2470,7 +2486,7 @@ doors: Backside Door: id: Count Up Room Area Doors/Door_yellow_backside - group: Backside Doors + door_group: Backside Doors location_name: Courtyard - FIRST, SECOND, THIRD, FOURTH item_name: Courtyard - Backside Door panels: @@ -2491,7 +2507,7 @@ Progress Door: id: Doorway Room Doors/Door_white item_name: The Colorful - White Door - group: Colorful Doors + door_group: Colorful Doors location_name: The Colorful - White panels: - BEGIN @@ -2512,7 +2528,7 @@ id: Doorway Room Doors/Door_black item_name: The Colorful - Black Door location_name: The Colorful - Black - group: Colorful Doors + door_group: Colorful Doors panels: - FOUND The Colorful (Red): @@ -2532,7 +2548,7 @@ id: Doorway Room Doors/Door_red item_name: The Colorful - Red Door location_name: The Colorful - Red - group: Colorful Doors + door_group: Colorful Doors panels: - LOAF The Colorful (Yellow): @@ -2552,7 +2568,7 @@ id: Doorway Room Doors/Door_yellow item_name: The Colorful - Yellow Door location_name: The Colorful - Yellow - group: Colorful Doors + door_group: Colorful Doors panels: - CREAM The Colorful (Blue): @@ -2572,7 +2588,7 @@ id: Doorway Room Doors/Door_blue item_name: The Colorful - Blue Door location_name: The Colorful - Blue - group: Colorful Doors + door_group: Colorful Doors panels: - SUN The Colorful (Purple): @@ -2592,7 +2608,7 @@ id: Doorway Room Doors/Door_purple item_name: The Colorful - Purple Door location_name: The Colorful - Purple - group: Colorful Doors + door_group: Colorful Doors panels: - SPOON The Colorful (Orange): @@ -2612,7 +2628,7 @@ id: Doorway Room Doors/Door_orange item_name: The Colorful - Orange Door location_name: The Colorful - Orange - group: Colorful Doors + door_group: Colorful Doors panels: - LETTERS The Colorful (Green): @@ -2632,7 +2648,7 @@ id: Doorway Room Doors/Door_green item_name: The Colorful - Green Door location_name: The Colorful - Green - group: Colorful Doors + door_group: Colorful Doors panels: - WALLS The Colorful (Brown): @@ -2652,7 +2668,7 @@ id: Doorway Room Doors/Door_brown item_name: The Colorful - Brown Door location_name: The Colorful - Brown - group: Colorful Doors + door_group: Colorful Doors panels: - IRON The Colorful (Gray): @@ -2672,7 +2688,7 @@ id: Doorway Room Doors/Door_gray item_name: The Colorful - Gray Door location_name: The Colorful - Gray - group: Colorful Doors + door_group: Colorful Doors panels: - OBSTACLE The Colorful: @@ -2768,7 +2784,7 @@ doors: Shortcut to Starting Room: id: Entry Room Area Doors/Door_return_return - group: Welcome Back Doors + door_group: Welcome Back Doors include_reduce: True panels: - WELCOME BACK @@ -2793,7 +2809,7 @@ doors: Shortcut to Hedge Maze: id: Maze Area Doors/Door_strays_maze - group: Hedge Maze Doors + door_group: Hedge Maze Doors panels: - STRAYS paintings: @@ -2916,14 +2932,14 @@ - UNCOVER Blue Barrier: id: Color Arrow Room Doors/Door_blue_3 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt panel: BLUE Orange Barrier: id: Color Arrow Room Doors/Door_orange_3 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt @@ -2931,6 +2947,7 @@ Initiated Entrance: id: Red Blue Purple Room Area Doors/Door_locked_knocked item_name: The Initiated - Entrance + item_group: Achievement Room Entrances panels: - OXEN # These would be more appropriate in Champion's Rest, but as currently @@ -2940,7 +2957,7 @@ id: Color Arrow Room Doors/Door_green_hider_1 location_name: Color Barriers - BLUE and YELLOW item_name: Color Hunt - Green Barrier - group: Color Hunt Barriers + door_group: Color Hunt Barriers panels: - BLUE - room: Directional Gallery @@ -2952,7 +2969,7 @@ - Color Arrow Room Doors/Door_purple_hider_3 location_name: Color Barriers - RED and BLUE item_name: Color Hunt - Purple Barrier - group: Color Hunt Barriers + door_group: Color Hunt Barriers panels: - BLUE - room: Orange Tower Third Floor @@ -2972,6 +2989,7 @@ panel: PURPLE Eight Door: id: Red Blue Purple Room Area Doors/Door_a_strands2 + item_group: Achievement Room Entrances skip_location: True panels: - room: The Incomparable @@ -3189,7 +3207,8 @@ doors: Color Hallways Entrance: id: Appendix Room Area Doors/Door_hello_hi - group: Entrance to The Traveled + door_group: Entrance to The Traveled + item_group: Achievement Room Entrances panels: - HELLO Color Hallways: @@ -3305,17 +3324,20 @@ Bold Entrance: id: Red Blue Purple Room Area Doors/Door_unopened_open item_name: The Bold - Entrance + item_group: Achievement Room Entrances panels: - UNOPEN Painting Shortcut: painting_id: pencil_painting6 skip_location: True item_name: Starting Room - Pencil Painting + item_group: Paintings panels: - UNOPEN Steady Entrance: id: Rock Room Doors/Door_2 item_name: The Steady - Entrance + item_group: Achievement Room Entrances panels: - BEGIN Lilac Entrance: @@ -3536,6 +3558,7 @@ Undeterred Entrance: id: Red Blue Purple Room Area Doors/Door_pen_open item_name: The Undeterred - Entrance + item_group: Achievement Room Entrances panels: - PEN Painting Shortcut: @@ -3544,11 +3567,13 @@ - arrows_painting3 skip_location: True item_name: Starting Room - Blue Painting + item_group: Paintings panels: - PEN Green Painting: painting_id: maze_painting_3 skip_location: True + item_group: Paintings panels: - FOUR Twos: @@ -3556,6 +3581,7 @@ - Count Up Room Area Doors/Door_two_hider - Count Up Room Area Doors/Door_two_hider_2 include_reduce: True + item_group: Numbers panels: - ONE Threes: @@ -3565,6 +3591,7 @@ - Count Up Room Area Doors/Door_three_hider_3 location_name: Twos include_reduce: True + item_group: Numbers panels: - TWO (1) - TWO (2) @@ -3583,6 +3610,7 @@ - Count Up Room Area Doors/Door_four_hider_3 - Count Up Room Area Doors/Door_four_hider_4 skip_location: True + item_group: Numbers panels: - THREE (1) - THREE (2) @@ -3594,6 +3622,7 @@ - Count Up Room Area Doors/Door_five_hider_5 location_name: Fours item_name: Number Hunt - Fives + item_group: Numbers include_reduce: True panels: - FOUR @@ -3606,6 +3635,7 @@ Challenge Entrance: id: Count Up Room Area Doors/Door_zero_unlocked item_name: Number Hunt - Challenge Entrance + item_group: Achievement Room Entrances panels: - ZERO paintings: @@ -3752,7 +3782,7 @@ doors: Door to Directional Gallery: id: Count Up Room Area Doors/Door_five_unlocked - group: Directional Gallery Doors + door_group: Directional Gallery Doors skip_location: True panels: - FIVE @@ -3766,6 +3796,7 @@ - Count Up Room Area Doors/Door_six_hider_6 painting_id: pencil_painting3 # See note in Outside The Bold location_name: Fives + item_group: Numbers include_reduce: True panels: - FIVE @@ -3788,6 +3819,7 @@ - Count Up Room Area Doors/Door_seven_hider_6 - Count Up Room Area Doors/Door_seven_hider_7 location_name: Sixes + item_group: Numbers include_reduce: True panels: - SIX @@ -3813,6 +3845,7 @@ - Count Up Room Area Doors/Door_eight_hider_7 - Count Up Room Area Doors/Door_eight_hider_8 location_name: Sevens + item_group: Numbers include_reduce: True panels: - SEVEN @@ -3840,6 +3873,7 @@ - Count Up Room Area Doors/Door_nine_hider_8 - Count Up Room Area Doors/Door_nine_hider_9 location_name: Eights + item_group: Numbers include_reduce: True panels: - EIGHT @@ -3862,6 +3896,7 @@ id: Count Up Room Area Doors/Door_zero_hider_2 location_name: Nines item_name: Outside The Undeterred - Zero Door + item_group: Numbers include_reduce: True panels: - NINE @@ -4030,13 +4065,13 @@ doors: Shortcut to The Undeterred: id: Count Up Room Area Doors/Door_return_double - group: Directional Gallery Doors + door_group: Directional Gallery Doors panels: - TURN - LEARN Yellow Barrier: id: Color Arrow Room Doors/Door_yellow_4 - group: Color Hunt Barriers + door_group: Color Hunt Barriers skip_location: True panels: - room: Color Hunt @@ -4231,11 +4266,12 @@ doors: Entrance: id: Red Blue Purple Room Area Doors/Door_middle_middle + item_group: Achievement Room Entrances panels: - MIDDLE Backside Door: id: Red Blue Purple Room Area Doors/Door_locked_knocked2 # yeah... - group: Backside Doors + door_group: Backside Doors panels: - FARTHER East Entrance: @@ -5223,7 +5259,7 @@ - Ceiling Room Doors/Door_blue - Ceiling Room Doors/Door_blue2 location_name: The Artistic - Smiley and Panda - group: Artistic Doors + door_group: Artistic Doors panels: - FINE - BLADE @@ -5333,7 +5369,7 @@ - Ceiling Room Doors/Door_red - Ceiling Room Doors/Door_red2 location_name: The Artistic - Panda and Lattice - group: Artistic Doors + door_group: Artistic Doors panels: - EYE (Top) - EYE (Bottom) @@ -5444,7 +5480,7 @@ - Ceiling Room Doors/Door_black - Ceiling Room Doors/Door_black2 location_name: The Artistic - Lattice and Apple - group: Artistic Doors + door_group: Artistic Doors panels: - POSH - MALL @@ -5557,7 +5593,7 @@ - Ceiling Room Doors/Door_yellow - Ceiling Room Doors/Door_yellow2 location_name: The Artistic - Apple and Smiley - group: Artistic Doors + door_group: Artistic Doors panels: - SPRIG - RELEASES @@ -5721,7 +5757,7 @@ doors: Exit: id: Count Up Room Area Doors/Door_near_near - group: Crossroads Doors + door_group: Crossroads Doors panels: - NEAR paintings: @@ -5762,6 +5798,7 @@ Wondrous Entrance: id: Red Blue Purple Room Area Doors/Door_wonderland item_name: The Wondrous - Entrance + item_group: Achievement Room Entrances panels: - SHRINK The Wondrous (Doorknob): @@ -5782,6 +5819,7 @@ - arrows_painting2 skip_location: True item_name: Starting Room - Symmetry Painting + item_group: Paintings panels: - room: Outside The Wondrous panel: SHRINK @@ -5886,6 +5924,7 @@ doors: Exit: id: Red Blue Purple Room Area Doors/Door_wonderland_exit + item_group: Paintings painting_id: arrows_painting_9 include_reduce: True panels: @@ -5955,7 +5994,7 @@ Exit: id: Red Blue Purple Room Area Doors/Door_room_3 location_name: Hallway Room - Second Room - group: Hallway Room Doors + door_group: Hallway Room Doors panels: - WISE - CLOCK @@ -5992,7 +6031,7 @@ Exit: id: Red Blue Purple Room Area Doors/Door_room_4 location_name: Hallway Room - Third Room - group: Hallway Room Doors + door_group: Hallway Room Doors panels: - TRANCE - FORM @@ -6014,7 +6053,7 @@ id: - Red Blue Purple Room Area Doors/Door_room_5 - Red Blue Purple Room Area Doors/Door_room_6 # this is the connection to The Artistic - group: Hallway Room Doors + door_group: Hallway Room Doors location_name: Hallway Room - Fourth Room panels: - WHEEL @@ -6082,6 +6121,7 @@ Wanderer Entrance: id: Tower Room Area Doors/Door_wanderer_entrance item_name: The Wanderer - Entrance + item_group: Achievement Room Entrances panels: - WANDERLUST Tower Entrance: @@ -6222,6 +6262,7 @@ id: Tower Room Area Doors/Door_painting_exit include_reduce: True item_name: Orange Tower Fifth Floor - Quadruple Intersection + item_group: Achievement Room Entrances panels: - ORDER paintings: @@ -6417,7 +6458,7 @@ - Double Room Area Doors/Door_room_3a - Double Room Area Doors/Door_room_3bc skip_location: True - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - SCHEME - FANTASY @@ -6518,7 +6559,7 @@ Exit: id: Double Room Area Doors/Door_room_exit location_name: Rhyme Room (Cross) - Exit Puzzles - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - PLUMP - BOUNCE @@ -6581,7 +6622,7 @@ - Double Room Area Doors/Door_room_2b - Double Room Area Doors/Door_room_3b location_name: Rhyme Room - Circle/Smiley Wall - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - BIRD - LETTER @@ -6664,7 +6705,7 @@ - Double Room Area Doors/Door_room_2a - Double Room Area Doors/Door_room_1c location_name: Rhyme Room - Circle/Looped Square Wall - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - WALKED - OBSTRUCTED @@ -6683,7 +6724,7 @@ - Double Room Area Doors/Door_room_1a - Double Room Area Doors/Door_room_5a location_name: Rhyme Room - Cross/Looped Square Wall - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - SKIES - SWELL @@ -6702,7 +6743,7 @@ - Double Room Area Doors/Door_room_1b - Double Room Area Doors/Door_room_4b location_name: Rhyme Room - Target/Looped Square Wall - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - PENNED - CLIMB @@ -6765,7 +6806,7 @@ Door to Cross: id: Double Room Area Doors/Door_room_4a location_name: Rhyme Room (Target) - Puzzles Toward Cross - group: Rhyme Room Doors + door_group: Rhyme Room Doors panels: - PISTOL - GEM @@ -7016,6 +7057,7 @@ Wise Entrance: id: Clock Room Area Doors/Door_time_start item_name: The Wise - Entrance + item_group: Achievement Room Entrances panels: - KITTEN - CAT @@ -7269,6 +7311,7 @@ Scientific Entrance: id: Red Blue Purple Room Area Doors/Door_chemistry_lab item_name: The Scientific - Entrance + item_group: Achievement Room Entrances panels: - OPEN The Scientific: @@ -7704,5 +7747,6 @@ doors: Welcome Door: id: Entry Room Area Doors/Door_challenge_challenge + item_group: Achievement Room Entrances panels: - WELCOME diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 49ea60df4183b9c7a59c4be54f03a95591668441..3bd8ff5a8b5fcd7ccfd60d32f34da3728e62c2d2 100644 GIT binary patch delta 17352 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 Date: Thu, 21 Mar 2024 11:50:07 -0400 Subject: [PATCH 13/74] TUNIC: Shuffle Ladders option (#2919) --- docs/CODEOWNERS | 2 +- worlds/tunic/__init__.py | 96 ++- worlds/tunic/docs/en_TUNIC.md | 2 +- worlds/tunic/er_data.py | 1100 +++++++++++++++++---------------- worlds/tunic/er_rules.py | 1029 ++++++++++++++++++++++-------- worlds/tunic/er_scripts.py | 178 ++---- worlds/tunic/items.py | 26 +- worlds/tunic/locations.py | 240 ++++--- worlds/tunic/options.py | 43 +- worlds/tunic/regions.py | 6 +- worlds/tunic/rules.py | 19 +- 11 files changed, 1672 insertions(+), 1069 deletions(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 64407356625b..a67d5883007e 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -177,7 +177,7 @@ /worlds/tloz/ @Rosalie-A @t3hf1gm3nt # TUNIC -/worlds/tunic/ @silent-destroyer +/worlds/tunic/ @silent-destroyer @ScipioWright # Undertale /worlds/undertale/ @jonloveslegos diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index c4b1bbec8ea3..3220c6c9347d 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -7,6 +7,7 @@ from .er_rules import set_er_location_rules from .regions import tunic_regions from .er_scripts import create_er_regions +from .er_data import portal_mapping from .options import TunicOptions from worlds.AutoWorld import WebWorld, World from decimal import Decimal, ROUND_HALF_UP @@ -44,7 +45,6 @@ class TunicWorld(World): game = "TUNIC" web = TunicWeb() - data_version = 2 options: TunicOptions options_dataclass = TunicOptions item_name_groups = item_name_groups @@ -72,6 +72,7 @@ def generate_early(self) -> None: self.options.maskless.value = passthrough["maskless"] self.options.hexagon_quest.value = passthrough["hexagon_quest"] self.options.entrance_rando.value = passthrough["entrance_rando"] + self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] def create_item(self, name: str) -> TunicItem: item_data = item_table[name] @@ -119,27 +120,46 @@ def create_items(self) -> None: items_to_create[rgb_hexagon] = 0 items_to_create[gold_hexagon] -= 3 + # Filler items in the item pool + available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and + item_table[filler].classification == ItemClassification.filler] + + # Remove filler to make room for other items + def remove_filler(amount: int): + for _ in range(0, amount): + if not available_filler: + fill = "Fool Trap" + else: + fill = self.random.choice(available_filler) + if items_to_create[fill] == 0: + raise Exception("No filler items left to accommodate options selected. Turn down fool trap amount.") + items_to_create[fill] -= 1 + if items_to_create[fill] == 0: + available_filler.remove(fill) + + if self.options.shuffle_ladders: + ladder_count = 0 + for item_name, item_data in item_table.items(): + if item_data.item_group == "ladders": + items_to_create[item_name] = 1 + ladder_count += 1 + remove_filler(ladder_count) + if hexagon_quest: # Calculate number of hexagons in item pool hexagon_goal = self.options.hexagon_goal extra_hexagons = self.options.extra_hexagon_percentage items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP)) - + # Replace pages and normal hexagons with filler for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)): - items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item] + filler_name = self.get_filler_item_name() + items_to_create[filler_name] += items_to_create[replaced_item] + if items_to_create[filler_name] >= 1 and filler_name not in available_filler: + available_filler.append(filler_name) items_to_create[replaced_item] = 0 - # Filler items that are still in the item pool to swap out - available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and - item_table[filler].classification == ItemClassification.filler] - - # Remove filler to make room for extra hexagons - for i in range(0, items_to_create[gold_hexagon]): - fill = self.random.choice(available_filler) - items_to_create[fill] -= 1 - if items_to_create[fill] == 0: - available_filler.remove(fill) + remove_filler(items_to_create[gold_hexagon]) if self.options.maskless: mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player) @@ -147,8 +167,8 @@ def create_items(self) -> None: items_to_create["Scavenger Mask"] = 0 if self.options.lanternless: - mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) - tunic_items.append(mask_item) + lantern_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player) + tunic_items.append(lantern_item) items_to_create["Lantern"] = 0 for item, quantity in items_to_create.items(): @@ -172,15 +192,16 @@ def create_regions(self) -> None: self.ability_unlocks["Pages 24-25 (Prayer)"] = passthrough["Hexagon Quest Prayer"] self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"] self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"] - - if self.options.entrance_rando: - portal_pairs, portal_hints = create_er_regions(self) - for portal1, portal2 in portal_pairs.items(): - self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() - - self.er_portal_hints = portal_hints + # ladder rando uses ER with vanilla connections, so that we're not managing more rules files + if self.options.entrance_rando or self.options.shuffle_ladders: + portal_pairs = create_er_regions(self) + if self.options.entrance_rando: + # these get interpreted by the game to tell it which entrances to connect + for portal1, portal2 in portal_pairs.items(): + self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() else: + # for non-ER, non-ladders for region_name in tunic_regions: region = Region(region_name, self.player, self.multiworld) self.multiworld.regions.append(region) @@ -201,7 +222,7 @@ def create_regions(self) -> None: victory_region.locations.append(victory_location) def set_rules(self) -> None: - if self.options.entrance_rando: + if self.options.entrance_rando or self.options.shuffle_ladders: set_er_location_rules(self, self.ability_unlocks) else: set_region_rules(self, self.ability_unlocks) @@ -212,7 +233,31 @@ def get_filler_item_name(self) -> str: def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): if self.options.entrance_rando: - hint_data[self.player] = self.er_portal_hints + hint_data.update({self.player: {}}) + # all state seems to have efficient paths + all_state = self.multiworld.get_all_state(True) + all_state.update_reachable_regions(self.player) + paths = all_state.path + portal_names = [portal.name for portal in portal_mapping] + for location in self.multiworld.get_locations(self.player): + # skipping event locations + if not location.address: + continue + path_to_loc = [] + previous_name = "placeholder" + name, connection = paths[location.parent_region] + while connection != ("Menu", None): + name, connection = connection + # for LS entrances, we just want to give the portal name + if "(LS)" in name: + name, _ = name.split(" (LS) ") + # was getting some cases like Library Grave -> Library Grave -> other place + if name in portal_names and name != previous_name: + previous_name = name + path_to_loc.append(name) + hint_text = " -> ".join(reversed(path_to_loc)) + if hint_text: + hint_data[self.player][location.address] = hint_text def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { @@ -226,7 +271,8 @@ def fill_slot_data(self) -> Dict[str, Any]: "logic_rules": self.options.logic_rules.value, "lanternless": self.options.lanternless.value, "maskless": self.options.maskless.value, - "entrance_rando": bool(self.options.entrance_rando.value), + "entrance_rando": int(bool(self.options.entrance_rando.value)), + "shuffle_ladders": self.options.shuffle_ladders.value, "Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"], "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index 5921d0ed092d..ad328999ac0c 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -67,7 +67,7 @@ For the Entrance Randomizer: Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, icebolt, and progressive sword. ## What location groups are there? -Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group. +Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), shop, bosses (for the bosses with checks associated with them), hero relic (for the 6 hero grave checks), and ladders (for the ladder items when you have shuffle ladders enabled). ## Is Connection Plando supported? Yes. The host needs to enable it in their `host.yaml`, and the player's yaml needs to contain a plando_connections block. diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 8d8db426f67d..d850a06dfa78 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -5,505 +5,509 @@ class Portal(NamedTuple): name: str # human-readable name region: str # AP region - destination: str # vanilla destination scene and tag + destination: str # vanilla destination scene + tag: str # vanilla tag def scene(self) -> str: # the actual scene name in Tunic return tunic_er_regions[self.region].game_scene def scene_destination(self) -> str: # full, nonchanging name to interpret by the mod - return self.scene() + ", " + self.destination + return self.scene() + ", " + self.destination + self.tag + + def destination_scene(self) -> str: # the vanilla connection + return self.destination + ", " + self.scene() + self.tag portal_mapping: List[Portal] = [ Portal(name="Stick House Entrance", region="Overworld", - destination="Sword Cave_"), + destination="Sword Cave", tag="_"), Portal(name="Windmill Entrance", region="Overworld", - destination="Windmill_"), - Portal(name="Well Ladder Entrance", region="Overworld", - destination="Sewer_entrance"), + destination="Windmill", tag="_"), + Portal(name="Well Ladder Entrance", region="Overworld Well Ladder", + destination="Sewer", tag="_entrance"), Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail", - destination="Sewer_west_aqueduct"), + destination="Sewer", tag="_west_aqueduct"), Portal(name="Old House Door Entrance", region="Overworld Old House Door", - destination="Overworld Interiors_house"), + destination="Overworld Interiors", tag="_house"), Portal(name="Old House Waterfall Entrance", region="Overworld", - destination="Overworld Interiors_under_checkpoint"), + destination="Overworld Interiors", tag="_under_checkpoint"), Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail", - destination="Furnace_gyro_upper_north"), + destination="Furnace", tag="_gyro_upper_north"), Portal(name="Entrance to Furnace under Windmill", region="Overworld", - destination="Furnace_gyro_upper_east"), + destination="Furnace", tag="_gyro_upper_east"), Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace", - destination="Furnace_gyro_west"), - Portal(name="Entrance to Furnace from Beach", region="Overworld", - destination="Furnace_gyro_lower"), - Portal(name="Caustic Light Cave Entrance", region="Overworld", - destination="Overworld Cave_"), + destination="Furnace", tag="_gyro_west"), + Portal(name="Entrance to Furnace from Beach", region="Overworld Tunnel Turret", + destination="Furnace", tag="_gyro_lower"), + Portal(name="Caustic Light Cave Entrance", region="Overworld Swamp Lower Entry", + destination="Overworld Cave", tag="_"), Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry", - destination="Swamp Redux 2_wall"), - Portal(name="Swamp Lower Entrance", region="Overworld", - destination="Swamp Redux 2_conduit"), - Portal(name="Ruined Passage Not-Door Entrance", region="Overworld", - destination="Ruins Passage_east"), + destination="Swamp Redux 2", tag="_wall"), + Portal(name="Swamp Lower Entrance", region="Overworld Swamp Lower Entry", + destination="Swamp Redux 2", tag="_conduit"), + Portal(name="Ruined Passage Not-Door Entrance", region="After Ruined Passage", + destination="Ruins Passage", tag="_east"), Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door", - destination="Ruins Passage_west"), - Portal(name="Atoll Upper Entrance", region="Overworld", - destination="Atoll Redux_upper"), - Portal(name="Atoll Lower Entrance", region="Overworld", - destination="Atoll Redux_lower"), + destination="Ruins Passage", tag="_west"), + Portal(name="Atoll Upper Entrance", region="Overworld to Atoll Upper", + destination="Atoll Redux", tag="_upper"), + Portal(name="Atoll Lower Entrance", region="Overworld Beach", + destination="Atoll Redux", tag="_lower"), Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry", - destination="ShopSpecial_"), - Portal(name="Maze Cave Entrance", region="Overworld", - destination="Maze Room_"), - Portal(name="West Garden Entrance near Belltower", region="Overworld Belltower", - destination="Archipelagos Redux_upper"), + destination="ShopSpecial", tag="_"), + Portal(name="Maze Cave Entrance", region="Overworld Beach", + destination="Maze Room", tag="_"), + Portal(name="West Garden Entrance near Belltower", region="Overworld to West Garden Upper", + destination="Archipelagos Redux", tag="_upper"), Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace", - destination="Archipelagos Redux_lower"), + destination="Archipelagos Redux", tag="_lower"), Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry", - destination="Archipelagos Redux_lowest"), + destination="Archipelagos Redux", tag="_lowest"), Portal(name="Temple Door Entrance", region="Overworld Temple Door", - destination="Temple_main"), - Portal(name="Temple Rafters Entrance", region="Overworld", - destination="Temple_rafters"), + destination="Temple", tag="_main"), + Portal(name="Temple Rafters Entrance", region="Overworld after Temple Rafters", + destination="Temple", tag="_rafters"), Portal(name="Ruined Shop Entrance", region="Overworld", - destination="Ruined Shop_"), - Portal(name="Patrol Cave Entrance", region="Overworld", - destination="PatrolCave_"), - Portal(name="Hourglass Cave Entrance", region="Overworld", - destination="Town Basement_beach"), + destination="Ruined Shop", tag="_"), + Portal(name="Patrol Cave Entrance", region="Overworld at Patrol Cave", + destination="PatrolCave", tag="_"), + Portal(name="Hourglass Cave Entrance", region="Overworld Beach", + destination="Town Basement", tag="_beach"), Portal(name="Changing Room Entrance", region="Overworld", - destination="Changing Room_"), + destination="Changing Room", tag="_"), Portal(name="Cube Cave Entrance", region="Overworld", - destination="CubeRoom_"), - Portal(name="Stairs from Overworld to Mountain", region="Overworld", - destination="Mountain_"), - Portal(name="Overworld to Fortress", region="Overworld", - destination="Fortress Courtyard_"), + destination="CubeRoom", tag="_"), + Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld", + destination="Mountain", tag="_"), + Portal(name="Overworld to Fortress", region="East Overworld", + destination="Fortress Courtyard", tag="_"), Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door", - destination="Town_FiligreeRoom_"), + destination="Town_FiligreeRoom", tag="_"), Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door", - destination="EastFiligreeCache_"), - Portal(name="Overworld to Quarry Connector", region="Overworld", - destination="Darkwoods Tunnel_"), + destination="EastFiligreeCache", tag="_"), + Portal(name="Overworld to Quarry Connector", region="Overworld Quarry Entry", + destination="Darkwoods Tunnel", tag="_"), Portal(name="Dark Tomb Main Entrance", region="Overworld", - destination="Crypt Redux_"), - Portal(name="Overworld to Forest Belltower", region="Overworld", - destination="Forest Belltower_"), + destination="Crypt Redux", tag="_"), + Portal(name="Overworld to Forest Belltower", region="East Overworld", + destination="Forest Belltower", tag="_"), Portal(name="Town to Far Shore", region="Overworld Town Portal", - destination="Transit_teleporter_town"), + destination="Transit", tag="_teleporter_town"), Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal", - destination="Transit_teleporter_starting island"), + destination="Transit", tag="_teleporter_starting island"), Portal(name="Secret Gathering Place Entrance", region="Overworld", - destination="Waterfall_"), + destination="Waterfall", tag="_"), Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Windmill Exit", region="Windmill", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Windmill Shop", region="Windmill", - destination="Shop_"), + destination="Shop", tag="_"), Portal(name="Old House Door Exit", region="Old House Front", - destination="Overworld Redux_house"), + destination="Overworld Redux", tag="_house"), Portal(name="Old House to Glyph Tower", region="Old House Front", - destination="g_elements_"), + destination="g_elements", tag="_"), Portal(name="Old House Waterfall Exit", region="Old House Back", - destination="Overworld Redux_under_checkpoint"), + destination="Overworld Redux", tag="_under_checkpoint"), Portal(name="Glyph Tower Exit", region="Relic Tower", - destination="Overworld Interiors_"), + destination="Overworld Interiors", tag="_"), Portal(name="Changing Room Exit", region="Changing Room", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Fountain HC Room Exit", region="Fountain Cross Room", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Cube Cave Exit", region="Cube Cave", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Guard Patrol Cave Exit", region="Patrol Cave", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Ruined Shop Exit", region="Ruined Shop", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Furnace Exit towards Well", region="Furnace Fuse", - destination="Overworld Redux_gyro_upper_north"), + destination="Overworld Redux", tag="_gyro_upper_north"), Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path", - destination="Crypt Redux_"), + destination="Crypt Redux", tag="_"), Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path", - destination="Overworld Redux_gyro_west"), + destination="Overworld Redux", tag="_gyro_west"), Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area", - destination="Overworld Redux_gyro_lower"), + destination="Overworld Redux", tag="_gyro_lower"), Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area", - destination="Overworld Redux_gyro_upper_east"), + destination="Overworld Redux", tag="_gyro_upper_east"), Portal(name="Stick House Exit", region="Stick House", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage", - destination="Overworld Redux_east"), + destination="Overworld Redux", tag="_east"), Portal(name="Ruined Passage Door Exit", region="Ruined Passage", - destination="Overworld Redux_west"), + destination="Overworld Redux", tag="_west"), Portal(name="Southeast HC Room Exit", region="Southeast Cross Room", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Maze Cave Exit", region="Maze Cave", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Hourglass Cave Exit", region="Hourglass Cave", - destination="Overworld Redux_beach"), + destination="Overworld Redux", tag="_beach"), Portal(name="Special Shop Exit", region="Special Shop", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters", - destination="Overworld Redux_rafters"), + destination="Overworld Redux", tag="_rafters"), Portal(name="Temple Door Exit", region="Sealed Temple", - destination="Overworld Redux_main"), + destination="Overworld Redux", tag="_main"), - Portal(name="Well Ladder Exit", region="Beneath the Well Front", - destination="Overworld Redux_entrance"), + Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit", + destination="Overworld Redux", tag="_entrance"), Portal(name="Well to Well Boss", region="Beneath the Well Back", - destination="Sewer_Boss_"), + destination="Sewer_Boss", tag="_"), Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", - destination="Overworld Redux_west_aqueduct"), + destination="Overworld Redux", tag="_west_aqueduct"), Portal(name="Well Boss to Well", region="Well Boss", - destination="Sewer_"), + destination="Sewer", tag="_"), Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", - destination="Crypt Redux_"), + destination="Crypt Redux", tag="_"), Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", - destination="Furnace_"), + destination="Furnace", tag="_"), Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", - destination="Sewer_Boss_"), + destination="Sewer_Boss", tag="_"), Portal(name="West Garden Exit near Hero's Grave", region="West Garden", - destination="Overworld Redux_lower"), + destination="Overworld Redux", tag="_lower"), Portal(name="West Garden to Magic Dagger House", region="West Garden", - destination="archipelagos_house_"), + destination="archipelagos_house", tag="_"), Portal(name="West Garden Exit after Boss", region="West Garden after Boss", - destination="Overworld Redux_upper"), + destination="Overworld Redux", tag="_upper"), Portal(name="West Garden Shop", region="West Garden", - destination="Shop_"), - Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit", - destination="Overworld Redux_lowest"), - Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave", - destination="RelicVoid_teleporter_relic plinth"), + destination="Shop", tag="_"), + Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", + destination="Overworld Redux", tag="_lowest"), + Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="West Garden to Far Shore", region="West Garden Portal", - destination="Transit_teleporter_archipelagos_teleporter"), + destination="Transit", tag="_teleporter_archipelagos_teleporter"), Portal(name="Magic Dagger House Exit", region="Magic Dagger House", - destination="Archipelagos Redux_"), + destination="Archipelagos Redux", tag="_"), Portal(name="Atoll Upper Exit", region="Ruined Atoll", - destination="Overworld Redux_upper"), + destination="Overworld Redux", tag="_upper"), Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area", - destination="Overworld Redux_lower"), + destination="Overworld Redux", tag="_lower"), Portal(name="Atoll Shop", region="Ruined Atoll", - destination="Shop_"), + destination="Shop", tag="_"), Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal", - destination="Transit_teleporter_atoll"), + destination="Transit", tag="_teleporter_atoll"), Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue", - destination="Library Exterior_"), - Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll", - destination="Frog Stairs_eye"), + destination="Library Exterior", tag="_"), + Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye", + destination="Frog Stairs", tag="_eye"), Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", - destination="Frog Stairs_mouth"), - - Portal(name="Frog Stairs Eye Exit", region="Frog's Domain Entry", - destination="Atoll Redux_eye"), - Portal(name="Frog Stairs Mouth Exit", region="Frog's Domain Entry", - destination="Atoll Redux_mouth"), - Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog's Domain Entry", - destination="frog cave main_Entrance"), - Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog's Domain Entry", - destination="frog cave main_Exit"), - - Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain", - destination="Frog Stairs_Entrance"), + destination="Frog Stairs", tag="_mouth"), + + Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", + destination="Atoll Redux", tag="_eye"), + Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", + destination="Atoll Redux", tag="_mouth"), + Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain", + destination="frog cave main", tag="_Entrance"), + Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", + destination="frog cave main", tag="_Exit"), + + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", + destination="Frog Stairs", tag="_Entrance"), Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", - destination="Frog Stairs_Exit"), + destination="Frog Stairs", tag="_Exit"), - Portal(name="Library Exterior Tree", region="Library Exterior Tree", - destination="Atoll Redux_"), - Portal(name="Library Exterior Ladder", region="Library Exterior Ladder", - destination="Library Hall_"), + Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", + destination="Atoll Redux", tag="_"), + Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", + destination="Library Hall", tag="_"), - Portal(name="Library Hall Bookshelf Exit", region="Library Hall", - destination="Library Exterior_"), - Portal(name="Library Hero's Grave", region="Library Hero's Grave", - destination="RelicVoid_teleporter_relic plinth"), - Portal(name="Library Hall to Rotunda", region="Library Hall", - destination="Library Rotunda_"), + Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", + destination="Library Exterior", tag="_"), + Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), + Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", + destination="Library Rotunda", tag="_"), - Portal(name="Library Rotunda Lower Exit", region="Library Rotunda", - destination="Library Hall_"), - Portal(name="Library Rotunda Upper Exit", region="Library Rotunda", - destination="Library Lab_"), + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", + destination="Library Hall", tag="_"), + Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", + destination="Library Lab", tag="_"), Portal(name="Library Lab to Rotunda", region="Library Lab Lower", - destination="Library Rotunda_"), + destination="Library Rotunda", tag="_"), Portal(name="Library to Far Shore", region="Library Portal", - destination="Transit_teleporter_library teleporter"), - Portal(name="Library Lab to Librarian Arena", region="Library Lab", - destination="Library Arena_"), + destination="Transit", tag="_teleporter_library teleporter"), + Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", + destination="Library Arena", tag="_"), Portal(name="Librarian Arena Exit", region="Library Arena", - destination="Library Lab_"), + destination="Library Lab", tag="_"), Portal(name="Forest to Belltower", region="East Forest", - destination="Forest Belltower_"), + destination="Forest Belltower", tag="_"), Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest", - destination="East Forest Redux Laddercave_lower"), + destination="East Forest Redux Laddercave", tag="_lower"), Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest", - destination="East Forest Redux Laddercave_gate"), + destination="East Forest Redux Laddercave", tag="_gate"), Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot", - destination="East Forest Redux Laddercave_upper"), + destination="East Forest Redux Laddercave", tag="_upper"), Portal(name="Forest to Far Shore", region="East Forest Portal", - destination="Transit_teleporter_forest teleporter"), - Portal(name="Forest Guard House 2 Lower Entrance", region="East Forest", - destination="East Forest Redux Interior_lower"), + destination="Transit", tag="_teleporter_forest teleporter"), + Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest", + destination="East Forest Redux Interior", tag="_lower"), Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest", - destination="East Forest Redux Interior_upper"), + destination="East Forest Redux Interior", tag="_upper"), Portal(name="Forest Grave Path Lower Entrance", region="East Forest", - destination="Sword Access_lower"), + destination="Sword Access", tag="_lower"), Portal(name="Forest Grave Path Upper Entrance", region="East Forest", - destination="Sword Access_upper"), + destination="Sword Access", tag="_upper"), Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West", - destination="East Forest Redux_upper"), + destination="East Forest Redux", tag="_upper"), Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West", - destination="East Forest Redux_lower"), + destination="East Forest Redux", tag="_lower"), Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East", - destination="East Forest Redux_gate"), + destination="East Forest Redux", tag="_gate"), Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East", - destination="Forest Boss Room_"), + destination="Forest Boss Room", tag="_"), Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper", - destination="East Forest Redux_upper"), + destination="East Forest Redux", tag="_upper"), Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main", - destination="East Forest Redux_lower"), + destination="East Forest Redux", tag="_lower"), Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave", - destination="RelicVoid_teleporter_relic plinth"), + destination="RelicVoid", tag="_teleporter_relic plinth"), - Portal(name="Guard House 2 Lower Exit", region="Guard House 2", - destination="East Forest Redux_lower"), - Portal(name="Guard House 2 Upper Exit", region="Guard House 2", - destination="East Forest Redux_upper"), + Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower", + destination="East Forest Redux", tag="_lower"), + Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper", + destination="East Forest Redux", tag="_upper"), Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room", - destination="East Forest Redux Laddercave_"), + destination="East Forest Redux Laddercave", tag="_"), Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room", - destination="Forest Belltower_"), + destination="Forest Belltower", tag="_"), Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main", - destination="Fortress Courtyard_"), + destination="Fortress Courtyard", tag="_"), Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower", - destination="East Forest Redux_"), + destination="East Forest Redux", tag="_"), Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper", - destination="Forest Boss Room_"), + destination="Forest Boss Room", tag="_"), Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard", - destination="Fortress Reliquary_Lower"), + destination="Fortress Reliquary", tag="_Lower"), Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper", - destination="Fortress Reliquary_Upper"), + destination="Fortress Reliquary", tag="_Upper"), Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard", - destination="Fortress Main_Big Door"), + destination="Fortress Main", tag="_Big Door"), Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper", - destination="Fortress East_"), - Portal(name="Fortress Courtyard to Beneath the Earth", region="Fortress Exterior near cave", - destination="Fortress Basement_"), + destination="Fortress East", tag="_"), + Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry", + destination="Fortress Basement", tag="_"), Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest", - destination="Forest Belltower_"), + destination="Forest Belltower", tag="_"), Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave", - destination="Shop_"), + destination="Shop", tag="_"), - Portal(name="Beneath the Earth to Fortress Interior", region="Beneath the Vault Back", - destination="Fortress Main_"), - Portal(name="Beneath the Earth to Fortress Courtyard", region="Beneath the Vault Front", - destination="Fortress Courtyard_"), + Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back", + destination="Fortress Main", tag="_"), + Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit", + destination="Fortress Courtyard", tag="_"), Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress", - destination="Fortress Courtyard_Big Door"), + destination="Fortress Courtyard", tag="_Big Door"), Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress", - destination="Fortress Basement_"), + destination="Fortress Basement", tag="_"), Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door", - destination="Fortress Arena_"), + destination="Fortress Arena", tag="_"), Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress", - destination="Shop_"), + destination="Shop", tag="_"), Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress", - destination="Fortress East_upper"), + destination="Fortress East", tag="_upper"), Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress", - destination="Fortress East_lower"), + destination="Fortress East", tag="_lower"), Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower", - destination="Fortress Main_lower"), + destination="Fortress Main", tag="_lower"), Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper", - destination="Fortress Courtyard_"), + destination="Fortress Courtyard", tag="_"), Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", - destination="Fortress Main_upper"), + destination="Fortress Main", tag="_upper"), Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path", - destination="Fortress Courtyard_Lower"), - Portal(name="Fortress Hero's Grave", region="Fortress Grave Path", - destination="RelicVoid_teleporter_relic plinth"), + destination="Fortress Courtyard", tag="_Lower"), + Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper", - destination="Fortress Courtyard_Upper"), - Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance", - destination="Dusty_"), + destination="Fortress Courtyard", tag="_Upper"), + Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region", + destination="Dusty", tag="_"), Portal(name="Dusty Exit", region="Fortress Leaf Piles", - destination="Fortress Reliquary_"), + destination="Fortress Reliquary", tag="_"), Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena", - destination="Fortress Main_"), + destination="Fortress Main", tag="_"), Portal(name="Fortress to Far Shore", region="Fortress Arena Portal", - destination="Transit_teleporter_spidertank"), + destination="Transit", tag="_teleporter_spidertank"), Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", - destination="Mountaintop_"), + destination="Mountaintop", tag="_"), Portal(name="Mountain to Quarry", region="Lower Mountain", - destination="Quarry Redux_"), + destination="Quarry Redux", tag="_"), Portal(name="Mountain to Overworld", region="Lower Mountain", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Top of the Mountain Exit", region="Top of the Mountain", - destination="Mountain_"), + destination="Mountain", tag="_"), Portal(name="Quarry Connector to Overworld", region="Quarry Connector", - destination="Overworld Redux_"), + destination="Overworld Redux", tag="_"), Portal(name="Quarry Connector to Quarry", region="Quarry Connector", - destination="Quarry Redux_"), + destination="Quarry Redux", tag="_"), Portal(name="Quarry to Overworld Exit", region="Quarry Entry", - destination="Darkwoods Tunnel_"), + destination="Darkwoods Tunnel", tag="_"), Portal(name="Quarry Shop", region="Quarry Entry", - destination="Shop_"), + destination="Shop", tag="_"), Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry", - destination="Monastery_front"), + destination="Monastery", tag="_front"), Portal(name="Quarry to Monastery Back", region="Monastery Rope", - destination="Monastery_back"), + destination="Monastery", tag="_back"), Portal(name="Quarry to Mountain", region="Quarry Back", - destination="Mountain_"), + destination="Mountain", tag="_"), Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door", - destination="ziggurat2020_0_"), + destination="ziggurat2020_0", tag="_"), Portal(name="Quarry to Far Shore", region="Quarry Portal", - destination="Transit_teleporter_quarry teleporter"), + destination="Transit", tag="_teleporter_quarry teleporter"), Portal(name="Monastery Rear Exit", region="Monastery Back", - destination="Quarry Redux_back"), + destination="Quarry Redux", tag="_back"), Portal(name="Monastery Front Exit", region="Monastery Front", - destination="Quarry Redux_front"), - Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave", - destination="RelicVoid_teleporter_relic plinth"), + destination="Quarry Redux", tag="_front"), + Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry", - destination="ziggurat2020_1_"), + destination="ziggurat2020_1", tag="_"), Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry", - destination="Quarry Redux_"), + destination="Quarry Redux", tag="_"), Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry", - destination="ziggurat2020_0_"), + destination="ziggurat2020_0", tag="_"), Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back", - destination="ziggurat2020_2_"), + destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top", - destination="ziggurat2020_1_"), + destination="ziggurat2020_1", tag="_"), Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", - destination="ziggurat2020_3_"), + destination="ziggurat2020_3", tag="_"), Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front", - destination="ziggurat2020_2_"), + destination="ziggurat2020_2", tag="_"), Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", - destination="ziggurat2020_FTRoom_"), + destination="ziggurat2020_FTRoom", tag="_"), Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit", - destination="ziggurat2020_3_"), + destination="ziggurat2020_3", tag="_"), Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal", - destination="Transit_teleporter_ziggurat teleporter"), + destination="Transit", tag="_teleporter_ziggurat teleporter"), - Portal(name="Swamp Lower Exit", region="Swamp", - destination="Overworld Redux_conduit"), - Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance", - destination="Cathedral Redux_main"), + Portal(name="Swamp Lower Exit", region="Swamp Front", + destination="Overworld Redux", tag="_conduit"), + Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance Region", + destination="Cathedral Redux", tag="_main"), Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room", - destination="Cathedral Redux_secret"), + destination="Cathedral Redux", tag="_secret"), Portal(name="Swamp to Gauntlet", region="Back of Swamp", - destination="Cathedral Arena_"), - Portal(name="Swamp Shop", region="Swamp", - destination="Shop_"), + destination="Cathedral Arena", tag="_"), + Portal(name="Swamp Shop", region="Swamp Front", + destination="Shop", tag="_"), Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area", - destination="Overworld Redux_wall"), - Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave", - destination="RelicVoid_teleporter_relic plinth"), + destination="Overworld Redux", tag="_wall"), + Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region", + destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Cathedral Main Exit", region="Cathedral", - destination="Swamp Redux 2_main"), + destination="Swamp Redux 2", tag="_main"), Portal(name="Cathedral Elevator", region="Cathedral", - destination="Cathedral Arena_"), + destination="Cathedral Arena", tag="_"), Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room", - destination="Swamp Redux 2_secret"), + destination="Swamp Redux 2", tag="_secret"), Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit", - destination="Swamp Redux 2_"), + destination="Swamp Redux 2", tag="_"), Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint", - destination="Cathedral Redux_"), + destination="Cathedral Redux", tag="_"), Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint", - destination="Shop_"), + destination="Shop", tag="_"), Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress", - destination="Fortress Reliquary_teleporter_relic plinth"), + destination="Fortress Reliquary", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry", - destination="Monastery_teleporter_relic plinth"), + destination="Monastery", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden", - destination="Archipelagos Redux_teleporter_relic plinth"), + destination="Archipelagos Redux", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest", - destination="Sword Access_teleporter_relic plinth"), + destination="Sword Access", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Library", region="Hero Relic - Library", - destination="Library Hall_teleporter_relic plinth"), + destination="Library Hall", tag="_teleporter_relic plinth"), Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp", - destination="Swamp Redux 2_teleporter_relic plinth"), - - Portal(name="Far Shore to West Garden", region="Far Shore to West Garden", - destination="Archipelagos Redux_teleporter_archipelagos_teleporter"), - Portal(name="Far Shore to Library", region="Far Shore to Library", - destination="Library Lab_teleporter_library teleporter"), - Portal(name="Far Shore to Quarry", region="Far Shore to Quarry", - destination="Quarry Redux_teleporter_quarry teleporter"), - Portal(name="Far Shore to East Forest", region="Far Shore to East Forest", - destination="East Forest Redux_teleporter_forest teleporter"), - Portal(name="Far Shore to Fortress", region="Far Shore to Fortress", - destination="Fortress Arena_teleporter_spidertank"), + destination="Swamp Redux 2", tag="_teleporter_relic plinth"), + + Portal(name="Far Shore to West Garden", region="Far Shore to West Garden Region", + destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter"), + Portal(name="Far Shore to Library", region="Far Shore to Library Region", + destination="Library Lab", tag="_teleporter_library teleporter"), + Portal(name="Far Shore to Quarry", region="Far Shore to Quarry Region", + destination="Quarry Redux", tag="_teleporter_quarry teleporter"), + Portal(name="Far Shore to East Forest", region="Far Shore to East Forest Region", + destination="East Forest Redux", tag="_teleporter_forest teleporter"), + Portal(name="Far Shore to Fortress", region="Far Shore to Fortress Region", + destination="Fortress Arena", tag="_teleporter_spidertank"), Portal(name="Far Shore to Atoll", region="Far Shore", - destination="Atoll Redux_teleporter_atoll"), + destination="Atoll Redux", tag="_teleporter_atoll"), Portal(name="Far Shore to Ziggurat", region="Far Shore", - destination="ziggurat2020_FTRoom_teleporter_ziggurat teleporter"), + destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter"), Portal(name="Far Shore to Heir", region="Far Shore", - destination="Spirit Arena_teleporter_spirit arena"), + destination="Spirit Arena", tag="_teleporter_spirit arena"), Portal(name="Far Shore to Town", region="Far Shore", - destination="Overworld Redux_teleporter_town"), - Portal(name="Far Shore to Spawn", region="Far Shore to Spawn", - destination="Overworld Redux_teleporter_starting island"), + destination="Overworld Redux", tag="_teleporter_town"), + Portal(name="Far Shore to Spawn", region="Far Shore to Spawn Region", + destination="Overworld Redux", tag="_teleporter_starting island"), Portal(name="Heir Arena Exit", region="Spirit Arena", - destination="Transit_teleporter_spirit arena"), + destination="Transit", tag="_teleporter_spirit arena"), Portal(name="Purgatory Bottom Exit", region="Purgatory", - destination="Purgatory_bottom"), + destination="Purgatory", tag="_bottom"), Portal(name="Purgatory Top Exit", region="Purgatory", - destination="Purgatory_top"), + destination="Purgatory", tag="_top"), ] @@ -520,32 +524,42 @@ class DeadEnd(IntEnum): # there's no dead ends that are only in unrestricted -class Hint(IntEnum): - none = 0 # big areas, empty hallways, etc. - region = 1 # at least one of the portals must not be a dead end - scene = 2 # multiple regions in the scene, so using region could mean no valid hints - special = 3 # for if there's a weird case of specific regions being viable - - # key is the AP region name. "Fake" in region info just means the mod won't receive that info at all tunic_er_regions: Dict[str, RegionInfo] = { "Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats), - "Overworld": RegionInfo("Overworld Redux"), - "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), + "Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area + "Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), # main overworld holy cross checks "Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest + "Overworld Belltower at Bell": RegionInfo("Overworld Redux"), # being able to ring the belltower, basically "Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot + "Overworld Swamp Lower Entry": RegionInfo("Overworld Redux"), # lower swamp entrance, rotating lights entrance + "After Ruined Passage": RegionInfo("Overworld Redux"), # just the door and chest + "Above Ruined Passage": RegionInfo("Overworld Redux"), # one ladder up from ruined passage + "East Overworld": RegionInfo("Overworld Redux"), # where the east forest and fortress entrances are "Overworld Special Shop Entry": RegionInfo("Overworld Redux"), # special shop entry spot + "Upper Overworld": RegionInfo("Overworld Redux"), # where the mountain stairs are + "Overworld above Quarry Entrance": RegionInfo("Overworld Redux"), # top of the ladder where the chest is + "Overworld after Temple Rafters": RegionInfo("Overworld Redux"), # the ledge after the rafters exit, before ladder + "Overworld Quarry Entry": RegionInfo("Overworld Redux"), # at the top of the ladder, to darkwoods + "Overworld after Envoy": RegionInfo("Overworld Redux"), # after the envoy on the thin bridge to quarry + "Overworld at Patrol Cave": RegionInfo("Overworld Redux"), # right at the patrol cave entrance + "Overworld above Patrol Cave": RegionInfo("Overworld Redux"), # where the hook is, and one ladder up from patrol "Overworld West Garden Laurels Entry": RegionInfo("Overworld Redux"), # west garden laurels entry - "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux", hint=Hint.region), - "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the tiny rail passageway + "Overworld to West Garden Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight + "Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest + "Overworld Well Ladder": RegionInfo("Overworld Redux"), # just the ladder entrance itself as a region + "Overworld Beach": RegionInfo("Overworld Redux"), # from the two turrets to invisble maze, and lower atoll entry + "Overworld Tunnel Turret": RegionInfo("Overworld Redux"), # the tunnel turret by the southwest beach ladder + "Overworld to Atoll Upper": RegionInfo("Overworld Redux"), # the little ledge before the ladder + "Overworld Well to Furnace Rail": RegionInfo("Overworld Redux"), # the rail hallway, bane of unrestricted logic "Overworld Ruined Passage Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal "Overworld Old House Door": RegionInfo("Overworld Redux"), # the too-small space between the door and the portal "Overworld Southeast Cross Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), + "Overworld Fountain Cross Door": RegionInfo("Overworld Redux"), # the small space between the door and the portal "Overworld Temple Door": RegionInfo("Overworld Redux"), # the small space betweeen the door and the portal - "Overworld Town Portal": RegionInfo("Overworld Redux"), - "Overworld Spawn Portal": RegionInfo("Overworld Redux"), - "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Overworld Town Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal + "Overworld Spawn Portal": RegionInfo("Overworld Redux"), # being able to go to or come from the portal + "Stick House": RegionInfo("Sword Cave", dead_end=DeadEnd.all_cats), "Windmill": RegionInfo("Windmill"), "Old House Back": RegionInfo("Overworld Interiors"), # part with the hc door "Old House Front": RegionInfo("Overworld Interiors"), # part with the bedroom @@ -553,87 +567,105 @@ class Hint(IntEnum): "Furnace Fuse": RegionInfo("Furnace"), # top of the furnace "Furnace Ladder Area": RegionInfo("Furnace"), # the two portals accessible by the ladder "Furnace Walking Path": RegionInfo("Furnace"), # dark tomb to west garden - "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Ruined Passage": RegionInfo("Ruins Passage", hint=Hint.region), - "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Sealed Temple": RegionInfo("Temple", hint=Hint.scene), - "Sealed Temple Rafters": RegionInfo("Temple", hint=Hint.scene), - "Forest Belltower Upper": RegionInfo("Forest Belltower", hint=Hint.region), + "Secret Gathering Place": RegionInfo("Waterfall", dead_end=DeadEnd.all_cats), + "Changing Room": RegionInfo("Changing Room", dead_end=DeadEnd.all_cats), + "Patrol Cave": RegionInfo("PatrolCave", dead_end=DeadEnd.all_cats), + "Ruined Shop": RegionInfo("Ruined Shop", dead_end=DeadEnd.all_cats), + "Ruined Passage": RegionInfo("Ruins Passage"), + "Special Shop": RegionInfo("ShopSpecial", dead_end=DeadEnd.all_cats), + "Caustic Light Cave": RegionInfo("Overworld Cave", dead_end=DeadEnd.all_cats), + "Maze Cave": RegionInfo("Maze Room", dead_end=DeadEnd.all_cats), + "Cube Cave": RegionInfo("CubeRoom", dead_end=DeadEnd.all_cats), + "Southeast Cross Room": RegionInfo("EastFiligreeCache", dead_end=DeadEnd.all_cats), + "Fountain Cross Room": RegionInfo("Town_FiligreeRoom", dead_end=DeadEnd.all_cats), + "Hourglass Cave": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats), + "Hourglass Cave Tower": RegionInfo("Town Basement", dead_end=DeadEnd.all_cats), # top of the tower + "Sealed Temple": RegionInfo("Temple"), + "Sealed Temple Rafters": RegionInfo("Temple"), + "Forest Belltower Upper": RegionInfo("Forest Belltower"), "Forest Belltower Main": RegionInfo("Forest Belltower"), "Forest Belltower Lower": RegionInfo("Forest Belltower"), "East Forest": RegionInfo("East Forest Redux"), "East Forest Dance Fox Spot": RegionInfo("East Forest Redux"), "East Forest Portal": RegionInfo("East Forest Redux"), + "Lower Forest": RegionInfo("East Forest Redux"), # bottom of the forest "Guard House 1 East": RegionInfo("East Forest Redux Laddercave"), "Guard House 1 West": RegionInfo("East Forest Redux Laddercave"), - "Guard House 2": RegionInfo("East Forest Redux Interior"), + "Guard House 2 Upper": RegionInfo("East Forest Redux Interior"), + "Guard House 2 Lower": RegionInfo("East Forest Redux Interior"), "Forest Boss Room": RegionInfo("Forest Boss Room"), "Forest Grave Path Main": RegionInfo("Sword Access"), "Forest Grave Path Upper": RegionInfo("Sword Access"), "Forest Grave Path by Grave": RegionInfo("Sword Access"), "Forest Hero's Grave": RegionInfo("Sword Access"), "Dark Tomb Entry Point": RegionInfo("Crypt Redux"), # both upper exits + "Dark Tomb Upper": RegionInfo("Crypt Redux"), # the part with the casket and the top of the ladder "Dark Tomb Main": RegionInfo("Crypt Redux"), "Dark Tomb Dark Exit": RegionInfo("Crypt Redux"), - "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"), # can laurels backwards - "Well Boss": RegionInfo("Sewer_Boss"), # can walk through (with bombs at least) - "Beneath the Well Front": RegionInfo("Sewer"), - "Beneath the Well Main": RegionInfo("Sewer"), - "Beneath the Well Back": RegionInfo("Sewer"), + "Dark Tomb Checkpoint": RegionInfo("Sewer_Boss"), + "Well Boss": RegionInfo("Sewer_Boss"), + "Beneath the Well Ladder Exit": RegionInfo("Sewer"), # just the ladder + "Beneath the Well Front": RegionInfo("Sewer"), # the front, to separate it from the weapon requirement in the mid + "Beneath the Well Main": RegionInfo("Sewer"), # the main section of it, requires a weapon + "Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests "West Garden": RegionInfo("Archipelagos Redux"), - "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats), "West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), - "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, hint=Hint.special), - "West Garden Laurels Exit": RegionInfo("Archipelagos Redux"), + "West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted), + "West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"), "West Garden after Boss": RegionInfo("Archipelagos Redux"), - "West Garden Hero's Grave": RegionInfo("Archipelagos Redux"), + "West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux"), "Ruined Atoll": RegionInfo("Atoll Redux"), "Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"), + "Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll "Ruined Atoll Frog Mouth": RegionInfo("Atoll Redux"), + "Ruined Atoll Frog Eye": RegionInfo("Atoll Redux"), "Ruined Atoll Portal": RegionInfo("Atoll Redux"), "Ruined Atoll Statue": RegionInfo("Atoll Redux"), - "Frog's Domain Entry": RegionInfo("Frog Stairs"), - "Frog's Domain": RegionInfo("frog cave main", hint=Hint.region), - "Frog's Domain Back": RegionInfo("frog cave main", hint=Hint.scene), - "Library Exterior Tree": RegionInfo("Library Exterior"), - "Library Exterior Ladder": RegionInfo("Library Exterior"), + "Frog Stairs Eye Exit": RegionInfo("Frog Stairs"), + "Frog Stairs Upper": RegionInfo("Frog Stairs"), + "Frog Stairs Lower": RegionInfo("Frog Stairs"), + "Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"), + "Frog's Domain Entry": RegionInfo("frog cave main"), + "Frog's Domain": RegionInfo("frog cave main"), + "Frog's Domain Back": RegionInfo("frog cave main"), + "Library Exterior Tree Region": RegionInfo("Library Exterior"), + "Library Exterior Ladder Region": RegionInfo("Library Exterior"), + "Library Hall Bookshelf": RegionInfo("Library Hall"), "Library Hall": RegionInfo("Library Hall"), - "Library Hero's Grave": RegionInfo("Library Hall"), + "Library Hero's Grave Region": RegionInfo("Library Hall"), + "Library Hall to Rotunda": RegionInfo("Library Hall"), + "Library Rotunda to Hall": RegionInfo("Library Rotunda"), "Library Rotunda": RegionInfo("Library Rotunda"), + "Library Rotunda to Lab": RegionInfo("Library Rotunda"), "Library Lab": RegionInfo("Library Lab"), "Library Lab Lower": RegionInfo("Library Lab"), "Library Portal": RegionInfo("Library Lab"), - "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Library Lab to Librarian": RegionInfo("Library Lab"), + "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats), "Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), "Fortress Exterior from Overworld": RegionInfo("Fortress Courtyard"), "Fortress Exterior near cave": RegionInfo("Fortress Courtyard"), # where the shop and beneath the earth entry are + "Beneath the Vault Entry": RegionInfo("Fortress Courtyard"), "Fortress Courtyard": RegionInfo("Fortress Courtyard"), "Fortress Courtyard Upper": RegionInfo("Fortress Courtyard"), - "Beneath the Vault Front": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla entry point - "Beneath the Vault Back": RegionInfo("Fortress Basement", hint=Hint.scene), # the vanilla exit point + "Beneath the Vault Ladder Exit": RegionInfo("Fortress Basement"), + "Beneath the Vault Front": RegionInfo("Fortress Basement"), # the vanilla entry point + "Beneath the Vault Back": RegionInfo("Fortress Basement"), # the vanilla exit point "Eastern Vault Fortress": RegionInfo("Fortress Main"), "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"), "Fortress East Shortcut Upper": RegionInfo("Fortress East"), "Fortress East Shortcut Lower": RegionInfo("Fortress East"), "Fortress Grave Path": RegionInfo("Fortress Reliquary"), - "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted, hint=Hint.region), - "Fortress Grave Path Dusty Entrance": RegionInfo("Fortress Reliquary"), - "Fortress Hero's Grave": RegionInfo("Fortress Reliquary"), - "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted), + "Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"), + "Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary"), + "Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats), "Fortress Arena": RegionInfo("Fortress Arena"), "Fortress Arena Portal": RegionInfo("Fortress Arena"), "Lower Mountain": RegionInfo("Mountain"), "Lower Mountain Stairs": RegionInfo("Mountain"), - "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Top of the Mountain": RegionInfo("Mountaintop", dead_end=DeadEnd.all_cats), "Quarry Connector": RegionInfo("Darkwoods Tunnel"), "Quarry Entry": RegionInfo("Quarry Redux"), "Quarry": RegionInfo("Quarry Redux"), @@ -642,9 +674,10 @@ class Hint(IntEnum): "Quarry Monastery Entry": RegionInfo("Quarry Redux"), "Monastery Front": RegionInfo("Monastery"), "Monastery Back": RegionInfo("Monastery"), - "Monastery Hero's Grave": RegionInfo("Monastery"), + "Monastery Hero's Grave Region": RegionInfo("Monastery"), "Monastery Rope": RegionInfo("Quarry Redux"), "Lower Quarry": RegionInfo("Quarry Redux"), + "Even Lower Quarry": RegionInfo("Quarry Redux"), "Lower Quarry Zig Door": RegionInfo("Quarry Redux"), "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), "Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), @@ -657,82 +690,61 @@ class Hint(IntEnum): "Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3"), # the door itself on the zig 3 side "Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom"), "Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"), - "Swamp": RegionInfo("Swamp Redux 2"), - "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), - "Swamp to Cathedral Main Entrance": RegionInfo("Swamp Redux 2"), + "Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south + "Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door + "Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door + "Swamp to Cathedral Treasure Room": RegionInfo("Swamp Redux 2"), # just the door + "Swamp to Cathedral Main Entrance Region": RegionInfo("Swamp Redux 2"), # just the door "Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance - "Swamp Hero's Grave": RegionInfo("Swamp Redux 2"), + "Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2"), "Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse "Cathedral": RegionInfo("Cathedral Redux"), - "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats), "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet Exit": RegionInfo("Cathedral Arena"), "Far Shore": RegionInfo("Transit"), - "Far Shore to Spawn": RegionInfo("Transit"), - "Far Shore to East Forest": RegionInfo("Transit"), - "Far Shore to Quarry": RegionInfo("Transit"), - "Far Shore to Fortress": RegionInfo("Transit"), - "Far Shore to Library": RegionInfo("Transit"), - "Far Shore to West Garden": RegionInfo("Transit"), - "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), - "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Far Shore to Spawn Region": RegionInfo("Transit"), + "Far Shore to East Forest Region": RegionInfo("Transit"), + "Far Shore to Quarry Region": RegionInfo("Transit"), + "Far Shore to Fortress Region": RegionInfo("Transit"), + "Far Shore to Library Region": RegionInfo("Transit"), + "Far Shore to West Garden Region": RegionInfo("Transit"), + "Hero Relic - Fortress": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), + "Hero Relic - Quarry": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), + "Hero Relic - West Garden": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), + "Hero Relic - East Forest": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), + "Hero Relic - Library": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), + "Hero Relic - Swamp": RegionInfo("RelicVoid", dead_end=DeadEnd.all_cats), "Purgatory": RegionInfo("Purgatory"), "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), - "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, hint=Hint.region), + "Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats), "Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats) } -# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints -hallways: Dict[str, str] = { - "Overworld Redux, Furnace_gyro_west": "Overworld Redux, Archipelagos Redux_lower", - "Overworld Redux, Furnace_gyro_upper_north": "Overworld Redux, Sewer_west_aqueduct", - "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", - "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", - "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", - "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", - "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", - "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", - "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", - "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", -} -hallway_helper: Dict[str, str] = {} -for p1, p2 in hallways.items(): - hallway_helper[p1] = p2 - hallway_helper[p2] = p1 - -# so we can just loop over this instead of doing some complicated thing to deal with hallways in the hints -hallways_ur: Dict[str, str] = { - "Ruins Passage, Overworld Redux_east": "Ruins Passage, Overworld Redux_west", - "East Forest Redux Interior, East Forest Redux_upper": "East Forest Redux Interior, East Forest Redux_lower", - "Forest Boss Room, East Forest Redux Laddercave_": "Forest Boss Room, Forest Belltower_", - "Library Exterior, Atoll Redux_": "Library Exterior, Library Hall_", - "Library Rotunda, Library Lab_": "Library Rotunda, Library Hall_", - "Darkwoods Tunnel, Quarry Redux_": "Darkwoods Tunnel, Overworld Redux_", - "ziggurat2020_0, Quarry Redux_": "ziggurat2020_0, ziggurat2020_1_", - "Purgatory, Purgatory_bottom": "Purgatory, Purgatory_top", -} -hallway_helper_ur: Dict[str, str] = {} -for p1, p2 in hallways_ur.items(): - hallway_helper_ur[p1] = p2 - hallway_helper_ur[p2] = p1 - - # the key is the region you have, the value is the regions you get for having that region # this is mostly so we don't have to do something overly complex to get this information +# really want to get rid of this, but waiting on item plando being workable with ER dependent_regions_restricted: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal"): - ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", - "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", - "Overworld Spawn Portal"], + ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", + "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", "Upper Overworld", + "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", + "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", + "Overworld to Atoll Upper", "Overworld above Quarry Entrance", "Overworld after Envoy", "Overworld Tunnel Turret"): + ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", + "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", + "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", + "Overworld to Atoll Upper", "Overworld Temple Door", "Overworld above Quarry Entrance", + "Overworld after Envoy", "Overworld Tunnel Turret"], + ("Hourglass Cave",): + ["Hourglass Cave", "Hourglass Cave Tower"], ("Old House Front",): ["Old House Front", "Old House Back"], ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): @@ -742,63 +754,70 @@ class Hint(IntEnum): ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], ("Forest Belltower Main",): ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], ("Guard House 1 East", "Guard House 1 West"): ["Guard House 1 East", "Guard House 1 West"], + ("Guard House 2 Upper", "Guard House 2 Lower"): + ["Guard House 2 Upper", "Guard House 2 Lower"], ("Forest Grave Path Main", "Forest Grave Path Upper"): ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], ("Forest Grave Path by Grave", "Forest Hero's Grave"): ["Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], ("Well Boss",): ["Dark Tomb Checkpoint", "Well Boss"], - ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"): - ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave"], + ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region"): + ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region"], ("West Garden Portal", "West Garden Portal Item"): ["West Garden Portal", "West Garden Portal Item"], ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"): + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"], - ("Frog's Domain",): - ["Frog's Domain", "Frog's Domain Back"], - ("Library Exterior Ladder", "Library Exterior Tree"): - ["Library Exterior Ladder", "Library Exterior Tree"], - ("Library Hall", "Library Hero's Grave"): - ["Library Hall", "Library Hero's Grave"], - ("Library Lab", "Library Lab Lower", "Library Portal"): - ["Library Lab", "Library Lab Lower", "Library Portal"], + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], + ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): + ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], + ("Frog's Domain", "Frog's Domain Entry"): + ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], + ("Library Exterior Ladder Region", "Library Exterior Tree Region"): + ["Library Exterior Ladder Region", "Library Exterior Tree Region"], + ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): + ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], + ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): + ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], + ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): + ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], ("Fortress Courtyard Upper",): ["Fortress Courtyard Upper", "Fortress Exterior from East Forest", "Fortress Exterior from Overworld", "Fortress Exterior near cave", "Fortress Courtyard"], ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard"): + "Fortress Exterior near cave", "Fortress Courtyard", "Beneath the Vault Entry"): ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard"], - ("Beneath the Vault Front", "Beneath the Vault Back"): - ["Beneath the Vault Front", "Beneath the Vault Back"], + "Fortress Exterior near cave", "Fortress Courtyard", "Beneath the Vault Entry"], + ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): + ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], ("Fortress East Shortcut Upper",): ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], ("Eastern Vault Fortress",): ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], ("Fortress Arena", "Fortress Arena Portal"): ["Fortress Arena", "Fortress Arena Portal"], ("Lower Mountain", "Lower Mountain Stairs"): ["Lower Mountain", "Lower Mountain Stairs"], ("Monastery Front",): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], - ("Monastery Back", "Monastery Hero's Grave"): - ["Monastery Back", "Monastery Hero's Grave"], - ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], + ("Monastery Back", "Monastery Hero's Grave Region"): + ["Monastery Back", "Monastery Hero's Grave Region"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Even Lower Quarry"): ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Lower Quarry Zig Door"], + "Lower Quarry Zig Door", "Even Lower Quarry"], ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", - "Lower Quarry Zig Door"], + "Lower Quarry Zig Door", "Even Lower Quarry"], ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], ("Rooted Ziggurat Middle Top",): @@ -807,31 +826,45 @@ class Hint(IntEnum): ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp", "Swamp to Cathedral Treasure Room"): - ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], - ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): - ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp Ledge under Cathedral Door"): + ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Swamp Ledge under Cathedral Door"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"], ("Cathedral Gauntlet Checkpoint",): ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): - ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] + ("Cathedral Gauntlet Exit",): + ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): + ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] } dependent_regions_nmg: Dict[Tuple[str, ...], List[str]] = { - ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Ruined Passage Door"): - ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", - "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", - "Overworld Spawn Portal"], + ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", + "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", + "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", + "Overworld above Patrol Cave", "Overworld at Patrol Cave", "Overworld to West Garden Upper", + "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", "Overworld above Quarry Entrance", + "Overworld after Envoy", "Overworld Tunnel Turret"): + ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", + "Overworld Southeast Cross Door", "Overworld Old House Door", "Overworld Temple Door", + "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", "East Overworld", + "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", "Overworld above Patrol Cave", + "Overworld at Patrol Cave", "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", + "Overworld to Atoll Upper", "Overworld above Quarry Entrance", "Overworld after Envoy", + "Overworld Tunnel Turret"], # can laurels through the gate ("Old House Front", "Old House Back"): ["Old House Front", "Old House Back"], + ("Hourglass Cave",): + ["Hourglass Cave", "Hourglass Cave Tower"], ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], @@ -839,60 +872,67 @@ class Hint(IntEnum): ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], ("Forest Belltower Main",): ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], ("Guard House 1 East", "Guard House 1 West"): ["Guard House 1 East", "Guard House 1 West"], + ("Guard House 2 Upper", "Guard House 2 Lower"): + ["Guard House 2 Upper", "Guard House 2 Lower"], ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], ("Dark Tomb Checkpoint", "Well Boss"): ["Dark Tomb Checkpoint", "Well Boss"], - ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", "West Garden Portal", "West Garden Portal Item"): - ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", "West Garden Portal", "West Garden Portal Item"], ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"): + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"], - ("Frog's Domain",): - ["Frog's Domain", "Frog's Domain Back"], - ("Library Exterior Ladder", "Library Exterior Tree"): - ["Library Exterior Ladder", "Library Exterior Tree"], - ("Library Hall", "Library Hero's Grave"): - ["Library Hall", "Library Hero's Grave"], - ("Library Lab", "Library Lab Lower", "Library Portal"): - ["Library Lab", "Library Lab Lower", "Library Portal"], + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], + ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): + ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], + ("Frog's Domain", "Frog's Domain Entry"): + ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], + ("Library Exterior Ladder Region", "Library Exterior Tree Region"): + ["Library Exterior Ladder Region", "Library Exterior Tree Region"], + ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): + ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], + ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): + ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], + ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): + ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"): ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], - ("Beneath the Vault Front", "Beneath the Vault Back"): - ["Beneath the Vault Front", "Beneath the Vault Back"], + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"], + ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): + ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], ("Fortress Grave Path Upper",): - ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", - "Fortress Hero's Grave"], + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", + "Fortress Hero's Grave Region"], ("Fortress Arena", "Fortress Arena Portal"): ["Fortress Arena", "Fortress Arena Portal"], ("Lower Mountain", "Lower Mountain Stairs"): ["Lower Mountain", "Lower Mountain Stairs"], - ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], - ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry"): + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], + ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", + "Even Lower Quarry"): ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Lower Quarry Zig Door"], + "Lower Quarry Zig Door", "Even Lower Quarry"], ("Monastery Rope",): ["Monastery Rope", "Quarry", "Quarry Entry", "Quarry Back", "Quarry Portal", "Lower Quarry", - "Lower Quarry Zig Door"], + "Lower Quarry Zig Door", "Even Lower Quarry"], ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], ("Rooted Ziggurat Middle Top",): @@ -901,33 +941,48 @@ class Hint(IntEnum): ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"): - ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], - ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave"): - ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave", "Swamp", - "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance"], + ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Swamp Ledge under Cathedral Door"): + ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Swamp Ledge under Cathedral Door"], + ("Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region"): + ["Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", "Swamp Front", "Swamp Mid", + "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Swamp Ledge under Cathedral Door"], ("Cathedral Gauntlet Checkpoint",): ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): - ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] + ("Cathedral Gauntlet Exit",): + ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): + ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] } dependent_regions_ur: Dict[Tuple[str, ...], List[str]] = { # can use ladder storage to get to the well rail - ("Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", "Overworld Temple Door", - "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", - "Overworld Ruined Passage Door"): - ["Overworld", "Overworld Belltower", "Overworld Swamp Upper Entry", "Overworld Special Shop Entry", - "Overworld West Garden Laurels Entry", "Overworld Ruined Passage Door", "Overworld Southeast Cross Door", - "Overworld Old House Door", "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", - "Overworld Spawn Portal", "Overworld Well to Furnace Rail"], + ("Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", + "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", "Above Ruined Passage", + "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", "Overworld Quarry Entry", + "Overworld above Patrol Cave", "Overworld at Patrol Cave", "Overworld to West Garden Upper", + "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", "Overworld above Quarry Entrance", + "Overworld after Envoy", "Overworld Tunnel Turret"): + ["Overworld", "Overworld Belltower", "Overworld Belltower at Bell", "Overworld Swamp Upper Entry", + "Overworld Special Shop Entry", "Overworld West Garden Laurels Entry", "Overworld Southeast Cross Door", + "Overworld Temple Door", "Overworld Fountain Cross Door", "Overworld Town Portal", "Overworld Spawn Portal", + "Overworld Ruined Passage Door", "Overworld Swamp Lower Entry", "After Ruined Passage", + "Above Ruined Passage", "East Overworld", "Upper Overworld", "Overworld after Temple Rafters", + "Overworld Quarry Entry", "Overworld above Patrol Cave", "Overworld at Patrol Cave", + "Overworld to West Garden Upper", "Overworld Well Ladder", "Overworld Beach", "Overworld to Atoll Upper", + "Overworld above Quarry Entrance", "Overworld after Envoy", "Overworld Tunnel Turret"], # can laurels through the gate ("Old House Front", "Old House Back"): ["Old House Front", "Old House Back"], + ("Hourglass Cave",): + ["Hourglass Cave", "Hourglass Cave Tower"], ("Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"): ["Furnace Fuse", "Furnace Ladder Area", "Furnace Walking Path"], ("Sealed Temple", "Sealed Temple Rafters"): ["Sealed Temple", "Sealed Temple Rafters"], @@ -935,65 +990,71 @@ class Hint(IntEnum): ["Forest Belltower Upper", "Forest Belltower Main", "Forest Belltower Lower"], ("Forest Belltower Main",): ["Forest Belltower Main", "Forest Belltower Lower"], - ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal"): - ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal"], + ("East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"): + ["East Forest", "East Forest Dance Fox Spot", "East Forest Portal", "Lower Forest"], ("Guard House 1 East", "Guard House 1 West"): ["Guard House 1 East", "Guard House 1 West"], + ("Guard House 2 Upper", "Guard House 2 Lower"): + ["Guard House 2 Upper", "Guard House 2 Lower"], # can use laurels, ice grapple, or ladder storage to traverse ("Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"): ["Forest Grave Path Main", "Forest Grave Path Upper", "Forest Grave Path by Grave", "Forest Hero's Grave"], - ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"): - ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back"], - ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"): - ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit"], + ("Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"): + ["Beneath the Well Front", "Beneath the Well Main", "Beneath the Well Back", "Beneath the Well Ladder Exit"], + ("Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"): + ["Dark Tomb Entry Point", "Dark Tomb Main", "Dark Tomb Dark Exit", "Dark Tomb Upper"], ("Dark Tomb Checkpoint", "Well Boss"): ["Dark Tomb Checkpoint", "Well Boss"], # can ice grapple from portal area to the rest, and vice versa - ("West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + ("West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", "West Garden Portal", "West Garden Portal Item"): - ["West Garden", "West Garden Laurels Exit", "West Garden after Boss", "West Garden Hero's Grave", + ["West Garden", "West Garden Laurels Exit Region", "West Garden after Boss", "West Garden Hero's Grave Region", "West Garden Portal", "West Garden Portal Item"], ("Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"): + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"): ["Ruined Atoll", "Ruined Atoll Lower Entry Area", "Ruined Atoll Frog Mouth", "Ruined Atoll Portal", - "Ruined Atoll Statue"], - ("Frog's Domain",): - ["Frog's Domain", "Frog's Domain Back"], - ("Library Exterior Ladder", "Library Exterior Tree"): - ["Library Exterior Ladder", "Library Exterior Tree"], - ("Library Hall", "Library Hero's Grave"): - ["Library Hall", "Library Hero's Grave"], - ("Library Lab", "Library Lab Lower", "Library Portal"): - ["Library Lab", "Library Lab Lower", "Library Portal"], + "Ruined Atoll Statue", "Ruined Atoll Ladder Tops", "Ruined Atoll Frog Eye", "Ruined Atoll Ladder Tops"], + ("Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"): + ["Frog Stairs Upper", "Frog Stairs Lower", "Frog Stairs to Frog's Domain"], + ("Frog's Domain", "Frog's Domain Entry"): + ["Frog's Domain", "Frog's Domain Back", "Frog's Domain Entry"], + ("Library Exterior Ladder Region", "Library Exterior Tree Region"): + ["Library Exterior Ladder Region", "Library Exterior Tree Region"], + ("Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"): + ["Library Hall", "Library Hero's Grave Region", "Library Hall Bookshelf", "Library Hall to Rotunda"], + ("Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"): + ["Library Rotunda to Hall", "Library Rotunda", "Library Rotunda to Lab"], + ("Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"): + ["Library Lab", "Library Lab Lower", "Library Portal", "Library Lab to Librarian"], # can use ice grapple or ladder storage to get from any ladder to upper ("Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"): + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"): ["Fortress Exterior from East Forest", "Fortress Exterior from Overworld", - "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper"], - ("Beneath the Vault Front", "Beneath the Vault Back"): - ["Beneath the Vault Front", "Beneath the Vault Back"], + "Fortress Exterior near cave", "Fortress Courtyard", "Fortress Courtyard Upper", "Beneath the Vault Entry"], + ("Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"): + ["Beneath the Vault Front", "Beneath the Vault Back", "Beneath the Vault Ladder Exit"], # can ice grapple up ("Fortress East Shortcut Upper", "Fortress East Shortcut Lower"): ["Fortress East Shortcut Upper", "Fortress East Shortcut Lower"], ("Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"): ["Eastern Vault Fortress", "Eastern Vault Fortress Gold Door"], - ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"): - ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance", "Fortress Hero's Grave"], + ("Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"): + ["Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", "Fortress Hero's Grave Region"], # can ice grapple down ("Fortress Grave Path Upper",): - ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance", - "Fortress Hero's Grave"], + ["Fortress Grave Path Upper", "Fortress Grave Path", "Fortress Grave Path Dusty Entrance Region", + "Fortress Hero's Grave Region"], ("Fortress Arena", "Fortress Arena Portal"): ["Fortress Arena", "Fortress Arena Portal"], ("Lower Mountain", "Lower Mountain Stairs"): ["Lower Mountain", "Lower Mountain Stairs"], - ("Monastery Front", "Monastery Back", "Monastery Hero's Grave"): - ["Monastery Front", "Monastery Back", "Monastery Hero's Grave"], + ("Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"): + ["Monastery Front", "Monastery Back", "Monastery Hero's Grave Region"], # can use ladder storage at any of the Quarry ladders to get to Monastery Rope ("Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Monastery Rope"): + "Monastery Rope", "Even Lower Quarry"): ["Quarry", "Quarry Portal", "Lower Quarry", "Quarry Entry", "Quarry Back", "Quarry Monastery Entry", - "Monastery Rope", "Lower Quarry Zig Door"], + "Monastery Rope", "Lower Quarry Zig Door", "Even Lower Quarry"], ("Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front"): ["Rooted Ziggurat Upper Entry", "Rooted Ziggurat Upper Front", "Rooted Ziggurat Upper Back"], ("Rooted Ziggurat Middle Top",): @@ -1002,14 +1063,17 @@ class Hint(IntEnum): ["Rooted Ziggurat Lower Front", "Rooted Ziggurat Lower Back", "Rooted Ziggurat Portal Room Entrance"], ("Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"): ["Rooted Ziggurat Portal", "Rooted Ziggurat Portal Room Exit"], - ("Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", - "Back of Swamp Laurels Area", "Swamp Hero's Grave"): - ["Swamp", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance", "Back of Swamp", - "Back of Swamp Laurels Area", "Swamp Hero's Grave"], + ("Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", "Swamp Ledge under Cathedral Door"): + ["Swamp Front", "Swamp Mid", "Swamp to Cathedral Treasure Room", "Swamp to Cathedral Main Entrance Region", + "Back of Swamp", "Back of Swamp Laurels Area", "Swamp Hero's Grave Region", + "Swamp Ledge under Cathedral Door"], ("Cathedral Gauntlet Checkpoint",): ["Cathedral Gauntlet Checkpoint", "Cathedral Gauntlet Exit", "Cathedral Gauntlet"], - ("Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"): - ["Far Shore", "Far Shore to Spawn", "Far Shore to East Forest", "Far Shore to Quarry", - "Far Shore to Fortress", "Far Shore to Library", "Far Shore to West Garden"] + ("Cathedral Gauntlet Exit",): + ["Cathedral Gauntlet Exit", "Cathedral Gauntlet"], + ("Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"): + ["Far Shore", "Far Shore to Spawn Region", "Far Shore to East Forest Region", "Far Shore to Quarry Region", + "Far Shore to Fortress Region", "Far Shore to Library Region", "Far Shore to West Garden Region"] } diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index fec6635422ac..fdfd064561fe 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,8 +1,9 @@ -from typing import Dict, TYPE_CHECKING +from typing import Dict, Set, List, Tuple, TYPE_CHECKING from worlds.generic.Rules import set_rule, forbid_item from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage from .er_data import Portal -from BaseClasses import Region +from .options import TunicOptions +from BaseClasses import Region, CollectionState if TYPE_CHECKING: from . import TunicWorld @@ -27,6 +28,10 @@ gold_hexagon = "Gold Questagon" +def has_ladder(ladder: str, state: CollectionState, player: int, options: TunicOptions): + return not options.shuffle_ladders or state.has(ladder, player) + + def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None: player = world.player @@ -40,17 +45,203 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Overworld Holy Cross"], rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + # grapple on the west side, down the stairs from moss wall, across from ruined shop + regions["Overworld"].connect( + connecting_region=regions["Overworld Beach"], + rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + or state.has_any({laurels, grapple}, player)) + regions["Overworld Beach"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + or state.has_any({laurels, grapple}, player)) + + regions["Overworld Beach"].connect( + connecting_region=regions["Overworld West Garden Laurels Entry"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld West Garden Laurels Entry"].connect( + connecting_region=regions["Overworld Beach"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld Beach"].connect( + connecting_region=regions["Overworld to Atoll Upper"], + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + regions["Overworld to Atoll Upper"].connect( + connecting_region=regions["Overworld Beach"], + rule=lambda state: has_ladder("Ladder to Ruined Atoll", state, player, options)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld to Atoll Upper"], + rule=lambda state: state.has(laurels, player)) + regions["Overworld to Atoll Upper"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has_any({laurels, grapple}, player)) + regions["Overworld"].connect( connecting_region=regions["Overworld Belltower"], rule=lambda state: state.has(laurels, player)) regions["Overworld Belltower"].connect( connecting_region=regions["Overworld"]) + regions["Overworld Belltower"].connect( + connecting_region=regions["Overworld to West Garden Upper"], + rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + regions["Overworld to West Garden Upper"].connect( + connecting_region=regions["Overworld Belltower"], + rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + + regions["Overworld Belltower"].connect( + connecting_region=regions["Overworld Belltower at Bell"], + rule=lambda state: has_ladder("Ladders to West Bell", state, player, options)) + + # long dong, do not make a reverse connection here or to belltower + regions["Overworld above Patrol Cave"].connect( + connecting_region=regions["Overworld Belltower at Bell"], + rule=lambda state: options.logic_rules and state.has(fire_wand, player)) + # nmg: can laurels through the ruined passage door regions["Overworld"].connect( connecting_region=regions["Overworld Ruined Passage Door"], rule=lambda state: state.has(key, player, 2) or (state.has(laurels, player) and options.logic_rules)) + regions["Overworld Ruined Passage Door"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has(laurels, player) and options.logic_rules) + + regions["Overworld"].connect( + connecting_region=regions["After Ruined Passage"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["After Ruined Passage"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + + regions["Overworld"].connect( + connecting_region=regions["Above Ruined Passage"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + or state.has(laurels, player)) + regions["Above Ruined Passage"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + or state.has(laurels, player)) + + regions["After Ruined Passage"].connect( + connecting_region=regions["Above Ruined Passage"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + regions["Above Ruined Passage"].connect( + connecting_region=regions["After Ruined Passage"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options)) + + regions["Above Ruined Passage"].connect( + connecting_region=regions["East Overworld"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["East Overworld"].connect( + connecting_region=regions["Above Ruined Passage"], + rule=lambda state: has_ladder("Ladders near Weathervane", state, player, options) + or state.has(laurels, player)) + + # nmg: ice grapple the slimes, works both ways consistently + regions["East Overworld"].connect( + connecting_region=regions["After Ruined Passage"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["After Ruined Passage"].connect( + connecting_region=regions["East Overworld"], + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Overworld"].connect( + connecting_region=regions["East Overworld"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["East Overworld"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + + regions["East Overworld"].connect( + connecting_region=regions["Overworld at Patrol Cave"]) + regions["Overworld at Patrol Cave"].connect( + connecting_region=regions["East Overworld"], + rule=lambda state: state.has(laurels, player)) + + regions["Overworld at Patrol Cave"].connect( + connecting_region=regions["Overworld above Patrol Cave"], + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Overworld above Patrol Cave"].connect( + connecting_region=regions["Overworld at Patrol Cave"], + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld above Patrol Cave"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + or state.has(grapple, player)) + regions["Overworld above Patrol Cave"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + + regions["East Overworld"].connect( + connecting_region=regions["Overworld above Patrol Cave"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Overworld above Patrol Cave"].connect( + connecting_region=regions["East Overworld"], + rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, player, options)) + + regions["Overworld above Patrol Cave"].connect( + connecting_region=regions["Upper Overworld"], + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Upper Overworld"].connect( + connecting_region=regions["Overworld above Patrol Cave"], + rule=lambda state: has_ladder("Ladders near Patrol Cave", state, player, options) + or state.has(grapple, player)) + + regions["Upper Overworld"].connect( + connecting_region=regions["Overworld above Quarry Entrance"], + rule=lambda state: state.has_any({grapple, laurels}, player)) + regions["Overworld above Quarry Entrance"].connect( + connecting_region=regions["Upper Overworld"], + rule=lambda state: state.has_any({grapple, laurels}, player)) + + regions["Upper Overworld"].connect( + connecting_region=regions["Overworld after Temple Rafters"], + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options)) + regions["Overworld after Temple Rafters"].connect( + connecting_region=regions["Upper Overworld"], + rule=lambda state: has_ladder("Ladder near Temple Rafters", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Overworld above Quarry Entrance"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + regions["Overworld"].connect( + connecting_region=regions["Overworld above Quarry Entrance"], + rule=lambda state: has_ladder("Ladders near Dark Tomb", state, player, options)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld after Envoy"], + rule=lambda state: state.has_any({laurels, grapple}, player) + or state.has("Sword Upgrade", player, 4) + or options.logic_rules) + regions["Overworld after Envoy"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has_any({laurels, grapple}, player) + or state.has("Sword Upgrade", player, 4) + or options.logic_rules) + + regions["Overworld after Envoy"].connect( + connecting_region=regions["Overworld Quarry Entry"], + rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + regions["Overworld Quarry Entry"].connect( + connecting_region=regions["Overworld after Envoy"], + rule=lambda state: has_ladder("Ladder to Quarry", state, player, options)) + + # ice grapple through the gate + regions["Overworld"].connect( + connecting_region=regions["Overworld Quarry Entry"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Overworld Quarry Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) regions["Overworld"].connect( connecting_region=regions["Overworld Swamp Upper Entry"], @@ -60,18 +251,24 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player)) regions["Overworld"].connect( + connecting_region=regions["Overworld Swamp Lower Entry"], + rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + regions["Overworld Swamp Lower Entry"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: has_ladder("Ladder to Swamp", state, player, options)) + + regions["East Overworld"].connect( connecting_region=regions["Overworld Special Shop Entry"], rule=lambda state: state.has(laurels, player)) regions["Overworld Special Shop Entry"].connect( - connecting_region=regions["Overworld"], + connecting_region=regions["East Overworld"], rule=lambda state: state.has(laurels, player)) regions["Overworld"].connect( - connecting_region=regions["Overworld West Garden Laurels Entry"], - rule=lambda state: state.has(laurels, player)) - regions["Overworld West Garden Laurels Entry"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: state.has(laurels, player)) + connecting_region=regions["Overworld Well Ladder"], + rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + regions["Overworld Well Ladder"].connect( + connecting_region=regions["Overworld"]) # nmg: can ice grapple through the door regions["Overworld"].connect( @@ -109,10 +306,30 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: ice grapple through temple door regions["Overworld"].connect( connecting_region=regions["Overworld Temple Door"], - name="Overworld Temple Door", rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player) or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + regions["Overworld Temple Door"].connect( + connecting_region=regions["Overworld above Patrol Cave"], + rule=lambda state: state.has(grapple, player)) + + regions["Overworld Tunnel Turret"].connect( + connecting_region=regions["Overworld Beach"], + rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + or state.has(grapple, player)) + regions["Overworld Beach"].connect( + connecting_region=regions["Overworld Tunnel Turret"], + rule=lambda state: has_ladder("Ladders in Overworld Town", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + regions["Overworld"].connect( + connecting_region=regions["Overworld Tunnel Turret"], + rule=lambda state: state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + regions["Overworld Tunnel Turret"].connect( + connecting_region=regions["Overworld"], + rule=lambda state: state.has_any({grapple, laurels}, player)) + # Overworld side areas regions["Old House Front"].connect( connecting_region=regions["Old House Back"]) @@ -148,12 +365,17 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Furnace Fuse"], rule=lambda state: state.has(laurels, player)) + regions["Hourglass Cave"].connect( + connecting_region=regions["Hourglass Cave Tower"], + rule=lambda state: has_ladder("Ladders in Hourglass Cave", state, player, options)) + # East Forest regions["Forest Belltower Upper"].connect( connecting_region=regions["Forest Belltower Main"]) regions["Forest Belltower Main"].connect( - connecting_region=regions["Forest Belltower Lower"]) + connecting_region=regions["Forest Belltower Lower"], + rule=lambda state: has_ladder("Ladder to East Forest", state, player, options)) # nmg: ice grapple up to dance fox spot, and vice versa regions["East Forest"].connect( @@ -171,12 +393,28 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["East Forest Portal"].connect( connecting_region=regions["East Forest"]) + regions["East Forest"].connect( + connecting_region=regions["Lower Forest"], + rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options) + or (state.has_all({grapple, fire_wand, ice_dagger}, player) # do ice slime, then go to the lower hook + and has_ability(state, player, icebolt, options, ability_unlocks))) + regions["Lower Forest"].connect( + connecting_region=regions["East Forest"], + rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + regions["Guard House 1 East"].connect( connecting_region=regions["Guard House 1 West"]) regions["Guard House 1 West"].connect( connecting_region=regions["Guard House 1 East"], rule=lambda state: state.has(laurels, player)) + regions["Guard House 2 Upper"].connect( + connecting_region=regions["Guard House 2 Lower"], + rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + regions["Guard House 2 Lower"].connect( + connecting_region=regions["Guard House 2 Upper"], + rule=lambda state: has_ladder("Ladders to Lower Forest", state, player, options)) + # nmg: ice grapple from upper grave path exit to the rest of it regions["Forest Grave Path Upper"].connect( connecting_region=regions["Forest Grave Path Main"], @@ -201,6 +439,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Forest Grave Path by Grave"]) # Beneath the Well and Dark Tomb + # don't need the ladder when entering at the ladder spot + regions["Beneath the Well Ladder Exit"].connect( + connecting_region=regions["Beneath the Well Front"], + rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + regions["Beneath the Well Front"].connect( + connecting_region=regions["Beneath the Well Ladder Exit"], + rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Main"], rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) @@ -208,12 +454,13 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Beneath the Well Front"], rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) - regions["Beneath the Well Back"].connect( - connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) regions["Beneath the Well Main"].connect( connecting_region=regions["Beneath the Well Back"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + rule=lambda state: has_ladder("Ladders in Well", state, player, options)) + regions["Beneath the Well Back"].connect( + connecting_region=regions["Beneath the Well Main"], + rule=lambda state: has_ladder("Ladders in Well", state, player, options) + and (has_stick(state, player) or state.has(fire_wand, player))) regions["Well Boss"].connect( connecting_region=regions["Dark Tomb Checkpoint"]) @@ -223,25 +470,30 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player) and options.logic_rules) regions["Dark Tomb Entry Point"].connect( - connecting_region=regions["Dark Tomb Main"], + connecting_region=regions["Dark Tomb Upper"], rule=lambda state: has_lantern(state, player, options)) + regions["Dark Tomb Upper"].connect( + connecting_region=regions["Dark Tomb Entry Point"]) + + regions["Dark Tomb Upper"].connect( + connecting_region=regions["Dark Tomb Main"], + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) regions["Dark Tomb Main"].connect( - connecting_region=regions["Dark Tomb Entry Point"], - rule=lambda state: has_lantern(state, player, options)) + connecting_region=regions["Dark Tomb Upper"], + rule=lambda state: has_ladder("Ladder in Dark Tomb", state, player, options)) regions["Dark Tomb Main"].connect( - connecting_region=regions["Dark Tomb Dark Exit"], - rule=lambda state: has_lantern(state, player, options)) + connecting_region=regions["Dark Tomb Dark Exit"]) regions["Dark Tomb Dark Exit"].connect( connecting_region=regions["Dark Tomb Main"], rule=lambda state: has_lantern(state, player, options)) # West Garden - regions["West Garden Laurels Exit"].connect( + regions["West Garden Laurels Exit Region"].connect( connecting_region=regions["West Garden"], rule=lambda state: state.has(laurels, player)) regions["West Garden"].connect( - connecting_region=regions["West Garden Laurels Exit"], + connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) regions["West Garden after Boss"].connect( @@ -252,9 +504,9 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player) or has_sword(state, player)) regions["West Garden"].connect( - connecting_region=regions["West Garden Hero's Grave"], + connecting_region=regions["West Garden Hero's Grave Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - regions["West Garden Hero's Grave"].connect( + regions["West Garden Hero's Grave Region"].connect( connecting_region=regions["West Garden"]) regions["West Garden Portal"].connect( @@ -282,6 +534,10 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Ruined Atoll"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Ladder Tops"], + rule=lambda state: has_ladder("Ladders in South Atoll", state, player, options)) + regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Frog Mouth"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) @@ -289,6 +545,13 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Ruined Atoll"], rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + regions["Ruined Atoll"].connect( + connecting_region=regions["Ruined Atoll Frog Eye"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Ruined Atoll Frog Eye"].connect( + connecting_region=regions["Ruined Atoll"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Portal"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) @@ -297,41 +560,109 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + and has_ladder("Ladders in South Atoll", state, player, options)) regions["Ruined Atoll Statue"].connect( connecting_region=regions["Ruined Atoll"]) + regions["Frog Stairs Eye Exit"].connect( + connecting_region=regions["Frog Stairs Upper"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Frog Stairs Upper"].connect( + connecting_region=regions["Frog Stairs Eye Exit"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + + regions["Frog Stairs Upper"].connect( + connecting_region=regions["Frog Stairs Lower"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Frog Stairs Lower"].connect( + connecting_region=regions["Frog Stairs Upper"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + + regions["Frog Stairs Lower"].connect( + connecting_region=regions["Frog Stairs to Frog's Domain"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Frog Stairs to Frog's Domain"].connect( + connecting_region=regions["Frog Stairs Lower"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + + regions["Frog's Domain Entry"].connect( + connecting_region=regions["Frog's Domain"], + rule=lambda state: has_ladder("Ladders to Frog's Domain", state, player, options)) + regions["Frog's Domain"].connect( connecting_region=regions["Frog's Domain Back"], rule=lambda state: state.has(grapple, player)) # Library - regions["Library Exterior Tree"].connect( - connecting_region=regions["Library Exterior Ladder"], - rule=lambda state: state.has(grapple, player) or state.has(laurels, player)) - regions["Library Exterior Ladder"].connect( - connecting_region=regions["Library Exterior Tree"], + regions["Library Exterior Tree Region"].connect( + connecting_region=regions["Library Exterior Ladder Region"], + rule=lambda state: state.has_any({grapple, laurels}, player) + and has_ladder("Ladders in Library", state, player, options)) + regions["Library Exterior Ladder Region"].connect( + connecting_region=regions["Library Exterior Tree Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and (state.has(grapple, player) or state.has(laurels, player))) + and state.has_any({grapple, laurels}, player) + and has_ladder("Ladders in Library", state, player, options)) + regions["Library Hall Bookshelf"].connect( + connecting_region=regions["Library Hall"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) regions["Library Hall"].connect( - connecting_region=regions["Library Hero's Grave"], + connecting_region=regions["Library Hall Bookshelf"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + + regions["Library Hall"].connect( + connecting_region=regions["Library Hero's Grave Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - regions["Library Hero's Grave"].connect( + regions["Library Hero's Grave Region"].connect( connecting_region=regions["Library Hall"]) + regions["Library Hall to Rotunda"].connect( + connecting_region=regions["Library Hall"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + regions["Library Hall"].connect( + connecting_region=regions["Library Hall to Rotunda"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + + regions["Library Rotunda to Hall"].connect( + connecting_region=regions["Library Rotunda"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + regions["Library Rotunda"].connect( + connecting_region=regions["Library Rotunda to Hall"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + + regions["Library Rotunda"].connect( + connecting_region=regions["Library Rotunda to Lab"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + regions["Library Rotunda to Lab"].connect( + connecting_region=regions["Library Rotunda"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + regions["Library Lab Lower"].connect( connecting_region=regions["Library Lab"], - rule=lambda state: state.has(laurels, player) or state.has(grapple, player)) + rule=lambda state: state.has_any({grapple, laurels}, player) + and has_ladder("Ladders in Library", state, player, options)) regions["Library Lab"].connect( connecting_region=regions["Library Lab Lower"], - rule=lambda state: state.has(laurels, player)) + rule=lambda state: state.has(laurels, player) + and has_ladder("Ladders in Library", state, player, options)) regions["Library Lab"].connect( connecting_region=regions["Library Portal"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) + rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + and has_ladder("Ladders in Library", state, player, options)) regions["Library Portal"].connect( - connecting_region=regions["Library Lab"]) + connecting_region=regions["Library Lab"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options) + or state.has(laurels, player)) + + regions["Library Lab"].connect( + connecting_region=regions["Library Lab to Librarian"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) + regions["Library Lab to Librarian"].connect( + connecting_region=regions["Library Lab"], + rule=lambda state: has_ladder("Ladders in Library", state, player, options)) # Eastern Vault Fortress regions["Fortress Exterior from East Forest"].connect( @@ -348,6 +679,13 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Fortress Exterior near cave"], rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks)) + regions["Fortress Exterior near cave"].connect( + connecting_region=regions["Beneath the Vault Entry"], + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + regions["Beneath the Vault Entry"].connect( + connecting_region=regions["Fortress Exterior near cave"], + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + regions["Fortress Courtyard"].connect( connecting_region=regions["Fortress Exterior from Overworld"], rule=lambda state: state.has(laurels, player)) @@ -367,6 +705,13 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) + regions["Beneath the Vault Ladder Exit"].connect( + connecting_region=regions["Beneath the Vault Front"], + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + regions["Beneath the Vault Front"].connect( + connecting_region=regions["Beneath the Vault Ladder Exit"], + rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)) + regions["Beneath the Vault Front"].connect( connecting_region=regions["Beneath the Vault Back"], rule=lambda state: has_lantern(state, player, options)) @@ -383,26 +728,24 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: ice grapple through the big gold door, can do it both ways regions["Eastern Vault Fortress"].connect( connecting_region=regions["Eastern Vault Fortress Gold Door"], - name="Fortress to Gold Door", rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses", "Activate Eastern Vault East Fuse"}, player) or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) regions["Eastern Vault Fortress Gold Door"].connect( connecting_region=regions["Eastern Vault Fortress"], - name="Gold Door to Fortress", rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) regions["Fortress Grave Path"].connect( - connecting_region=regions["Fortress Grave Path Dusty Entrance"], + connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], rule=lambda state: state.has(laurels, player)) - regions["Fortress Grave Path Dusty Entrance"].connect( + regions["Fortress Grave Path Dusty Entrance Region"].connect( connecting_region=regions["Fortress Grave Path"], rule=lambda state: state.has(laurels, player)) regions["Fortress Grave Path"].connect( - connecting_region=regions["Fortress Hero's Grave"], + connecting_region=regions["Fortress Hero's Grave Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - regions["Fortress Hero's Grave"].connect( + regions["Fortress Hero's Grave Region"].connect( connecting_region=regions["Fortress Grave Path"]) # nmg: ice grapple from upper grave path to lower @@ -412,7 +755,6 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Fortress Arena"].connect( connecting_region=regions["Fortress Arena Portal"], - name="Fortress Arena to Fortress Portal", rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) regions["Fortress Arena Portal"].connect( connecting_region=regions["Fortress Arena"]) @@ -427,7 +769,6 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Quarry Entry"].connect( connecting_region=regions["Quarry Portal"], - name="Quarry to Quarry Portal", rule=lambda state: state.has("Activate Quarry Fuse", player)) regions["Quarry Portal"].connect( connecting_region=regions["Quarry Entry"]) @@ -464,17 +805,23 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re connecting_region=regions["Lower Quarry"], rule=lambda state: has_mask(state, player, options)) - # nmg: bring a scav over, then ice grapple through the door + # need the ladder, or you can ice grapple down in nmg regions["Lower Quarry"].connect( + connecting_region=regions["Even Lower Quarry"], + rule=lambda state: has_ladder("Ladders in Lower Quarry", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + + # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock + regions["Even Lower Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], - name="Quarry to Zig Door", rule=lambda state: state.has("Activate Quarry Fuse", player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + or (has_ice_grapple_logic(False, state, player, options, ability_unlocks) and options.entrance_rando)) - # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask + # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on regions["Quarry"].connect( connecting_region=regions["Lower Quarry Zig Door"], - rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks) + and options.entrance_rando) regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) @@ -484,9 +831,9 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re rule=lambda state: state.has(laurels, player) and options.logic_rules) regions["Monastery Back"].connect( - connecting_region=regions["Monastery Hero's Grave"], + connecting_region=regions["Monastery Hero's Grave Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - regions["Monastery Hero's Grave"].connect( + regions["Monastery Hero's Grave Region"].connect( connecting_region=regions["Monastery Back"]) # Ziggurat @@ -511,10 +858,11 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Lower Front"], - rule=lambda state: ((state.has(laurels, player) or - has_ice_grapple_logic(True, state, player, options, ability_unlocks)) and - has_ability(state, player, prayer, options, ability_unlocks) - and has_sword(state, player)) or can_ladder_storage(state, player, options)) + rule=lambda state: ((state.has(laurels, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) + and has_ability(state, player, prayer, options, ability_unlocks) + and has_sword(state, player)) + or can_ladder_storage(state, player, options)) regions["Rooted Ziggurat Lower Back"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Entrance"], @@ -524,27 +872,46 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Rooted Ziggurat Portal"].connect( connecting_region=regions["Rooted Ziggurat Portal Room Exit"], - name="Zig Portal Room Exit", rule=lambda state: state.has("Activate Ziggurat Fuse", player)) regions["Rooted Ziggurat Portal Room Exit"].connect( connecting_region=regions["Rooted Ziggurat Portal"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) # Swamp and Cathedral + regions["Swamp Front"].connect( + connecting_region=regions["Swamp Mid"], + rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + or state.has(laurels, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + regions["Swamp Mid"].connect( + connecting_region=regions["Swamp Front"], + rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + or state.has(laurels, player) + or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) # nmg: ice grapple through gate + # nmg: ice grapple through cathedral door, can do it both ways - regions["Swamp"].connect( - connecting_region=regions["Swamp to Cathedral Main Entrance"], - rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) + regions["Swamp Mid"].connect( + connecting_region=regions["Swamp to Cathedral Main Entrance Region"], + rule=lambda state: (has_ability(state, player, prayer, options, ability_unlocks) + and state.has(laurels, player)) or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) - regions["Swamp to Cathedral Main Entrance"].connect( - connecting_region=regions["Swamp"], + regions["Swamp to Cathedral Main Entrance Region"].connect( + connecting_region=regions["Swamp Mid"], rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)) - regions["Swamp"].connect( + regions["Swamp Mid"].connect( + connecting_region=regions["Swamp Ledge under Cathedral Door"], + rule=lambda state: has_ladder("Ladders in Swamp", state, player, options)) + regions["Swamp Ledge under Cathedral Door"].connect( + connecting_region=regions["Swamp Mid"], + rule=lambda state: has_ladder("Ladders in Swamp", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks)) # nmg: ice grapple the enemy at door + + regions["Swamp Ledge under Cathedral Door"].connect( connecting_region=regions["Swamp to Cathedral Treasure Room"], rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) regions["Swamp to Cathedral Treasure Room"].connect( - connecting_region=regions["Swamp"]) + connecting_region=regions["Swamp Ledge under Cathedral Door"]) regions["Back of Swamp"].connect( connecting_region=regions["Back of Swamp Laurels Area"], @@ -555,14 +922,14 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # nmg: can ice grapple down while you're on the pillars regions["Back of Swamp Laurels Area"].connect( - connecting_region=regions["Swamp"], + connecting_region=regions["Swamp Mid"], rule=lambda state: state.has(laurels, player) and has_ice_grapple_logic(True, state, player, options, ability_unlocks)) regions["Back of Swamp"].connect( - connecting_region=regions["Swamp Hero's Grave"], + connecting_region=regions["Swamp Hero's Grave Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)) - regions["Swamp Hero's Grave"].connect( + regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) regions["Cathedral Gauntlet Checkpoint"].connect( @@ -577,45 +944,41 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # Far Shore regions["Far Shore"].connect( - connecting_region=regions["Far Shore to Spawn"], + connecting_region=regions["Far Shore to Spawn Region"], rule=lambda state: state.has(laurels, player)) - regions["Far Shore to Spawn"].connect( + regions["Far Shore to Spawn Region"].connect( connecting_region=regions["Far Shore"], rule=lambda state: state.has(laurels, player)) regions["Far Shore"].connect( - connecting_region=regions["Far Shore to East Forest"], + connecting_region=regions["Far Shore to East Forest Region"], rule=lambda state: state.has(laurels, player)) - regions["Far Shore to East Forest"].connect( + regions["Far Shore to East Forest Region"].connect( connecting_region=regions["Far Shore"], rule=lambda state: state.has(laurels, player)) regions["Far Shore"].connect( - connecting_region=regions["Far Shore to West Garden"], - name="Far Shore to West Garden", + connecting_region=regions["Far Shore to West Garden Region"], rule=lambda state: state.has("Activate West Garden Fuse", player)) - regions["Far Shore to West Garden"].connect( + regions["Far Shore to West Garden Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( - connecting_region=regions["Far Shore to Quarry"], - name="Far Shore to Quarry", + connecting_region=regions["Far Shore to Quarry Region"], rule=lambda state: state.has("Activate Quarry Fuse", player)) - regions["Far Shore to Quarry"].connect( + regions["Far Shore to Quarry Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( - connecting_region=regions["Far Shore to Fortress"], - name="Far Shore to Fortress", + connecting_region=regions["Far Shore to Fortress Region"], rule=lambda state: state.has("Activate Eastern Vault West Fuses", player)) - regions["Far Shore to Fortress"].connect( + regions["Far Shore to Fortress Region"].connect( connecting_region=regions["Far Shore"]) regions["Far Shore"].connect( - connecting_region=regions["Far Shore to Library"], - name="Far Shore to Library", + connecting_region=regions["Far Shore to Library Region"], rule=lambda state: state.has("Activate Library Fuse", player)) - regions["Far Shore to Library"].connect( + regions["Far Shore to Library Region"].connect( connecting_region=regions["Far Shore"]) # Misc @@ -628,174 +991,335 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re # connecting the regions portals are in to other portals you can access via ladder storage # using has_stick instead of can_ladder_storage since it's already checking the logic rules if options.logic_rules == "unrestricted": - def get_paired_region(portal_sd: str) -> str: + def get_portal_info(portal_sd: str) -> (str, str): for portal1, portal2 in portal_pairs.items(): if portal1.scene_destination() == portal_sd: - return portal2.region + return portal1.name, portal2.region if portal2.scene_destination() == portal_sd: - return portal1.region + return portal2.name, portal1.region raise Exception("no matches found in get_paired_region") - # The upper Swamp entrance - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Swamp Redux 2_wall")], - rule=lambda state: has_stick(state, player)) - # Western Furnace entrance, next to the sign that leads to West Garden - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Furnace_gyro_west")], - rule=lambda state: has_stick(state, player)) - # Upper West Garden entry, by the belltower - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Archipelagos Redux_upper")], - rule=lambda state: has_stick(state, player)) - # West Garden entry by the Furnace - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Archipelagos Redux_lower")], - rule=lambda state: has_stick(state, player)) - # West Garden laurels entrance, by the beach - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Archipelagos Redux_lowest")], - rule=lambda state: has_stick(state, player)) - # Well rail, west side. Can ls in town, get extra height by going over the portal pad - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Sewer_west_aqueduct")], - rule=lambda state: has_stick(state, player)) - # Well rail, east side. Need some height from the temple stairs - regions["Overworld"].connect( - regions[get_paired_region("Overworld Redux, Furnace_gyro_upper_north")], - rule=lambda state: has_stick(state, player)) - - # Furnace ladder to the fuse entrance - regions["Furnace Ladder Area"].connect( - regions[get_paired_region("Furnace, Overworld Redux_gyro_upper_north")], - rule=lambda state: has_stick(state, player)) - # Furnace ladder to Dark Tomb - regions["Furnace Ladder Area"].connect( - regions[get_paired_region("Furnace, Crypt Redux_")], - rule=lambda state: has_stick(state, player)) - # Furnace ladder to the West Garden connector - regions["Furnace Ladder Area"].connect( - regions[get_paired_region("Furnace, Overworld Redux_gyro_west")], - rule=lambda state: has_stick(state, player)) - - # West Garden exit after Garden Knight - regions["West Garden"].connect( - regions[get_paired_region("Archipelagos Redux, Overworld Redux_upper")], - rule=lambda state: has_stick(state, player)) - # West Garden laurels exit - regions["West Garden"].connect( - regions[get_paired_region("Archipelagos Redux, Overworld Redux_lowest")], - rule=lambda state: has_stick(state, player)) - - # Frog mouth entrance - regions["Ruined Atoll"].connect( - regions[get_paired_region("Atoll Redux, Frog Stairs_mouth")], - rule=lambda state: has_stick(state, player)) - - # Entrance by the dancing fox holy cross spot - regions["East Forest"].connect( - regions[get_paired_region("East Forest Redux, East Forest Redux Laddercave_upper")], - rule=lambda state: has_stick(state, player)) - - # From the west side of guard house 1 to the east side - regions["Guard House 1 West"].connect( - regions[get_paired_region("East Forest Redux Laddercave, East Forest Redux_gate")], - rule=lambda state: has_stick(state, player)) - regions["Guard House 1 West"].connect( - regions[get_paired_region("East Forest Redux Laddercave, Forest Boss Room_")], - rule=lambda state: has_stick(state, player)) - - # Upper exit from the Forest Grave Path, use ls at the ladder by the gate switch - regions["Forest Grave Path Main"].connect( - regions[get_paired_region("Sword Access, East Forest Redux_upper")], - rule=lambda state: has_stick(state, player)) - - # Fortress exterior shop, ls at the ladder by the telescope - regions["Fortress Exterior from Overworld"].connect( - regions[get_paired_region("Fortress Courtyard, Shop_")], - rule=lambda state: has_stick(state, player)) - # Fortress main entry and grave path lower entry, ls at the ladder by the telescope - regions["Fortress Exterior from Overworld"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from Overworld"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], - rule=lambda state: has_stick(state, player)) - # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse - regions["Fortress Exterior from Overworld"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from Overworld"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress East_")], - rule=lambda state: has_stick(state, player)) - - # same as above, except from the east side of the area - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Shop_")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior from East Forest"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress East_")], - rule=lambda state: has_stick(state, player)) - - # same as above, except from the Beneath the Vault entrance ladder - regions["Fortress Exterior near cave"].connect( - regions[get_paired_region("Fortress Courtyard, Overworld Redux_")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior near cave"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior near cave"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior near cave"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")], - rule=lambda state: has_stick(state, player)) - regions["Fortress Exterior near cave"].connect( - regions[get_paired_region("Fortress Courtyard, Fortress East_")], - rule=lambda state: has_stick(state, player)) - - # ls at the ladder, need to gain a little height to get up the stairs - regions["Lower Mountain"].connect( - regions[get_paired_region("Mountain, Mountaintop_")], - rule=lambda state: has_stick(state, player)) - - # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword - regions["Quarry Monastery Entry"].connect( - regions[get_paired_region("Quarry Redux, Monastery_back")], - rule=lambda state: has_stick(state, player)) - - # Swamp to Gauntlet - regions["Swamp"].connect( - regions[get_paired_region("Swamp Redux 2, Cathedral Arena_")], - rule=lambda state: has_stick(state, player)) - # Swamp to Overworld upper - regions["Swamp"].connect( - regions[get_paired_region("Swamp Redux 2, Overworld Redux_wall")], - rule=lambda state: has_stick(state, player)) - # Ladder by the hero grave - regions["Back of Swamp"].connect( - regions[get_paired_region("Swamp Redux 2, Overworld Redux_conduit")], - rule=lambda state: has_stick(state, player)) - regions["Back of Swamp"].connect( - regions[get_paired_region("Swamp Redux 2, Shop_")], - rule=lambda state: has_stick(state, player)) - # Need to put the cathedral HC code mid-flight - regions["Back of Swamp"].connect( - regions[get_paired_region("Swamp Redux 2, Cathedral Redux_secret")], - rule=lambda state: has_stick(state, player) - and has_ability(state, player, holy_cross, options, ability_unlocks)) + ladder_storages: List[Tuple[str, str, Set[str]]] = [ + # LS from Overworld main + # The upper Swamp entrance + ("Overworld", "Overworld Redux, Swamp Redux 2_wall", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), + # Upper atoll entrance + ("Overworld", "Overworld Redux, Atoll Redux_upper", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), + # Furnace entrance, next to the sign that leads to West Garden + ("Overworld", "Overworld Redux, Furnace_gyro_west", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), + # Upper West Garden entry, by the belltower + ("Overworld", "Overworld Redux, Archipelagos Redux_upper", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), + # Ruined Passage + ("Overworld", "Overworld Redux, Ruins Passage_east", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town"}), + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + ("Overworld", "Overworld Redux, Sewer_west_aqueduct", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), + # Well rail, east side. Need some height from the temple stairs + ("Overworld", "Overworld Redux, Furnace_gyro_upper_north", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladder to Quarry"}), + # Quarry entry + ("Overworld", "Overworld Redux, Darkwoods Tunnel_", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well"}), + # East Forest entry + ("Overworld", "Overworld Redux, Forest Belltower_", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", + "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), + # Fortress entry + ("Overworld", "Overworld Redux, Fortress Courtyard_", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", + "Ladders near Patrol Cave", "Ladder to Quarry", "Ladders near Dark Tomb"}), + # Patrol Cave entry + ("Overworld", "Overworld Redux, PatrolCave_", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", + "Ladders near Overworld Checkpoint", "Ladder to Quarry", "Ladders near Dark Tomb"}), + # Special Shop entry, excluded in non-ER due to soft lock potential + ("Overworld", "Overworld Redux, ShopSpecial_", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", + "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", + "Ladders near Dark Tomb"}), + # Temple Rafters, excluded in non-ER + ladder rando due to soft lock potential + ("Overworld", "Overworld Redux, Temple_rafters", + {"Ladders near Weathervane", "Ladder to Swamp", "Ladders in Overworld Town", "Ladders in Well", + "Ladders near Overworld Checkpoint", "Ladders near Patrol Cave", "Ladder to Quarry", + "Ladders near Dark Tomb"}), + # Spot above the Quarry entrance, + # only gets you to the mountain stairs + ("Overworld above Quarry Entrance", "Overworld Redux, Mountain_", + {"Ladders near Dark Tomb"}), + + # LS from the Overworld Beach + # West Garden entry by the Furnace + ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lower", + {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), + # West Garden laurels entry + ("Overworld Beach", "Overworld Redux, Archipelagos Redux_lowest", + {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), + # Swamp lower entrance + ("Overworld Beach", "Overworld Redux, Swamp Redux 2_conduit", + {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), + # Rotating Lights entrance + ("Overworld Beach", "Overworld Redux, Overworld Cave_", + {"Ladders in Overworld Town", "Ladder to Ruined Atoll"}), + # Swamp upper entrance + ("Overworld Beach", "Overworld Redux, Swamp Redux 2_wall", + {"Ladder to Ruined Atoll"}), + # Furnace entrance, next to the sign that leads to West Garden + ("Overworld Beach", "Overworld Redux, Furnace_gyro_west", + {"Ladder to Ruined Atoll"}), + # Upper West Garden entry, by the belltower + ("Overworld Beach", "Overworld Redux, Archipelagos Redux_upper", + {"Ladder to Ruined Atoll"}), + # Ruined Passage + ("Overworld Beach", "Overworld Redux, Ruins Passage_east", + {"Ladder to Ruined Atoll"}), + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + ("Overworld Beach", "Overworld Redux, Sewer_west_aqueduct", + {"Ladder to Ruined Atoll"}), + # Well rail, east side. Need some height from the temple stairs + ("Overworld Beach", "Overworld Redux, Furnace_gyro_upper_north", + {"Ladder to Ruined Atoll"}), + # Quarry entry + ("Overworld Beach", "Overworld Redux, Darkwoods Tunnel_", + {"Ladder to Ruined Atoll"}), + + # LS from that low spot where you normally walk to swamp + # Only has low ones you can't get to from main Overworld + # West Garden main entry from swamp ladder + ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lower", + {"Ladder to Swamp"}), + # Maze Cave entry from swamp ladder + ("Overworld Swamp Lower Entry", "Overworld Redux, Maze Room_", + {"Ladder to Swamp"}), + # Hourglass Cave entry from swamp ladder + ("Overworld Swamp Lower Entry", "Overworld Redux, Town Basement_beach", + {"Ladder to Swamp"}), + # Lower Atoll entry from swamp ladder + ("Overworld Swamp Lower Entry", "Overworld Redux, Atoll Redux_lower", + {"Ladder to Swamp"}), + # Lowest West Garden entry from swamp ladder + ("Overworld Swamp Lower Entry", "Overworld Redux, Archipelagos Redux_lowest", + {"Ladder to Swamp"}), + + # from the ladders by the belltower + # Ruined Passage + ("Overworld to West Garden Upper", "Overworld Redux, Ruins Passage_east", + {"Ladders to West Bell"}), + # Well rail, west side. Can ls in town, get extra height by going over the portal pad + ("Overworld to West Garden Upper", "Overworld Redux, Sewer_west_aqueduct", + {"Ladders to West Bell"}), + # Well rail, east side. Need some height from the temple stairs + ("Overworld to West Garden Upper", "Overworld Redux, Furnace_gyro_upper_north", + {"Ladders to West Bell"}), + # Quarry entry + ("Overworld to West Garden Upper", "Overworld Redux, Darkwoods Tunnel_", + {"Ladders to West Bell"}), + # East Forest entry + ("Overworld to West Garden Upper", "Overworld Redux, Forest Belltower_", + {"Ladders to West Bell"}), + # Fortress entry + ("Overworld to West Garden Upper", "Overworld Redux, Fortress Courtyard_", + {"Ladders to West Bell"}), + # Patrol Cave entry + ("Overworld to West Garden Upper", "Overworld Redux, PatrolCave_", + {"Ladders to West Bell"}), + # Special Shop entry, excluded in non-ER due to soft lock potential + ("Overworld to West Garden Upper", "Overworld Redux, ShopSpecial_", + {"Ladders to West Bell"}), + # Temple Rafters, excluded in non-ER and ladder rando due to soft lock potential + ("Overworld to West Garden Upper", "Overworld Redux, Temple_rafters", + {"Ladders to West Bell"}), + + # In the furnace + # Furnace ladder to the fuse entrance + ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north", set()), + # Furnace ladder to Dark Tomb + ("Furnace Ladder Area", "Furnace, Crypt Redux_", set()), + # Furnace ladder to the West Garden connector + ("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_west", set()), + + # West Garden + # exit after Garden Knight + ("West Garden", "Archipelagos Redux, Overworld Redux_upper", set()), + # West Garden laurels exit + ("West Garden", "Archipelagos Redux, Overworld Redux_lowest", set()), + + # Atoll, use the little ladder you fix at the beginning + ("Ruined Atoll", "Atoll Redux, Overworld Redux_lower", set()), + ("Ruined Atoll", "Atoll Redux, Frog Stairs_mouth", set()), + ("Ruined Atoll", "Atoll Redux, Frog Stairs_eye", set()), + + # East Forest + # Entrance by the dancing fox holy cross spot + ("East Forest", "East Forest Redux, East Forest Redux Laddercave_upper", set()), + + # From the west side of Guard House 1 to the east side + ("Guard House 1 West", "East Forest Redux Laddercave, East Forest Redux_gate", set()), + ("Guard House 1 West", "East Forest Redux Laddercave, Forest Boss Room_", set()), + + # Upper exit from the Forest Grave Path, use LS at the ladder by the gate switch + ("Forest Grave Path Main", "Sword Access, East Forest Redux_upper", set()), + + # Fortress Exterior + # shop, ls at the ladder by the telescope + ("Fortress Exterior from Overworld", "Fortress Courtyard, Shop_", set()), + # Fortress main entry and grave path lower entry, ls at the ladder by the telescope + ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Main_Big Door", set()), + ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Lower", set()), + # Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse + ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress Reliquary_Upper", set()), + ("Fortress Exterior from Overworld", "Fortress Courtyard, Fortress East_", set()), + + # same as above, except from the east side of the area + ("Fortress Exterior from East Forest", "Fortress Courtyard, Overworld Redux_", set()), + ("Fortress Exterior from East Forest", "Fortress Courtyard, Shop_", set()), + ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Main_Big Door", set()), + ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Lower", set()), + ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress Reliquary_Upper", set()), + ("Fortress Exterior from East Forest", "Fortress Courtyard, Fortress East_", set()), + + # same as above, except from the Beneath the Vault entrance ladder + ("Fortress Exterior near cave", "Fortress Courtyard, Overworld Redux_", + {"Ladder to Beneath the Vault"}), + ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Main_Big Door", + {"Ladder to Beneath the Vault"}), + ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Lower", + {"Ladder to Beneath the Vault"}), + ("Fortress Exterior near cave", "Fortress Courtyard, Fortress Reliquary_Upper", + {"Ladder to Beneath the Vault"}), + ("Fortress Exterior near cave", "Fortress Courtyard, Fortress East_", + {"Ladder to Beneath the Vault"}), + + # ls at the ladder, need to gain a little height to get up the stairs + # excluded in non-ER due to soft lock potential + ("Lower Mountain", "Mountain, Mountaintop_", set()), + + # Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword + ("Quarry Monastery Entry", "Quarry Redux, Monastery_back", set()), + + # Swamp to Gauntlet + ("Swamp Mid", "Swamp Redux 2, Cathedral Arena_", + {"Ladders in Swamp"}), + # Swamp to Overworld upper + ("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", + {"Ladders in Swamp"}), + # Ladder by the hero grave + ("Back of Swamp", "Swamp Redux 2, Overworld Redux_conduit", set()), + ("Back of Swamp", "Swamp Redux 2, Shop_", set()), + # Need to put the cathedral HC code mid-flight + ("Back of Swamp", "Swamp Redux 2, Cathedral Redux_secret", set()), + ] + + for region_name, scene_dest, ladders in ladder_storages: + portal_name, paired_region = get_portal_info(scene_dest) + # this is the only exception, requiring holy cross as well + if portal_name == "Swamp to Cathedral Secret Legend Room Entrance" and region_name == "Back of Swamp": + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and has_ability(state, player, holy_cross, options, ability_unlocks) + and (has_ladder("Ladders in Swamp", state, player, options) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks) + or not options.entrance_rando)) + elif portal_name == "West Garden Exit after Boss" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player) + and (state.has("Ladders to West Bell", player))) + # soft locked unless you have either ladder. if you have laurels, you use the other Entrance + elif portal_name in {"Furnace Exit towards West Garden", "Furnace Exit to Dark Tomb"} \ + and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) + # soft locked for the same reasons as above + elif portal_name in {"Entrance to Furnace near West Garden", "West Garden Entrance from Furnace"} \ + and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player) + and state.has_any({"Ladder in Dark Tomb", "Ladders to West Bell"}, player)) + # soft locked if you can't get past garden knight backwards or up the belltower ladders + elif portal_name == "West Garden Entrance near Belltower" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) and state.has_any(ladders, player) + and state.has_any({"Ladders to West Bell", laurels}, player)) + # soft locked if you can't get back out + elif portal_name == "Fortress Courtyard to Beneath the Vault" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has("Ladder to Beneath the Vault", player) + and has_lantern(state, player, options)) + elif portal_name == "Atoll Lower Entrance" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player) + and (state.has_any({"Ladders in Overworld Town", grapple}, player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks))) + elif portal_name == "Atoll Upper Entrance" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player) + and state.has(grapple, player) or has_ability(state, player, prayer, options, ability_unlocks)) + # soft lock potential + elif portal_name in {"Special Shop Entrance", "Stairs to Top of the Mountain", "Swamp Upper Entrance", + "Swamp Lower Entrance", "Caustic Light Cave Entrance"} and not options.entrance_rando: + continue + # soft lock if you don't have the ladder, I regret writing unrestricted logic + elif portal_name == "Temple Rafters Entrance" and not options.entrance_rando: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player) + and (state.has("Ladder near Temple Rafters", player) + or (state.has_all({laurels, grapple}, player) + and ((state.has("Ladders near Patrol Cave", player) + and (state.has("Ladders near Dark Tomb", player) + or state.has("Ladder to Quarry", player) + and (state.has(fire_wand, player) or has_sword(state, player)))) + or state.has("Ladders near Overworld Checkpoint", player) + or has_ice_grapple_logic(True, state, player, options, ability_unlocks))))) + # if no ladder items are required, just do the basic stick only lambda + elif not ladders or not options.shuffle_ladders: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player)) + # one ladder required + elif len(ladders) == 1: + ladder = ladders.pop() + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has(ladder, player)) + # if multiple ladders can be used + else: + regions[region_name].connect( + regions[paired_region], + name=portal_name + " (LS) " + region_name, + rule=lambda state: has_stick(state, player) + and state.has_any(ladders, player)) def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None: @@ -825,6 +1349,16 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player), lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Southwest] Flowers Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [East] Weathervane Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Northeast] Flowers Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Southwest] Haiku Holy Cross", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) + set_rule(multiworld.get_location("Overworld - [Northwest] Golden Obelisk Page", player), + lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)) # Overworld set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player), @@ -939,7 +1473,8 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) lambda state: has_sword(state, player)) # nmg - kill Librarian with a lure, or gun I guess set_rule(multiworld.get_location("Librarian - Hexagon Green", player), - lambda state: has_sword(state, player) or options.logic_rules) + lambda state: (has_sword(state, player) or options.logic_rules) + and has_ladder("Ladders in Library", state, player, options)) # nmg - kill boss scav with orb + firecracker, or similar set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player), lambda state: has_sword(state, player) or (state.has(grapple, player) and options.logic_rules)) @@ -954,8 +1489,6 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) # these two swamp checks really want you to kill the big skeleton first set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), - lambda state: has_sword(state, player)) # Hero's Grave and Far Shore set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 291cd7b3310e..5756ec90be14 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,7 @@ from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_ur, \ +from .er_data import Portal, tunic_er_regions, portal_mapping, \ dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur from .er_rules import set_er_region_rules from worlds.generic import PlandoConnection @@ -19,118 +19,26 @@ class TunicERLocation(Location): game: str = "TUNIC" -def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]: +def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: regions: Dict[str, Region] = {} - portal_pairs: Dict[Portal, Portal] = pair_portals(world) - logic_rules = world.options.logic_rules + if world.options.entrance_rando: + portal_pairs: Dict[Portal, Portal] = pair_portals(world) - # output the entrances to the spoiler log here for convenience - for portal1, portal2 in portal_pairs.items(): - world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) - - # check if a portal leads to a hallway. if it does, update the hint text accordingly - def hint_helper(portal: Portal, hint_string: str = "") -> str: - # start by setting it as the name of the portal, for the case we're not using the hallway helper - if hint_string == "": - hint_string = portal.name + # output the entrances to the spoiler log here for convenience + for portal1, portal2 in portal_pairs.items(): + world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player) + else: + portal_pairs: Dict[Portal, Portal] = vanilla_portals() - # unrestricted has fewer hallways, like the well rail - if logic_rules == "unrestricted": - hallways = hallway_helper_ur - else: - hallways = hallway_helper - - if portal.scene_destination() in hallways: - # if we have a hallway, we want the region rather than the portal name - if hint_string == portal.name: - hint_string = portal.region - # library exterior is two regions, we just want to fix up the name - if hint_string in {"Library Exterior Tree", "Library Exterior Ladder"}: - hint_string = "Library Exterior" - - # search through the list for the other end of the hallway - for portala, portalb in portal_pairs.items(): - if portala.scene_destination() == hallways[portal.scene_destination()]: - # if we find that we have a chain of hallways, do recursion - if portalb.scene_destination() in hallways: - hint_region = portalb.region - if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: - hint_region = "Library Exterior" - hint_string = hint_region + " then " + hint_string - hint_string = hint_helper(portalb, hint_string) - else: - # if we didn't find a chain, get the portal name for the end of the chain - hint_string = portalb.name + " then " + hint_string - return hint_string - # and then the same thing for the other portal, since we have to check each separately - if portalb.scene_destination() == hallways[portal.scene_destination()]: - if portala.scene_destination() in hallways: - hint_region = portala.region - if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}: - hint_region = "Library Exterior" - hint_string = hint_region + " then " + hint_string - hint_string = hint_helper(portala, hint_string) - else: - hint_string = portala.name + " then " + hint_string - return hint_string - return hint_string - - # create our regions, give them hint text if they're in a spot where it makes sense to - # we're limiting which ones get hints so that it still gets that ER feel with a little less BS for region_name, region_data in tunic_er_regions.items(): - hint_text = "error" - if region_data.hint == 1: - for portal1, portal2 in portal_pairs.items(): - if portal1.region == region_name: - hint_text = hint_helper(portal2) - break - if portal2.region == region_name: - hint_text = hint_helper(portal1) - break - regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) - elif region_data.hint == 2: - for portal1, portal2 in portal_pairs.items(): - if portal1.scene() == tunic_er_regions[region_name].game_scene: - hint_text = hint_helper(portal2) - break - if portal2.scene() == tunic_er_regions[region_name].game_scene: - hint_text = hint_helper(portal1) - break - regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) - elif region_data.hint == 3: - # west garden portal item is at a dead end in restricted, otherwise just in west garden - if region_name == "West Garden Portal Item": - if world.options.logic_rules: - for portal1, portal2 in portal_pairs.items(): - if portal1.scene() == "Archipelagos Redux": - hint_text = hint_helper(portal2) - break - if portal2.scene() == "Archipelagos Redux": - hint_text = hint_helper(portal1) - break - regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) - else: - for portal1, portal2 in portal_pairs.items(): - if portal1.region == "West Garden Portal": - hint_text = hint_helper(portal2) - break - if portal2.region == "West Garden Portal": - hint_text = hint_helper(portal1) - break - regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text) - else: - regions[region_name] = Region(region_name, world.player, world.multiworld) + regions[region_name] = Region(region_name, world.player, world.multiworld) set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs) - er_hint_data: Dict[int, str] = {} for location_name, location_id in world.location_name_to_id.items(): region = regions[location_table[location_name].er_region] location = TunicERLocation(world.player, location_name, location_id, region) region.locations.append(location) - if region.name == region.hint_text: - continue - er_hint_data[location.address] = region.hint_text create_randomized_entrances(portal_pairs, regions) @@ -145,14 +53,12 @@ def hint_helper(portal: Portal, hint_string: str = "") -> str: world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player) victory_region.locations.append(victory_location) - portals_and_hints = (portal_pairs, er_hint_data) - - return portals_and_hints + return portal_pairs tunic_events: Dict[str, str] = { "Eastern Bell": "Forest Belltower Upper", - "Western Bell": "Overworld Belltower", + "Western Bell": "Overworld Belltower at Bell", "Furnace Fuse": "Furnace Fuse", "South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld", "Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper", @@ -163,7 +69,7 @@ def hint_helper(portal: Portal, hint_string: str = "") -> str: "Quarry Fuse": "Quarry", "Ziggurat Fuse": "Rooted Ziggurat Lower Back", "West Garden Fuse": "West Garden", - "Library Fuse": "Library Lab", + "Library Fuse": "Library Lab" } @@ -180,6 +86,38 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: region.locations.append(location) +def vanilla_portals() -> Dict[Portal, Portal]: + portal_pairs: Dict[Portal, Portal] = {} + portal_map = portal_mapping.copy() + shop_num = 1 + + while portal_map: + portal1 = portal_map[0] + portal2 = None + # portal2 scene destination tag is portal1's destination scene tag + portal2_sdt = portal1.destination_scene() + + if portal2_sdt.startswith("Shop,"): + portal2 = Portal(name=f"Shop", region="Shop", + destination="Previous Region", tag="_") + shop_num += 1 + + if portal2_sdt == "Purgatory, Purgatory_bottom": + portal2_sdt = "Purgatory, Purgatory_top" + + for portal in portal_map: + if portal.scene_destination() == portal2_sdt: + portal2 = portal + break + + portal_pairs[portal1] = portal2 + portal_map.remove(portal1) + if not portal2_sdt.startswith("Shop,"): + portal_map.remove(portal2) + + return portal_pairs + + # pairing off portals, starting with dead ends def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: # separate the portals into dead ends and non-dead ends @@ -290,7 +228,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: break if p_exit in ["Shop Portal", "Shop"]: portal2 = Portal(name="Shop Portal", region=f"Shop", - destination="Previous Region_") + destination="Previous Region", tag="_") shop_count -= 1 if shop_count < 0: shop_count += 2 @@ -355,10 +293,12 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: if portal.scene_destination() == "Overworld Redux, Windmill_": portal1 = portal break - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region_") if not portal1: raise Exception(f"Failed to do Fixed Shop option. " f"Did {player_name} plando connection the Windmill Shop entrance?") + + portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + portal_pairs[portal1] = portal2 two_plus.remove(portal1) @@ -433,7 +373,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]: break if portal1 is None: raise Exception("Too many shops in the pool, or something else went wrong.") - portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region_") + portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region", tag="_") + portal_pairs[portal1] = portal2 # connect dead ends to random non-dead ends @@ -465,10 +406,10 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic for portal1, portal2 in portal_pairs.items(): region1 = regions[portal1.region] region2 = regions[portal2.region] - region1.connect(region2, f"{portal1.name} -> {portal2.name}") + region1.connect(connecting_region=region2, name=portal1.name) # prevent the logic from thinking you can get to any shop-connected region from the shop - if not portal2.name.startswith("Shop"): - region2.connect(region1, f"{portal2.name} -> {portal1.name}") + if portal2.name not in {"Shop", "Shop Portal"}: + region2.connect(connecting_region=region1, name=portal2.name) # loop through the static connections, return regions you can reach from this region @@ -519,8 +460,8 @@ def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: return True # fortress teleporter needs only the left fuses - elif check_portal.scene_destination() in ["Fortress Arena, Transit_teleporter_spidertank", - "Transit, Fortress Arena_teleporter_spidertank"]: + elif check_portal.scene_destination() in {"Fortress Arena, Transit_teleporter_spidertank", + "Transit, Fortress Arena_teleporter_spidertank"}: i = j = k = 0 for portal in two_plus: if portal.scene() == "Fortress Courtyard": @@ -537,7 +478,8 @@ def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main": i = 0 for portal in two_plus: - if portal.region == "Swamp": + if portal.region in {"Swamp Front", "Swamp to Cathedral Treasure Room", + "Swamp to Cathedral Main Entrance Region"}: i += 1 if i == 4: return True @@ -553,8 +495,8 @@ def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool: # Quarry teleporter needs you to hit the Darkwoods fuse # Since it's physically in Quarry, we don't need to check for it - elif check_portal.scene_destination() in ["Quarry Redux, Transit_teleporter_quarry teleporter", - "Quarry Redux, ziggurat2020_0_"]: + elif check_portal.scene_destination() in {"Quarry Redux, Transit_teleporter_quarry teleporter", + "Quarry Redux, ziggurat2020_0_"}: i = 0 for portal in two_plus: if portal.scene() == "Darkwoods Tunnel": diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 547a0ffb816f..7483d55bf1cc 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -143,6 +143,28 @@ class TunicItemData(NamedTuple): "Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"), "Pages 52-53 (Icebolt)": TunicItemData(ItemClassification.progression, 1, 128, "pages"), "Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"), + + "Ladders near Weathervane": TunicItemData(ItemClassification.progression, 0, 130, "ladders"), + "Ladders near Overworld Checkpoint": TunicItemData(ItemClassification.progression, 0, 131, "ladders"), + "Ladders near Patrol Cave": TunicItemData(ItemClassification.progression, 0, 132, "ladders"), + "Ladder near Temple Rafters": TunicItemData(ItemClassification.progression, 0, 133, "ladders"), + "Ladders near Dark Tomb": TunicItemData(ItemClassification.progression, 0, 134, "ladders"), + "Ladder to Quarry": TunicItemData(ItemClassification.progression, 0, 135, "ladders"), + "Ladders to West Bell": TunicItemData(ItemClassification.progression, 0, 136, "ladders"), + "Ladders in Overworld Town": TunicItemData(ItemClassification.progression, 0, 137, "ladders"), + "Ladder to Ruined Atoll": TunicItemData(ItemClassification.progression, 0, 138, "ladders"), + "Ladder to Swamp": TunicItemData(ItemClassification.progression, 0, 139, "ladders"), + "Ladders in Well": TunicItemData(ItemClassification.progression, 0, 140, "ladders"), + "Ladder in Dark Tomb": TunicItemData(ItemClassification.progression, 0, 141, "ladders"), + "Ladder to East Forest": TunicItemData(ItemClassification.progression, 0, 142, "ladders"), + "Ladders to Lower Forest": TunicItemData(ItemClassification.progression, 0, 143, "ladders"), + "Ladder to Beneath the Vault": TunicItemData(ItemClassification.progression, 0, 144, "ladders"), + "Ladders in Hourglass Cave": TunicItemData(ItemClassification.progression, 0, 145, "ladders"), + "Ladders in South Atoll": TunicItemData(ItemClassification.progression, 0, 146, "ladders"), + "Ladders to Frog's Domain": TunicItemData(ItemClassification.progression, 0, 147, "ladders"), + "Ladders in Library": TunicItemData(ItemClassification.progression, 0, 148, "ladders"), + "Ladders in Lower Quarry": TunicItemData(ItemClassification.progression, 0, 149, "ladders"), + "Ladders in Swamp": TunicItemData(ItemClassification.progression, 0, 150, "ladders"), } fool_tiers: List[List[str]] = [ @@ -209,7 +231,9 @@ def get_item_group(item_name: str) -> str: "melee weapons": {"Stick", "Sword", "Sword Upgrade"}, "progressive sword": {"Sword Upgrade"}, "abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Icebolt)"}, - "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"} + "questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}, + "ladder to atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't + "ladders to bell": {"Ladders to West Bell"}, } item_name_groups.update(extra_groups) diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 1501fb7da24d..4d95e91cb3cc 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -1,11 +1,11 @@ -from typing import Dict, NamedTuple, Set -from itertools import groupby +from typing import Dict, NamedTuple, Set, Optional, List class TunicLocationData(NamedTuple): region: str er_region: str # entrance rando region - location_group: str = "region" + location_group: Optional[str] = None + location_groups: Optional[List[str]] = None location_base_id = 509342400 @@ -22,10 +22,10 @@ class TunicLocationData(NamedTuple): "Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), "Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"), "Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"), - "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Main"), + "Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Back"), "Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"), "Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"), - "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Beneath the Well", "Dark Tomb Checkpoint"), + "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"), "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"), @@ -39,25 +39,25 @@ class TunicLocationData(NamedTuple): "Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"), - "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Main"), + "Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Upper"), "Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"), - "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2"), - "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2"), + "Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2 Upper"), + "Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2 Lower"), "Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"), "Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"), - "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", "holy cross"), - "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "East Forest", "holy cross"), + "East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", location_group="holy cross"), + "East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "Lower Forest", location_group="holy cross"), "East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"), "East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"), "East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"), "East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"), - "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "Lower Forest"), "East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"), - "East Forest - Spider Chest": TunicLocationData("East Forest", "East Forest"), - "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "East Forest"), - "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "East Forest"), + "East Forest - Spider Chest": TunicLocationData("East Forest", "Lower Forest"), + "East Forest - Lower Dash Chest": TunicLocationData("East Forest", "Lower Forest"), + "East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "Lower Forest"), "East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"), "East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"), "Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"), @@ -65,18 +65,18 @@ class TunicLocationData(NamedTuple): "Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"), "Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"), "Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"), - "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", "holy cross"), + "Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", location_group="holy cross"), "Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"), "Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"), "Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"), "Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"), - "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"), + "Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest", location_group="hero relic"), "Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"), "Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"), "Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), - "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"), + "Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena", location_group="bosses"), "Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"), - "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", "holy cross"), + "Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", location_group="holy cross"), "Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), "Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"), @@ -84,7 +84,7 @@ class TunicLocationData(NamedTuple): "Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"), "Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), "Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"), - "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"), + "Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress", location_group="hero relic"), "Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"), "Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"), @@ -101,8 +101,8 @@ class TunicLocationData(NamedTuple): "Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"), "Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"), "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"), - "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena"), - "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", "holy cross"), + "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="bosses"), + "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="holy cross"), "Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), "Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"), "Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"), @@ -110,7 +110,7 @@ class TunicLocationData(NamedTuple): "Library Lab - Page 3": TunicLocationData("Library", "Library Lab"), "Library Lab - Page 1": TunicLocationData("Library", "Library Lab"), "Library Lab - Page 2": TunicLocationData("Library", "Library Lab"), - "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"), + "Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library", location_group="hero relic"), "Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"), "Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"), "Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"), @@ -122,42 +122,42 @@ class TunicLocationData(NamedTuple): "Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"), "Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"), "Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"), - "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"), "Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"), - "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "East Overworld"), + "Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld above Quarry Entrance"), "Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"), "Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"), "Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld"), - "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"), + "Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"), + "Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld Beach"), + "Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld above Patrol Cave"), + "Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld Beach"), + "Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"), "Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"), - "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld"), - "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld Tunnel Turret"), + "Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Above Ruined Passage"), + "Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Upper Overworld"), + "Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"), - "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Above Ruined Passage"), "Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"), - "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld"), + "Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld Beach"), "Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"), "Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"), "Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"), - "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "Overworld"), + "Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "East Overworld"), "Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"), - "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Overworld"), + "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"), "Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"), "Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"), "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"), @@ -165,49 +165,49 @@ class TunicLocationData(NamedTuple): "Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"), - "Shop - Potion 1": TunicLocationData("Overworld", "Shop", "shop"), - "Shop - Potion 2": TunicLocationData("Overworld", "Shop", "shop"), - "Shop - Coin 1": TunicLocationData("Overworld", "Shop", "shop"), - "Shop - Coin 2": TunicLocationData("Overworld", "Shop", "shop"), + "Shop - Potion 1": TunicLocationData("Overworld", "Shop", location_group="shop"), + "Shop - Potion 2": TunicLocationData("Overworld", "Shop", location_group="shop"), + "Shop - Coin 1": TunicLocationData("Overworld", "Shop", location_group="shop"), + "Shop - Coin 2": TunicLocationData("Overworld", "Shop", location_group="shop"), "Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"), "Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"), "Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"), "Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"), "Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"), - "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn"), - "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", "well"), - "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", "well"), - "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", "well"), - "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", "well"), - "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), - "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"), - "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"), - "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", "holy cross"), - "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", "holy cross"), - "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), - "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), - "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"), - "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", "holy cross"), - "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", "holy cross"), - "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", "holy cross"), - "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", "holy cross"), - "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", "holy cross"), - "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave", "holy cross"), - "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", "holy cross"), - "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", "holy cross"), - "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", "holy cross"), - "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", "holy cross"), + "Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn Region"), + "Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), + "Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), + "Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), + "Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", location_group="well"), + "Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"), + "Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", location_group="fairies"), + "Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), + "Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"), + "Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), + "Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"), + "Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "East Overworld", location_group="holy cross"), + "Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), + "Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Beach", location_group="holy cross"), + "Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), + "Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", location_group="holy cross"), + "Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Upper Overworld", location_group="holy cross"), + "Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", location_group="holy cross"), + "Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", location_group="holy cross"), + "Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), + "Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), + "Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", location_group="holy cross"), + "Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", location_group="holy cross"), + "Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", location_group="holy cross"), + "Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", location_group="holy cross"), + "Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", location_group="holy cross"), + "Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", location_group="holy cross"), + "Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave Tower", location_group="holy cross"), + "Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", location_group="holy cross"), + "Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", location_group="holy cross"), + "Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", location_group="holy cross"), + "Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", location_group="holy cross"), "Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"), - "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", "holy cross"), + "Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", location_group="holy cross"), "Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"), "Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"), @@ -225,16 +225,16 @@ class TunicLocationData(NamedTuple): "Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"), "Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"), "Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"), - "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"), + "Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry", location_group="hero relics"), "Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"), - "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Even Lower Quarry"), "Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"), - "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"), "Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), "Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"), - "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Lower Quarry"), + "Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry"), "Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"), "Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"), "Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"), @@ -246,15 +246,15 @@ class TunicLocationData(NamedTuple): "Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), - "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back"), + "Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="bosses"), "Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"), - "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"), "Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"), - "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"), "Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), @@ -262,40 +262,40 @@ class TunicLocationData(NamedTuple): "Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"), "Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"), - "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"), + "Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"), "Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"), "Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"), "Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"), - "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp Front"), "Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"), - "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp"), + "Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp Ledge under Cathedral Door"), + "Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp Front"), "Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"), - "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp"), - "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp"), - "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp"), - "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"), + "Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp Mid"), + "Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp Front"), + "Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp Mid"), + "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp", location_group="hero relic"), "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), "Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), - "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", "holy cross"), - "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), + "West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), "West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"), - "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", "holy cross"), + "West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="holy cross"), "West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"), "West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"), @@ -307,12 +307,12 @@ class TunicLocationData(NamedTuple): "West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"), "West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"), - "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("West Garden", "West Garden after Boss"), + "West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="bosses"), "West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"), "West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"), "West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"), "West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"), - "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"), + "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden", location_group="hero relic"), } hexagon_locations: Dict[str, str] = { @@ -323,15 +323,9 @@ class TunicLocationData(NamedTuple): location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)} - -def get_loc_group(location_name: str) -> str: - loc_group = location_table[location_name].location_group - if loc_group == "region": - # set loc_group as the region name. Typically, location groups are lowercase - loc_group = location_table[location_name].region.lower() - return loc_group - - -location_name_groups: Dict[str, Set[str]] = { - group: set(item_names) for group, item_names in groupby(sorted(location_table, key=get_loc_group), get_loc_group) -} +location_name_groups: Dict[str, Set[str]] = {} +for loc_name, loc_data in location_table.items(): + if loc_data.location_group: + if loc_data.location_group not in location_name_groups.keys(): + location_name_groups[loc_data.location_group] = set() + location_name_groups[loc_data.location_group].add(loc_name) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index 779e632326db..38ddcbe8e40f 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -4,8 +4,7 @@ class SwordProgression(DefaultOnToggle): - """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new - swords with increased range and attack power.""" + """Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new swords with increased range and attack power.""" internal_name = "sword_progression" display_name = "Sword Progression" @@ -24,25 +23,24 @@ class KeysBehindBosses(Toggle): class AbilityShuffling(Toggle): """Locks the usage of Prayer, Holy Cross*, and the Icebolt combo until the relevant pages of the manual have been found. - If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required - Hexagon goal amount. - *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other - player-facing codes. + If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required Hexagon goal amount. + *Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other player-facing codes. """ internal_name = "ability_shuffling" display_name = "Shuffle Abilities" class LogicRules(Choice): - """Set which logic rules to use for your world. + """ + Set which logic rules to use for your world. Restricted: Standard logic, no glitches. No Major Glitches: Sneaky Laurels zips, ice grapples through doors, shooting the west bell, and boss quick kills are included in logic. * Ice grappling through the Ziggurat door is not in logic since you will get stuck in there without Prayer. Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early. - *Special Shop is not in logic without the Hero's Laurels due to soft lock potential. + *Torch is given to the player at the start of the game due to the high softlock potential with various tricks. Using the torch is not required in logic. *Using Ladder Storage to get to individual chests is not in logic to avoid tedium. - *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in - Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on.""" + *Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on. + """ internal_name = "logic_rules" display_name = "Logic Rules" option_restricted = 0 @@ -68,8 +66,7 @@ class Maskless(Toggle): class FoolTraps(Choice): - """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative - effects to the player.""" + """Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative effects to the player.""" internal_name = "fool_traps" display_name = "Fool Traps" option_off = 0 @@ -80,8 +77,7 @@ class FoolTraps(Choice): class HexagonQuest(Toggle): - """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed - after collecting the required number of them.""" + """An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed after collecting the required number of them.""" internal_name = "hexagon_quest" display_name = "Hexagon Quest" @@ -105,9 +101,11 @@ class ExtraHexagonPercentage(Range): class EntranceRando(TextChoice): - """Randomize the connections between scenes. - You can choose a custom seed by editing this option. - A small, very lost fox on a big adventure.""" + """ + Randomize the connections between scenes. + If you set this to a value besides true or false, that value will be used as a custom seed. + A small, very lost fox on a big adventure. + """ internal_name = "entrance_rando" display_name = "Entrance Rando" alias_false = 0 @@ -137,15 +135,24 @@ class LaurelsLocation(Choice): default = 0 +class ShuffleLadders(Toggle): + """Turns several ladders in the game into items that must be found before they can be climbed on. + Adds more layers of progression to the game by blocking access to many areas early on. + "Ladders were a mistake." —Andrew Shouldice""" + internal_name = "shuffle_ladders" + display_name = "Shuffle Ladders" + + @dataclass class TunicOptions(PerGameCommonOptions): sword_progression: SwordProgression start_with_sword: StartWithSword keys_behind_bosses: KeysBehindBosses ability_shuffling: AbilityShuffling - logic_rules: LogicRules + shuffle_ladders: ShuffleLadders entrance_rando: EntranceRando fixed_shop: FixedShop + logic_rules: LogicRules fool_traps: FoolTraps hexagon_quest: HexagonQuest hexagon_goal: HexagonGoal diff --git a/worlds/tunic/regions.py b/worlds/tunic/regions.py index 70204c639733..c30a44bb8ff6 100644 --- a/worlds/tunic/regions.py +++ b/worlds/tunic/regions.py @@ -6,10 +6,10 @@ "Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp", "Spirit Arena"}, "Overworld Holy Cross": set(), - "East Forest": {"Eastern Vault Fortress"}, + "East Forest": set(), "Dark Tomb": {"West Garden"}, - "Beneath the Well": {"Dark Tomb"}, - "West Garden": {"Overworld", "Dark Tomb"}, + "Beneath the Well": set(), + "West Garden": set(), "Ruined Atoll": {"Frog's Domain", "Library"}, "Frog's Domain": set(), "Library": set(), diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index b3dd0b683220..c82c5ca13339 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -103,18 +103,10 @@ def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> No multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \ lambda state: state.has(laurels, player) \ or can_ladder_storage(state, player, options) - multiworld.get_entrance("Beneath the Well -> Dark Tomb", player).access_rule = \ - lambda state: has_lantern(state, player, options) - multiworld.get_entrance("West Garden -> Dark Tomb", player).access_rule = \ - lambda state: has_lantern(state, player, options) multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \ lambda state: state.has(laurels, player) \ or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ or can_ladder_storage(state, player, options) - multiworld.get_entrance("East Forest -> Eastern Vault Fortress", player).access_rule = \ - lambda state: state.has(laurels, player) \ - or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \ - or can_ladder_storage(state, player, options) # using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \ lambda state: has_lantern(state, player, options) and \ @@ -211,7 +203,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player), lambda state: state.has(laurels, player) - or (has_lantern(state, player, options) and has_sword(state, player))) + or (has_lantern(state, player, options) and has_sword(state, player)) + or can_ladder_storage(state, player, options)) set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player), lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules) set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player), @@ -228,6 +221,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player) or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player))) or has_ice_grapple_logic(False, state, player, options, ability_unlocks)) + set_rule(multiworld.get_location("West Furnace - Lantern Pickup", player), + lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player)) set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player), lambda state: state.has(fairies, player, 10)) @@ -265,8 +260,8 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player), lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player), - lambda state: has_sword(state, player) or state.has(laurels, player) - or has_ice_grapple_logic(False, state, player, options, ability_unlocks) + lambda state: state.has(laurels, player) + or (has_lantern(state, player, options) and has_sword(state, player)) or can_ladder_storage(state, player, options)) # Ruined Atoll @@ -325,8 +320,6 @@ def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> lambda state: state.has(laurels, player)) set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player), lambda state: has_sword(state, player)) - set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player), - lambda state: has_sword(state, player)) # Hero's Grave set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player), From 43084da23c719133fcae672e20c9b046e6ef8067 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:51:29 +0100 Subject: [PATCH 14/74] The Witness: Fix newlines in Witness option tooltips (#2971) --- worlds/witness/options.py | 169 ++++++++++++++++++++++++-------------- 1 file changed, 106 insertions(+), 63 deletions(-) diff --git a/worlds/witness/options.py b/worlds/witness/options.py index 5bce3e3a220c..b66308df432a 100644 --- a/worlds/witness/options.py +++ b/worlds/witness/options.py @@ -8,19 +8,23 @@ class DisableNonRandomizedPuzzles(Toggle): - """Disables puzzles that cannot be randomized. + """ + Disables puzzles that cannot be randomized. This includes many puzzles that heavily involve the environment, such as Shadows, Monastery or Orchard. + The lasers for those areas will activate as you solve optional puzzles, such as Discarded Panels. - Additionally, the panels activating Monastery Laser and Jungle Popup Wall will be on from the start.""" + Additionally, the panel activating the Jungle Popup Wall will be on from the start. + """ display_name = "Disable non randomized puzzles" class EarlyCaves(Choice): - """Adds an item that opens the Caves Shortcuts to Swamp and Mountain, - allowing early access to the Caves even if you are not playing a remote Door Shuffle mode. - You can either add this item to the pool to be found on one of your randomized checks, - or you can outright start with it and have immediate access to the Caves. - If you choose "add_to_pool" and you are already playing a remote Door Shuffle mode, this setting will do nothing.""" + """ + Adds an item that opens the Caves Shortcuts to Swamp and Mountain, allowing early access to the Caves even if you are not playing a remote Door Shuffle mode. + You can either add this item to the pool to be found in the multiworld, or you can outright start with it and have immediate access to the Caves. + + If you choose "Add To Pool" and you are already playing a remote Door Shuffle mode, this option will do nothing. + """ display_name = "Early Caves" option_off = 0 alias_false = 0 @@ -31,15 +35,19 @@ class EarlyCaves(Choice): class ShuffleSymbols(DefaultOnToggle): - """You will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. - If you turn this off, there will be no progression items in the game unless you turn on door shuffle.""" + """ + If on, you will need to unlock puzzle symbols as items to be able to solve the panels that contain those symbols. + + Please note that there is no minimum set of progression items in this randomizer. + If you turn this option off and don't turn on door shuffle or obelisk keys, there will be no progression items, which will disallow you from adding your yaml to a multiworld generation. + """ display_name = "Shuffle Symbols" class ShuffleLasers(Choice): - """If on, the 11 lasers are turned into items and will activate on their own upon receiving them. - Note: There is a visual bug that can occur with the Desert Laser. It does not affect gameplay - The Laser can still - be redirected as normal, for both applications of redirection.""" + """ + If on, the 11 lasers are turned into items and will activate on their own upon receiving them. + """ display_name = "Shuffle Lasers" option_off = 0 alias_false = 0 @@ -50,10 +58,12 @@ class ShuffleLasers(Choice): class ShuffleDoors(Choice): - """If on, opening doors, moving bridges etc. will require a "key". + """ + If on, opening doors, moving bridges etc. will require a "key". If set to "panels", the panel on the door will be locked until receiving its corresponding key. If set to "doors", the door will open immediately upon receiving its key. Door panels are added as location checks. - "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels".""" + "Mixed" includes all doors from "doors", and all control panels (bridges, elevators etc.) from "panels". + """ display_name = "Shuffle Doors" option_off = 0 option_panels = 1 @@ -62,38 +72,45 @@ class ShuffleDoors(Choice): class DoorGroupings(Choice): - """If set to "none", there will be one key for every door, resulting in up to 120 keys being added to the item pool. - If set to "regional", all doors in the same general region will open at once with a single key, - reducing the amount of door items and complexity.""" + """ + If set to "none", there will be one key for each door, potentially resulting in upwards of 120 keys being added to the item pool. + If set to "regional", all doors in the same general region will open at once with a single key, reducing the amount of door items and complexity. + """ display_name = "Door Groupings" option_off = 0 option_regional = 1 class ShuffleBoat(DefaultOnToggle): - """If set, adds a "Boat" item to the item pool. Before receiving this item, you will not be able to use the boat.""" + """ + If on, adds a "Boat" item to the item pool. Before receiving this item, you will not be able to use the boat. + """ display_name = "Shuffle Boat" class ShuffleDiscardedPanels(Toggle): - """Add Discarded Panels into the location pool. - Solving certain Discarded Panels may still be necessary to beat the game, even if this is off - The main example - of this being the alternate activation triggers in disable_non_randomized.""" + """ + Adds Discarded Panels into the location pool. + Even if this is off, solving certain Discarded Panels may still be necessary to beat the game - The main example of this being the alternate activation triggers in "Disable non randomized puzzles". + """ display_name = "Shuffle Discarded Panels" class ShuffleVaultBoxes(Toggle): - """Add Vault Boxes to the location pool.""" + """ + Adds Vault Boxes to the location pool. + """ display_name = "Shuffle Vault Boxes" class ShuffleEnvironmentalPuzzles(Choice): """ - Add Environmental/Obelisk Puzzles into the location pool. - In "individual", every Environmental Puzzle sends an item. - In "obelisk_sides", completing every puzzle on one side of an Obelisk sends an item. - Note: In Obelisk Sides, any EPs excluded through another setting will be counted as pre-completed on their Obelisk. + Adds Environmental/Obelisk Puzzles into the location pool. + If set to "individual", every Environmental Puzzle sends an item. + If set to "Obelisk Sides", completing every puzzle on one side of an Obelisk sends an item. + + Note: In Obelisk Sides, any EPs excluded through another option will be pre-completed on their Obelisk. """ display_name = "Shuffle Environmental Puzzles" option_off = 0 @@ -102,17 +119,18 @@ class ShuffleEnvironmentalPuzzles(Choice): class ShuffleDog(Toggle): - """Add petting the Town dog into the location pool.""" - + """ + Adds petting the Town dog into the location pool. + """ display_name = "Pet the Dog" class EnvironmentalPuzzlesDifficulty(Choice): """ When "Shuffle Environmental Puzzles" is on, this setting governs which EPs are eligible for the location pool. - On "eclipse", every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP". - On "tedious", Theater Eclipse EP is excluded from the location pool. - On "normal", several other difficult or long EPs are excluded as well. + If set to "eclipse", every EP in the game is eligible, including the 1-hour-long "Theater Eclipse EP". + If set to "tedious", Theater Eclipse EP is excluded from the location pool. + If set to "normal", several other difficult or long EPs are excluded as well. """ display_name = "Environmental Puzzles Difficulty" option_normal = 0 @@ -123,26 +141,31 @@ class EnvironmentalPuzzlesDifficulty(Choice): class ObeliskKeys(DefaultOnToggle): """ Add one Obelisk Key item per Obelisk, locking you out of solving any of the associated Environmental Puzzles. + Does nothing if "Shuffle Environmental Puzzles" is set to "off". """ display_name = "Obelisk Keys" class ShufflePostgame(Toggle): - """Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. - Use this if you don't play with release on victory. IMPORTANT NOTE: The possibility of your second - "Progressive Dots" showing up in the Caves is ignored, they will still be considered "postgame" in base settings.""" + """ + Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal. + Use this if you don't play with release on victory. + """ display_name = "Shuffle Postgame" class VictoryCondition(Choice): - """Set the victory condition for this world. + """ + Set the victory condition for this world. Elevator: Start the elevator at the bottom of the mountain (requires Mountain Lasers). Challenge: Beat the secret Challenge (requires Challenge Lasers). Mountain Box Short: Input the short solution to the Mountaintop Box (requires Mountain Lasers). Mountain Box Long: Input the long solution to the Mountaintop Box (requires Challenge Lasers). + It is important to note that while the Mountain Box requires Desert Laser to be redirected in Town for that laser - to count, the laser locks on the Elevator and Challenge Timer panels do not.""" + to count, the laser locks on the Elevator and Challenge Timer panels do not. + """ display_name = "Victory Condition" option_elevator = 0 option_challenge = 1 @@ -151,7 +174,9 @@ class VictoryCondition(Choice): class PuzzleRandomization(Choice): - """Puzzles in this randomizer are randomly generated. This setting changes the difficulty/types of puzzles.""" + """ + Puzzles in this randomizer are randomly generated. This option changes the difficulty/types of puzzles. + """ display_name = "Puzzle Randomization" option_sigma_normal = 0 option_sigma_expert = 1 @@ -159,10 +184,11 @@ class PuzzleRandomization(Choice): class MountainLasers(Range): - """Sets the amount of lasers required to enter the Mountain. - If set to a higher amount than 7, the mountaintop box will be slightly rotated to make it possible to solve without - the hatch being opened. - This change will also be applied logically to the long solution ("Challenge Lasers" setting).""" + """ + Sets the number of lasers required to enter the Mountain. + If set to a higher number than 7, the mountaintop box will be slightly rotated to make it possible to solve without the hatch being opened. + This change will also be applied logically to the long solution ("Challenge Lasers" option). + """ display_name = "Required Lasers for Mountain Entry" range_start = 1 range_end = 11 @@ -170,7 +196,9 @@ class MountainLasers(Range): class ChallengeLasers(Range): - """Sets the amount of beams required to enter the Caves through the Mountain Bottom Floor Discard.""" + """ + Sets the number of lasers required to enter the Caves through the Mountain Bottom Floor Discard and to unlock the Challenge Timer Panel. + """ display_name = "Required Lasers for Challenge" range_start = 1 range_end = 11 @@ -178,13 +206,17 @@ class ChallengeLasers(Range): class ElevatorsComeToYou(Toggle): - """If true, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. - This does actually affect logic as it allows unintended backwards / early access into these areas.""" + """ + If on, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them. + This does actually affect logic as it allows unintended backwards / early access into these areas. + """ display_name = "All Bridges & Elevators come to you" class TrapPercentage(Range): - """Replaces junk items with traps, at the specified rate.""" + """ + Replaces junk items with traps, at the specified rate. + """ display_name = "Trap Percentage" range_start = 0 range_end = 100 @@ -192,10 +224,11 @@ class TrapPercentage(Range): class TrapWeights(OptionDict): - """Specify the weights determining how many copies of each trap item will be in your itempool. + """ + Specify the weights determining how many copies of each trap item will be in your itempool. If you don't want a specific type of trap, you can set the weight for it to 0 (Do not delete the entry outright!). - If you set all trap weights to 0, you will get no traps, bypassing the "Trap Percentage" option.""" - + If you set all trap weights to 0, you will get no traps, bypassing the "Trap Percentage" option. + """ display_name = "Trap Weights" schema = Schema({ trap_name: And(int, lambda n: n >= 0) @@ -210,8 +243,9 @@ class TrapWeights(OptionDict): class PuzzleSkipAmount(Range): - """Adds this number of Puzzle Skips into the pool, if there is room. Puzzle Skips let you skip one panel. - Works on most panels in the game - The only big exception is The Challenge.""" + """ + Adds this many Puzzle Skips into the pool, if there is room. Puzzle Skips let you skip one panel. + """ display_name = "Puzzle Skips" range_start = 0 range_end = 30 @@ -219,8 +253,10 @@ class PuzzleSkipAmount(Range): class HintAmount(Range): - """Adds hints to Audio Logs. If set to a low amount, up to 2 additional duplicates of each hint will be added. - Remaining Audio Logs will have junk hints.""" + """ + Adds hints to Audio Logs. If set to a low amount, up to 2 additional duplicates of each hint will be added. + Remaining Audio Logs will have junk hints. + """ display_name = "Hints on Audio Logs" range_start = 0 range_end = 49 @@ -228,11 +264,12 @@ class HintAmount(Range): class AreaHintPercentage(Range): - """There are two types of hints for The Witness. - "Location hints" hint one location in your world / containing an item for your world. - "Area hints" will tell you some general info about the items you can find in one of the - main geographic areas on the island. - Use this option to specify how many of your hints you want to be area hints. The rest will be location hints.""" + """ + There are two types of hints for The Witness. + "Location hints" hint one location in your world or one location containing an item for your world. + "Area hints" tell you some general info about the items you can find in one of the main geographic areas on the island. + Use this option to specify how many of your hints you want to be area hints. The rest will be location hints. + """ display_name = "Area Hint Percentage" range_start = 0 range_end = 100 @@ -240,20 +277,26 @@ class AreaHintPercentage(Range): class LaserHints(Toggle): - """If on, lasers will tell you where their items are if you walk close to them in-game. - Only applies if laser shuffle is enabled.""" + """ + If on, lasers will tell you where their items are if you walk close to them in-game. + Only applies if Laser Shuffle is enabled. + """ display_name = "Laser Hints" class DeathLink(Toggle): - """If on: Whenever you fail a puzzle (with some exceptions), everyone who is also on Death Link dies. - The effect of a "death" in The Witness is a Bonk Trap.""" + """ + If on, whenever you fail a puzzle (with some exceptions), you and everyone who is also on Death Link dies. + The effect of a "death" in The Witness is a Bonk Trap. + """ display_name = "Death Link" class DeathLinkAmnesty(Range): - """Number of panel fails to allow before sending a death through Death Link. - 0 means every panel fail will send a death, 1 means every other panel fail will send a death, etc.""" + """ + The number of panel fails to allow before sending a death through Death Link. + 0 means every panel fail will send a death, 1 means every other panel fail will send a death, etc. + """ display_name = "Death Link Amnesty" range_start = 0 range_end = 5 From da333fbb0c88feedd4821a7bade3f56028a02111 Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Thu, 21 Mar 2024 09:52:16 -0600 Subject: [PATCH 15/74] Shivers: Adds missing logic rule for skull dial door location (#2997) --- worlds/shivers/Rules.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index 8aa8aa2c28dd..b1abb718c275 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -71,6 +71,12 @@ def first_nine_ixupi_capturable(state: CollectionState, player: int) -> bool: and metal_capturable(state, player) +def all_skull_dials_available(state: CollectionState, player: int) -> bool: + return state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region", player) \ + and state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) \ + and state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player) + + def get_rules_lookup(player: int): rules_lookup: Dict[str, List[Callable[[CollectionState], bool]]] = { "entrances": { @@ -116,10 +122,7 @@ def get_rules_lookup(player: int): "To Tar River From Lobby": lambda state: (state.has("Crawling", player) and oil_capturable(state, player) and state.can_reach("Tar River", "Region", player)), "To Burial From Egypt": lambda state: state.can_reach("Egypt", "Region", player), "To Gods Room From Anansi": lambda state: state.can_reach("Gods Room", "Region", player), - "To Slide Room": lambda state: ( - state.can_reach("Prehistoric", "Region", player) and state.can_reach("Tar River", "Region",player) and - state.can_reach("Egypt", "Region", player) and state.can_reach("Burial", "Region", player) and - state.can_reach("Gods Room", "Region", player) and state.can_reach("Werewolf", "Region", player)), + "To Slide Room": lambda state: all_skull_dials_available(state, player), "To Lobby From Slide Room": lambda state: (beths_body_available(state, player)) }, "locations_required": { @@ -141,6 +144,7 @@ def get_rules_lookup(player: int): "Final Riddle: Norse God Stone Message": lambda state: (state.can_reach("Fortune Teller", "Region", player) and state.can_reach("UFO", "Region", player)), "Final Riddle: Beth's Body Page 17": lambda state: beths_body_available(state, player), "Final Riddle: Guillotine Dropped": lambda state: beths_body_available(state, player), + "Puzzle Solved Skull Dial Door": lambda state: all_skull_dials_available(state, player), }, "locations_puzzle_hints": { "Puzzle Solved Clock Tower Door": lambda state: state.can_reach("Three Floor Elevator", "Region", player), From 40f843f54d5970302caeb2a21b76a4845cf5c0ed Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Thu, 21 Mar 2024 11:00:53 -0500 Subject: [PATCH 16/74] Lingo: Minor game data fixes (#3003) --- worlds/lingo/data/LL1.yaml | 20 ++++++++++++-------- worlds/lingo/data/generated.dat | Bin 130791 -> 130691 bytes worlds/lingo/data/ids.yaml | 5 +---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/worlds/lingo/data/LL1.yaml b/worlds/lingo/data/LL1.yaml index 75f688268f9d..f2d2a9ff5448 100644 --- a/worlds/lingo/data/LL1.yaml +++ b/worlds/lingo/data/LL1.yaml @@ -452,6 +452,7 @@ id: Shuffle Room/Panel_lost_found colors: black tag: botblack + check: True FORWARD: id: Entry Room/Panel_forward_forward tag: midwhite @@ -463,7 +464,12 @@ tag: midwhite doors: Crossroads Entrance: - id: Shuffle Room Area Doors/Door_chaos + id: + - Shuffle Room Area Doors/Door_chaos + - Shuffle Room Area Doors/Door_swap + - Shuffle Room Area Doors/Door_swap2 + - Shuffle Room Area Doors/Door_swap3 + - Shuffle Room Area Doors/Door_swap4 panels: - ORDER Tenacious Entrance: @@ -491,11 +497,6 @@ item_group: Achievement Room Entrances panels: - OPEN - Lost Door: - id: Shuffle Room Area Doors/Door_lost_found - junk_item: True - panels: - - LOST paintings: - id: maze_painting orientation: west @@ -699,6 +700,8 @@ door: Hollow Hallway tag: midwhite SWAP: + # In vanilla doors, solving this panel will open the way to Hub Room. This does not impact logic at all because + # Hub Room is always sphere 1 in vanilla doors. id: Shuffle Room/Panel_swap_wasp colors: yellow tag: midyellow @@ -1298,7 +1301,7 @@ id: Antonym Room/Panel_star_rats colors: black tag: midblack - TAME: + TUBE: id: Antonym Room/Panel_tame_mate colors: black tag: topblack @@ -1967,6 +1970,7 @@ door: Shortcut to Tower Rhyme Room (Smiley): door: Rhyme Room Entrance + Art Gallery: True # mark this as a warp in the sunwarps branch panels: RED: id: Color Arrow Room/Panel_red_afar @@ -4988,7 +4992,7 @@ colors: - red - blue - tag: mid red blue + tag: chain mid red blue required_panel: - room: Knight Night (Right Lower Segment) panel: ADJUST diff --git a/worlds/lingo/data/generated.dat b/worlds/lingo/data/generated.dat index 3bd8ff5a8b5fcd7ccfd60d32f34da3728e62c2d2..c957e5d51c895e444f1d5245bb9708a0a799cd14 100644 GIT binary patch delta 24324 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 delta 24677 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 diff --git a/worlds/lingo/data/ids.yaml b/worlds/lingo/data/ids.yaml index 4cad94855512..d3307deaa300 100644 --- a/worlds/lingo/data/ids.yaml +++ b/worlds/lingo/data/ids.yaml @@ -165,7 +165,7 @@ panels: THAT: 444525 STRESSED: 444526 STAR: 444527 - TAME: 444528 + TUBE: 444528 CAT: 444529 Hedge Maze: DOWN: 444530 @@ -995,9 +995,6 @@ doors: Traveled Entrance: item: 444433 location: 444438 - Lost Door: - item: 444435 - location: 444440 Pilgrim Antechamber: Sun Painting: item: 444436 From 4196bde597cdbb6186ff614294fd54ff043a0c99 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:38:36 -0400 Subject: [PATCH 17/74] Docs: Fixing special_range_names example (#3005) --- docs/options api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/options api.md b/docs/options api.md index 1141528991df..6205089f3dc3 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -204,7 +204,7 @@ For example: ```python range_start = 1 range_end = 99 -special_range_names: { +special_range_names = { "normal": 20, "extreme": 99, "unlimited": -1, From 218cd45844f9d733618af9088941156cd79b80bc Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 22 Mar 2024 03:02:38 -0500 Subject: [PATCH 18/74] APProcedurePatch: fix RLE/COPY incorrect sizing (#3006) * change class variables to instance variables * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/Files.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * move required_extensions to tuple * fix missing tuple ellipsis * fix classvar mixup * rename tokens to _tokens. use hasattr * type hint cleanup * Update Files.py * check using isinstance instead * Update Files.py --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/Files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/Files.py b/worlds/Files.py index 6e9bf6b31b59..69a88218efd4 100644 --- a/worlds/Files.py +++ b/worlds/Files.py @@ -322,7 +322,7 @@ def get_token_binary(self) -> bytes: data.append(args) elif token_type in [APTokenTypes.COPY, APTokenTypes.RLE]: assert isinstance(args, tuple), f"Arguments to COPY/RLE must be of type tuple, not {type(args)}" - data.extend(int.to_bytes(4, 4, "little")) + data.extend(int.to_bytes(8, 4, "little")) data.extend(args[0].to_bytes(4, "little")) data.extend(args[1].to_bytes(4, "little")) elif token_type == APTokenTypes.WRITE: From 11b32f17abebc08a6140506a375179f8a46bcfe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dana=C3=ABl=20V?= <104455676+ReverM@users.noreply.github.com> Date: Fri, 22 Mar 2024 12:46:14 -0400 Subject: [PATCH 19/74] Docs: replacing "setting" to "option" in world docs (#2622) * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md * Update contributing.md Added non-AP World specific information * Update contributing.md Fixed broken link * Some minor touchups * Update Contributing.md Draft for version with picture * Update contributing.md Small word change * Minor updates for conciseness, mostly * Changed all instances of settings to options in info and setup guides I combed through all world docs and swapped "setting" to "option" when this was refering to yaml options. I also changed a leftover "setting" in option.py * Update contributing.md * Update contributing.md * Update setup_en.md Woops I forgot one * Update Options.py Reverted changes regarding options.py * Update worlds/noita/docs/en_Noita.md Co-authored-by: Scipio Wright * Update worlds/sc2wol/docs/en_Starcraft 2 Wings of Liberty.md revert change waiting for that page to be updated * Update worlds/witness/docs/setup_en.md * Update worlds/witness/docs/en_The Witness.md * Update worlds/soe/docs/multiworld_en.md Fixed Typo Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update worlds/witness/docs/en_The Witness.md * Update worlds/adventure/docs/en_Adventure.md * Update worlds/witness/docs/setup_en.md * Updated Stardew valley to hopefully get rid of the merge conflicts * Didn't work :dismay: * Delete worlds/sc2wol/docs/setup_en.md I think this will fix the merge issue * Now it should work * Woops --------- Co-authored-by: Scipio Wright Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/adventure/docs/en_Adventure.md | 10 +-- worlds/adventure/docs/setup_en.md | 2 +- worlds/alttp/docs/en_A Link to the Past.md | 4 +- worlds/archipidle/docs/en_ArchipIDLE.md | 4 +- worlds/archipidle/docs/guide_en.md | 2 +- worlds/bk_sudoku/docs/en_Sudoku.md | 4 +- worlds/blasphemous/docs/en_Blasphemous.md | 4 +- worlds/bumpstik/docs/en_Bumper Stickers.md | 4 +- worlds/checksfinder/docs/en_ChecksFinder.md | 4 +- worlds/checksfinder/docs/setup_en.md | 2 +- worlds/clique/docs/en_Clique.md | 4 +- worlds/dark_souls_3/docs/en_Dark Souls III.md | 6 +- worlds/dark_souls_3/docs/setup_en.md | 4 +- worlds/dkc3/docs/en_Donkey Kong Country 3.md | 4 +- worlds/dkc3/docs/setup_en.md | 8 +- worlds/dlcquest/docs/en_DLCQuest.md | 4 +- worlds/dlcquest/docs/setup_en.md | 2 +- worlds/doom_1993/docs/en_DOOM 1993.md | 4 +- worlds/doom_ii/docs/en_DOOM II.md | 4 +- worlds/factorio/docs/en_Factorio.md | 6 +- worlds/factorio/docs/setup_en.md | 14 ++-- worlds/ff1/docs/en_Final Fantasy.md | 6 +- .../docs/en_Final Fantasy Mystic Quest.md | 4 +- worlds/ffmq/docs/setup_en.md | 8 +- worlds/heretic/docs/en_Heretic.md | 4 +- worlds/hk/docs/en_Hollow Knight.md | 6 +- worlds/hk/docs/setup_en.md | 2 +- worlds/hylics2/docs/en_Hylics 2.md | 4 +- worlds/kh2/docs/en_Kingdom Hearts 2.md | 4 +- worlds/kh2/docs/setup_en.md | 2 +- worlds/ladx/docs/en_Links Awakening DX.md | 6 +- worlds/ladx/docs/setup_en.md | 6 +- ...andstalker - The Treasures of King Nole.md | 8 +- .../landstalker/docs/landstalker_setup_en.md | 6 +- worlds/lingo/docs/en_Lingo.md | 4 +- .../lufia2ac/docs/en_Lufia II Ancient Cave.md | 4 +- worlds/lufia2ac/docs/setup_en.md | 6 +- worlds/meritous/docs/en_Meritous.md | 4 +- worlds/meritous/docs/setup_en.md | 4 +- worlds/minecraft/docs/en_Minecraft.md | 4 +- worlds/minecraft/docs/minecraft_en.md | 2 +- .../mmbn3/docs/en_MegaMan Battle Network 3.md | 4 +- worlds/mmbn3/docs/setup_en.md | 4 +- worlds/musedash/docs/en_Muse Dash.md | 4 +- worlds/musedash/docs/setup_en.md | 4 +- worlds/noita/docs/en_Noita.md | 12 +-- worlds/noita/docs/setup_en.md | 2 +- worlds/oot/docs/en_Ocarina of Time.md | 6 +- worlds/oot/docs/setup_en.md | 4 +- worlds/overcooked2/docs/en_Overcooked! 2.md | 4 +- worlds/overcooked2/docs/setup_en.md | 10 +-- .../docs/en_Pokemon Red and Blue.md | 10 +-- worlds/pokemon_rb/docs/setup_en.md | 6 +- worlds/raft/docs/en_Raft.md | 4 +- worlds/rogue_legacy/docs/en_Rogue Legacy.md | 6 +- worlds/rogue_legacy/docs/rogue-legacy_en.md | 2 +- worlds/ror2/docs/en_Risk of Rain 2.md | 16 ++-- worlds/ror2/docs/setup_en.md | 2 +- .../sa2b/docs/en_Sonic Adventure 2 Battle.md | 4 +- worlds/shivers/docs/en_Shivers.md | 4 +- worlds/shivers/docs/setup_en.md | 4 +- worlds/sm/docs/en_Super Metroid.md | 4 +- worlds/sm/docs/multiworld_en.md | 8 +- worlds/sm64ex/docs/en_Super Mario 64.md | 6 +- worlds/sm64ex/docs/setup_en.md | 4 +- worlds/smw/docs/en_Super Mario World.md | 4 +- worlds/smw/docs/setup_en.md | 4 +- worlds/smz3/docs/en_SMZ3.md | 4 +- worlds/smz3/docs/multiworld_en.md | 8 +- worlds/soe/docs/en_Secret of Evermore.md | 4 +- worlds/soe/docs/multiworld_en.md | 8 +- worlds/spire/docs/en_Slay the Spire.md | 4 +- worlds/spire/docs/slay-the-spire_en.md | 4 +- .../stardew_valley/docs/en_Stardew Valley.md | 75 ++++++++++++------- worlds/stardew_valley/docs/setup_en.md | 32 +++++--- worlds/subnautica/docs/en_Subnautica.md | 4 +- worlds/subnautica/docs/setup_en.md | 2 +- worlds/terraria/docs/en_Terraria.md | 6 +- worlds/terraria/docs/setup_en.md | 2 +- worlds/timespinner/docs/en_Timespinner.md | 4 +- worlds/timespinner/docs/setup_en.md | 4 +- worlds/tloz/docs/en_The Legend of Zelda.md | 4 +- worlds/tloz/docs/multiworld_en.md | 8 +- worlds/undertale/docs/en_Undertale.md | 4 +- worlds/undertale/docs/setup_en.md | 2 +- worlds/v6/docs/en_VVVVVV.md | 6 +- worlds/v6/docs/setup_en.md | 2 +- worlds/wargroove/docs/en_Wargroove.md | 4 +- worlds/wargroove/docs/wargroove_en.md | 2 +- worlds/witness/docs/en_The Witness.md | 8 +- worlds/zillion/docs/en_Zillion.md | 4 +- worlds/zillion/docs/setup_en.md | 4 +- 92 files changed, 290 insertions(+), 255 deletions(-) diff --git a/worlds/adventure/docs/en_Adventure.md b/worlds/adventure/docs/en_Adventure.md index c39e0f7d919d..f5216e9145b2 100644 --- a/worlds/adventure/docs/en_Adventure.md +++ b/worlds/adventure/docs/en_Adventure.md @@ -1,11 +1,11 @@ # Adventure -## Where is the settings page? -The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file. +## Where is the options page? +The [player options page for Adventure](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All -Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized, +Adventure items are added to the multiworld item pool. Depending on the `dragon_rando_type` value, dragon locations may be randomized, slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates' can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist to reduce their speeds. @@ -15,7 +15,7 @@ Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle ## Which items can be in another player's world? All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on -settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found. +options, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found. ## What is considered a location check in Adventure? Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item. @@ -41,7 +41,7 @@ A message is shown in the client log. While empty handed, the player can press order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions. -## What are recommended settings to tweak for beginners to the rando? +## What are recommended options to tweak for beginners to the rando? Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or the credits room. diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md index 7378a018c7c1..94a735bb74f4 100644 --- a/worlds/adventure/docs/setup_en.md +++ b/worlds/adventure/docs/setup_en.md @@ -41,7 +41,7 @@ an experience customized for their taste, and different players in the same mult ### Where do I get a YAML file? -You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings) +You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options) ### What are recommended settings to tweak for beginners to the rando? Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to diff --git a/worlds/alttp/docs/en_A Link to the Past.md b/worlds/alttp/docs/en_A Link to the Past.md index 6808f69e759f..1a2cb310ce07 100644 --- a/worlds/alttp/docs/en_A Link to the Past.md +++ b/worlds/alttp/docs/en_A Link to the Past.md @@ -1,8 +1,8 @@ # A Link to the Past -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/archipidle/docs/en_ArchipIDLE.md b/worlds/archipidle/docs/en_ArchipIDLE.md index 3d57e3a0551a..c3b396c64901 100644 --- a/worlds/archipidle/docs/en_ArchipIDLE.md +++ b/worlds/archipidle/docs/en_ArchipIDLE.md @@ -6,8 +6,8 @@ ArchipIDLE was originally the 2022 Archipelago April Fools' Day joke. It is an i on regular intervals. Updated annually with more items, gimmicks, and features, the game is visible only during the month of April. -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. diff --git a/worlds/archipidle/docs/guide_en.md b/worlds/archipidle/docs/guide_en.md index e1a6532992b5..f9d7f08aab83 100644 --- a/worlds/archipidle/docs/guide_en.md +++ b/worlds/archipidle/docs/guide_en.md @@ -1,7 +1,7 @@ # ArchipIdle Setup Guide ## Joining a MultiWorld Game -1. Generate a `.yaml` file from the [ArchipIDLE Player Settings Page](/games/ArchipIDLE/player-settings) +1. Generate a `.yaml` file from the [ArchipIDLE Player Options Page](/games/ArchipIDLE/player-options) 2. Open the ArchipIDLE Client in your web browser by either: - Navigate to the [ArchipIDLE Client](http://idle.multiworld.link) - Download the client and run it locally from the diff --git a/worlds/bk_sudoku/docs/en_Sudoku.md b/worlds/bk_sudoku/docs/en_Sudoku.md index d69514752460..dae5a9e3e513 100644 --- a/worlds/bk_sudoku/docs/en_Sudoku.md +++ b/worlds/bk_sudoku/docs/en_Sudoku.md @@ -8,6 +8,6 @@ BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku cli After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. -## Where is the settings page? +## Where is the options page? -There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. +There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld. diff --git a/worlds/blasphemous/docs/en_Blasphemous.md b/worlds/blasphemous/docs/en_Blasphemous.md index 1ff7f5a9035c..a99eea6fa4b8 100644 --- a/worlds/blasphemous/docs/en_Blasphemous.md +++ b/worlds/blasphemous/docs/en_Blasphemous.md @@ -1,8 +1,8 @@ # Blasphemous -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/bumpstik/docs/en_Bumper Stickers.md b/worlds/bumpstik/docs/en_Bumper Stickers.md index 17a66d76122a..42599ec64121 100644 --- a/worlds/bumpstik/docs/en_Bumper Stickers.md +++ b/worlds/bumpstik/docs/en_Bumper Stickers.md @@ -1,7 +1,7 @@ # Bumper Stickers -## Where is the settings page? -The [player settings page for Bumper Stickers](../player-settings) contains all the options you need to configure and export a config file. +## Where is the options page? +The [player options page for Bumper Stickers](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? Playing this in Archipelago is a very different experience from Classic mode. You start with a very small board and a set of tasks. Completing those tasks will give you a larger board and more, harder tasks. In addition, special types of bumpers exist that must be cleared in order to progress. diff --git a/worlds/checksfinder/docs/en_ChecksFinder.md b/worlds/checksfinder/docs/en_ChecksFinder.md index 96fb0529df64..c9569376c5f6 100644 --- a/worlds/checksfinder/docs/en_ChecksFinder.md +++ b/worlds/checksfinder/docs/en_ChecksFinder.md @@ -1,8 +1,8 @@ # ChecksFinder -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What is considered a location check in ChecksFinder? diff --git a/worlds/checksfinder/docs/setup_en.md b/worlds/checksfinder/docs/setup_en.md index 77eca6f71b34..673b34900af7 100644 --- a/worlds/checksfinder/docs/setup_en.md +++ b/worlds/checksfinder/docs/setup_en.md @@ -15,7 +15,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [ChecksFinder Player Settings Page](/games/ChecksFinder/player-settings) +You can customize your options by visiting the [ChecksFinder Player Options Page](/games/ChecksFinder/player-options) ### Generating a ChecksFinder game diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md index 862454f5c613..e9cb164fecbf 100644 --- a/worlds/clique/docs/en_Clique.md +++ b/worlds/clique/docs/en_Clique.md @@ -10,7 +10,7 @@ wait for someone else in the multiworld to "activate" their button before they c Clique can be played on most modern HTML5-capable browsers. -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. diff --git a/worlds/dark_souls_3/docs/en_Dark Souls III.md b/worlds/dark_souls_3/docs/en_Dark Souls III.md index e844925df1ea..f31358bb9c2f 100644 --- a/worlds/dark_souls_3/docs/en_Dark Souls III.md +++ b/worlds/dark_souls_3/docs/en_Dark Souls III.md @@ -1,8 +1,8 @@ # Dark Souls III -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -13,7 +13,7 @@ location "Titanite Shard #5" is the fifth titanite shard you pick up, no matter happens when you randomize Estus Shards and Undead Bone Shards. It's also possible to randomize the upgrade level of weapons and shields as well as their infusions (if they can have -one). Additionally, there are settings that can make the randomized experience more convenient or more interesting, such as +one). Additionally, there are options that can make the randomized experience more convenient or more interesting, such as removing weapon requirements or auto-equipping whatever equipment you most recently received. The goal is to find the four "Cinders of a Lord" items randomized into the multiworld and defeat the Soul of Cinder. diff --git a/worlds/dark_souls_3/docs/setup_en.md b/worlds/dark_souls_3/docs/setup_en.md index 72c665af9507..ed2cb867b827 100644 --- a/worlds/dark_souls_3/docs/setup_en.md +++ b/worlds/dark_souls_3/docs/setup_en.md @@ -51,5 +51,5 @@ add it at the root folder of your game (e.g. "SteamLibrary\steamapps\common\DARK ## Where do I get a config file? -The [Player Settings](/games/Dark%20Souls%20III/player-settings) page on the website allows you to -configure your personal settings and export them into a config file. +The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to +configure your personal options and export them into a config file. diff --git a/worlds/dkc3/docs/en_Donkey Kong Country 3.md b/worlds/dkc3/docs/en_Donkey Kong Country 3.md index 2041f0a41bd2..83ba25a9960b 100644 --- a/worlds/dkc3/docs/en_Donkey Kong Country 3.md +++ b/worlds/dkc3/docs/en_Donkey Kong Country 3.md @@ -1,8 +1,8 @@ # Donkey Kong Country 3 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/dkc3/docs/setup_en.md b/worlds/dkc3/docs/setup_en.md index 236d1cb8ad32..c832bcd702e9 100644 --- a/worlds/dkc3/docs/setup_en.md +++ b/worlds/dkc3/docs/setup_en.md @@ -45,8 +45,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options) ### Verifying your config file @@ -55,8 +55,8 @@ validator page: [YAML Validation page](/check) ## Generating a Single-Player Game -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [Donkey Kong Country 3 Player Settings Page](/games/Donkey%20Kong%20Country%203/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [Donkey Kong Country 3 Player Options Page](/games/Donkey%20Kong%20Country%203/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. diff --git a/worlds/dlcquest/docs/en_DLCQuest.md b/worlds/dlcquest/docs/en_DLCQuest.md index eaccc8ff0a46..0ae8f2291e1f 100644 --- a/worlds/dlcquest/docs/en_DLCQuest.md +++ b/worlds/dlcquest/docs/en_DLCQuest.md @@ -1,8 +1,8 @@ # DLC Quest -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/dlcquest/docs/setup_en.md b/worlds/dlcquest/docs/setup_en.md index 47e22e0f74ee..7c82b9d69fc0 100644 --- a/worlds/dlcquest/docs/setup_en.md +++ b/worlds/dlcquest/docs/setup_en.md @@ -19,7 +19,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [DLC Quest Player Settings Page](/games/DLCQuest/player-settings) +You can customize your options by visiting the [DLC Quest Player Options Page](/games/DLCQuest/player-options) ## Joining a MultiWorld Game diff --git a/worlds/doom_1993/docs/en_DOOM 1993.md b/worlds/doom_1993/docs/en_DOOM 1993.md index 0419741bc308..ea7e3200307f 100644 --- a/worlds/doom_1993/docs/en_DOOM 1993.md +++ b/worlds/doom_1993/docs/en_DOOM 1993.md @@ -1,8 +1,8 @@ # DOOM 1993 -## Where is the settings page? +## Where is the options page? -The [player settings page](../player-settings) contains the options needed to configure your game session. +The [player options page](../player-options) contains the options needed to configure your game session. ## What does randomization do to this game? diff --git a/worlds/doom_ii/docs/en_DOOM II.md b/worlds/doom_ii/docs/en_DOOM II.md index d561745b76c2..d02f75cb6c8f 100644 --- a/worlds/doom_ii/docs/en_DOOM II.md +++ b/worlds/doom_ii/docs/en_DOOM II.md @@ -1,8 +1,8 @@ # DOOM II -## Where is the settings page? +## Where is the options page? -The [player settings page](../player-settings) contains the options needed to configure your game session. +The [player options page](../player-options) contains the options needed to configure your game session. ## What does randomization do to this game? diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md index dbc33d05dfde..94d2a5505bda 100644 --- a/worlds/factorio/docs/en_Factorio.md +++ b/worlds/factorio/docs/en_Factorio.md @@ -1,8 +1,8 @@ # Factorio -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -36,7 +36,7 @@ inventory. ## What is EnergyLink? EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. -In Factorio, if enabled in the player settings, EnergyLink Bridge buildings can be crafted and placed, which allow +In Factorio, if enabled in the player options, EnergyLink Bridge buildings can be crafted and placed, which allow depositing excess energy and supplementing energy deficits, much like Accumulators. Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy diff --git a/worlds/factorio/docs/setup_en.md b/worlds/factorio/docs/setup_en.md index b6d45459253a..0b4ccee0d7f2 100644 --- a/worlds/factorio/docs/setup_en.md +++ b/worlds/factorio/docs/setup_en.md @@ -25,8 +25,8 @@ options. ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Factorio player options page: [Factorio Options Page](/games/Factorio/player-options) ### Verifying your config file @@ -133,7 +133,7 @@ This allows you to host your own Factorio game. For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server, you can also issue the `!help` command to learn about additional commands like `!hint`. For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and -[Other Settings](#other-settings). +[Other Options](#other-options). ## Allowing Other People to Join Your Game @@ -148,11 +148,11 @@ For more information about the commands you can use, see the [Commands Guide](/t By default, peaceful mode is disabled. There are two methods to enable peaceful mode: ### By config file -You can specify Factorio game settings such as peaceful mode and terrain and resource generation parameters in your -config .yaml file by including the `world_gen` setting. This setting is currently not supported by the web UI, so you'll +You can specify Factorio game options such as peaceful mode and terrain and resource generation parameters in your +config .yaml file by including the `world_gen` option. This option is currently not supported by the web UI, so you'll have to manually create or edit your config file with a text editor of your choice. The [template file](/static/generated/configs/Factorio.yaml) is a good starting point and contains the default value of -the `world_gen` setting. If you already have a config file you may also just copy that setting over from the template. +the `world_gen` option. If you already have a config file you may also just copy that option over from the template. To enable peaceful mode, simply replace `peaceful_mode: false` with `peaceful_mode: true`. Finally, use the [.yaml checker](/check) to ensure your file is valid. @@ -165,7 +165,7 @@ enable peaceful mode by entering the following commands into your Archipelago Fa ``` (If this warns you that these commands may disable achievements, you may need to repeat them for them to take effect.) -## Other Settings +## Other Options ### filter_item_sends diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 59fa85d91613..889bb46e0c35 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -1,9 +1,9 @@ # Final Fantasy 1 (NES) -## Where is the settings page? +## Where is the options page? -Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled entirely by the original randomzier. You -can find an exhaustive list of documented settings on the FFR +Unlike most games on Archipelago.gg, Final Fantasy 1's options are controlled entirely by the original randomzier. You +can find an exhaustive list of documented options on the FFR website: [FF1R Website](https://finalfantasyrandomizer.com/) ## What does randomization do to this game? diff --git a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md index dd4ea354fab1..a652d4e5adcd 100644 --- a/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md +++ b/worlds/ffmq/docs/en_Final Fantasy Mystic Quest.md @@ -1,8 +1,8 @@ # Final Fantasy Mystic Quest -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/ffmq/docs/setup_en.md b/worlds/ffmq/docs/setup_en.md index 61b8d7e306bf..de2493df74f2 100644 --- a/worlds/ffmq/docs/setup_en.md +++ b/worlds/ffmq/docs/setup_en.md @@ -39,8 +39,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options) ### Verifying your config file @@ -49,8 +49,8 @@ validator page: [YAML Validation page](/mysterycheck) ## Generating a Single-Player Game -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [Final Fantasy Mystic Quest Player Settings Page](/games/Final%20Fantasy%20Mystic%20Quest/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [Final Fantasy Mystic Quest Player Options Page](/games/Final%20Fantasy%20Mystic%20Quest/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your `.apmq` patch file. diff --git a/worlds/heretic/docs/en_Heretic.md b/worlds/heretic/docs/en_Heretic.md index 97d371de2c2f..a7ae3d1ea748 100644 --- a/worlds/heretic/docs/en_Heretic.md +++ b/worlds/heretic/docs/en_Heretic.md @@ -1,8 +1,8 @@ # Heretic -## Where is the settings page? +## Where is the options page? -The [player settings page](../player-settings) contains the options needed to configure your game session. +The [player options page](../player-options) contains the options needed to configure your game session. ## What does randomization do to this game? diff --git a/worlds/hk/docs/en_Hollow Knight.md b/worlds/hk/docs/en_Hollow Knight.md index 6e74c8a1fbbc..e31eb892a004 100644 --- a/worlds/hk/docs/en_Hollow Knight.md +++ b/worlds/hk/docs/en_Hollow Knight.md @@ -1,8 +1,8 @@ # Hollow Knight -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -12,7 +12,7 @@ Shop costs are presently always randomized. ## What Hollow Knight items can appear in other players' worlds? -This is dependent entirely upon your YAML settings. Some examples include: charms, grubs, lifeblood cocoons, geo, etc. +This is dependent entirely upon your YAML options. Some examples include: charms, grubs, lifeblood cocoons, geo, etc. ## What does another world's item look like in Hollow Knight? diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index fef0f051fec0..b85818f30eca 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -31,7 +31,7 @@ You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) h about why Archipelago uses YAML files and what they're for. ### Where do I get a YAML? -You can use the [game settings page for Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago +You can use the [game options page for Hollow Knight](/games/Hollow%20Knight/player-options) here on the Archipelago website to generate a YAML using a graphical interface. ### Joining an Archipelago Game in Hollow Knight diff --git a/worlds/hylics2/docs/en_Hylics 2.md b/worlds/hylics2/docs/en_Hylics 2.md index cb201a52bb3e..4ebd0742eba5 100644 --- a/worlds/hylics2/docs/en_Hylics 2.md +++ b/worlds/hylics2/docs/en_Hylics 2.md @@ -1,8 +1,8 @@ # Hylics 2 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index f08a1fc51fc0..5aae7ad3a70e 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -4,9 +4,9 @@ This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss. -

Where is the settings page

+

Where is the options page

-The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.

What is randomized in this game?

diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index 96b3b936f338..70b3a24abeb4 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -2,7 +2,7 @@

Quick Links

- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en) -- [Player Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings) +- [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)

Required Software:

`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) diff --git a/worlds/ladx/docs/en_Links Awakening DX.md b/worlds/ladx/docs/en_Links Awakening DX.md index bceda4bc89c1..91a34107c169 100644 --- a/worlds/ladx/docs/en_Links Awakening DX.md +++ b/worlds/ladx/docs/en_Links Awakening DX.md @@ -1,8 +1,8 @@ # Links Awakening DX -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -85,7 +85,7 @@ Title screen graphics by toomanyteeth✨ (https://instagram.com/toomanyyyteeth)

The walrus is moved a bit, so that you can access the desert without taking Marin on a date.

Logic

-

Depending on your settings, you can only steal after you find the sword, always, or never.

+

Depending on your options, you can only steal after you find the sword, always, or never.

Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.

Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.

D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.

diff --git a/worlds/ladx/docs/setup_en.md b/worlds/ladx/docs/setup_en.md index aad077d73037..d12f9b8b3b84 100644 --- a/worlds/ladx/docs/setup_en.md +++ b/worlds/ladx/docs/setup_en.md @@ -35,8 +35,8 @@ options. ### Where do I get a config file? -The [Player Settings](/games/Links%20Awakening%20DX/player-settings) page on the website allows you to configure -your personal settings and export a config file from them. +The [Player Options](/games/Links%20Awakening%20DX/player-options) page on the website allows you to configure +your personal options and export a config file from them. ### Verifying your config file @@ -45,7 +45,7 @@ If you would like to validate your config file to make sure it works, you may do ## Generating a Single-Player Game -1. Navigate to the [Player Settings](/games/Links%20Awakening%20DX/player-settings) page, configure your options, +1. Navigate to the [Player Options](/games/Links%20Awakening%20DX/player-options) page, configure your options, and click the "Generate Game" button. 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. diff --git a/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md index 90a79f8bd986..9239f741b436 100644 --- a/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md +++ b/worlds/landstalker/docs/en_Landstalker - The Treasures of King Nole.md @@ -1,8 +1,8 @@ # Landstalker: The Treasures of King Nole -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains most of the options you need to +The [player options page for this game](../player-options) contains most of the options you need to configure and export a config file. ## What does randomization do to this game? @@ -35,7 +35,7 @@ All key doors are gone, except three of them : The secondary shop of Mercator requiring to do the traders sidequest in the original game is now unlocked by having **Buyer Card** in your inventory. -You will need as many **jewels** as specified in the settings to use the teleporter to go to Kazalt and the final dungeon. +You will need as many **jewels** as specified in the options to use the teleporter to go to Kazalt and the final dungeon. If you find and use the **Lithograph**, it will tell you in which world are each one of your jewels. Each seed, there is a random dungeon which is chosen to be the "dark dungeon" where you won't see anything unless you @@ -54,7 +54,7 @@ be significantly harder, both combat-wise and logic-wise. Having fully open & shuffled teleportation trees is an interesting way to play, but is discouraged for beginners as well since it can force you to go in late-game zones with few Life Stocks. -Overall, the default settings are good for a beginner-friendly seed, and if you don't feel too confident, you can also +Overall, the default options are good for a beginner-friendly seed, and if you don't feel too confident, you can also lower the combat difficulty to make it more forgiving. *Have fun on your adventure!* diff --git a/worlds/landstalker/docs/landstalker_setup_en.md b/worlds/landstalker/docs/landstalker_setup_en.md index 32e46a4b3354..30f85dd8f19b 100644 --- a/worlds/landstalker/docs/landstalker_setup_en.md +++ b/worlds/landstalker/docs/landstalker_setup_en.md @@ -30,8 +30,10 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows -you to easily configure your personal settings + +The [Player Options Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-options) on the website allows +you to easily configure your personal options. + ## How-to-play diff --git a/worlds/lingo/docs/en_Lingo.md b/worlds/lingo/docs/en_Lingo.md index cff0581d9b2f..c7e1bfc8192e 100644 --- a/worlds/lingo/docs/en_Lingo.md +++ b/worlds/lingo/docs/en_Lingo.md @@ -1,8 +1,8 @@ # Lingo -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md index d24c4ef9f9af..1080a77d54f4 100644 --- a/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md +++ b/worlds/lufia2ac/docs/en_Lufia II Ancient Cave.md @@ -1,8 +1,8 @@ # Lufia II - Rise of the Sinistrals (Ancient Cave) -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/lufia2ac/docs/setup_en.md b/worlds/lufia2ac/docs/setup_en.md index 4d4ea811abcf..d82853d4fddf 100644 --- a/worlds/lufia2ac/docs/setup_en.md +++ b/worlds/lufia2ac/docs/setup_en.md @@ -39,8 +39,8 @@ options. ### Where do I get a config file? -The [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page on the website allows you to configure -your personal settings and export a config file from them. +The [Player Options](/games/Lufia%20II%20Ancient%20Cave/player-options) page on the website allows you to configure +your personal options and export a config file from them. ### Verifying your config file @@ -49,7 +49,7 @@ If you would like to validate your config file to make sure it works, you may do ## Generating a Single-Player Game -1. Navigate to the [Player Settings](/games/Lufia%20II%20Ancient%20Cave/player-settings) page, configure your options, +1. Navigate to the [Player Options](/games/Lufia%20II%20Ancient%20Cave/player-options) page, configure your options, and click the "Generate Game" button. 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. diff --git a/worlds/meritous/docs/en_Meritous.md b/worlds/meritous/docs/en_Meritous.md index bceae7d9ae2b..d119c3634c58 100644 --- a/worlds/meritous/docs/en_Meritous.md +++ b/worlds/meritous/docs/en_Meritous.md @@ -1,7 +1,7 @@ # Meritous -## Where is the settings page? -The [player settings page for Meritous](../player-settings) contains all the options you need to configure and export a config file. +## Where is the options page? +The [player options page for Meritous](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? The PSI Enhancement Tiles have become general-purpose Item Caches, and all upgrades and artifacts are added to the multiworld item pool. Optionally, the progression-critical PSI Keys can also be added to the pool, as well as monster evolution traps which (in vanilla) trigger when bosses are defeated. diff --git a/worlds/meritous/docs/setup_en.md b/worlds/meritous/docs/setup_en.md index 63f8657b63bb..9b91f12106de 100644 --- a/worlds/meritous/docs/setup_en.md +++ b/worlds/meritous/docs/setup_en.md @@ -40,9 +40,9 @@ Eventually, this process will be moved to in-game menus for better ease of use. ## Finishing the Game -Your initial goal is to find all three PSI Keys. Depending on your YAML settings, these may be located on pedestals in special rooms in the Atlas Dome, or they may be scattered across other players' worlds. These PSI Keys are then brought to their respective locations in the Dome, where you will be subjected to a boss battle. Once all three bosses are defeated, this unlocks the Cursed Seal, hidden in the farthest-away location from the Entrance. The Compass tiles can help you find your way to these locations. +Your initial goal is to find all three PSI Keys. Depending on your YAML options, these may be located on pedestals in special rooms in the Atlas Dome, or they may be scattered across other players' worlds. These PSI Keys are then brought to their respective locations in the Dome, where you will be subjected to a boss battle. Once all three bosses are defeated, this unlocks the Cursed Seal, hidden in the farthest-away location from the Entrance. The Compass tiles can help you find your way to these locations. -At minimum, every seed will require you to find the Cursed Seal and bring it back to the Entrance. The goal can then vary based on your `goal` YAML setting: +At minimum, every seed will require you to find the Cursed Seal and bring it back to the Entrance. The goal can then vary based on your `goal` YAML option: - `return_the_cursed_seal`: You will fight the final boss, but win or lose, a victory will be posted. - `any_ending`: You must defeat the final boss. diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md index c700f59a51c7..3a69a7f59a22 100644 --- a/worlds/minecraft/docs/en_Minecraft.md +++ b/worlds/minecraft/docs/en_Minecraft.md @@ -1,8 +1,8 @@ # Minecraft -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/minecraft/docs/minecraft_en.md b/worlds/minecraft/docs/minecraft_en.md index b71ed930a5d8..e0b5ae3b98b5 100644 --- a/worlds/minecraft/docs/minecraft_en.md +++ b/worlds/minecraft/docs/minecraft_en.md @@ -15,7 +15,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [Minecraft Player Settings Page](/games/Minecraft/player-settings) +You can customize your options by visiting the [Minecraft Player Options Page](/games/Minecraft/player-options) ## Joining a MultiWorld Game diff --git a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md index 7ffa4665fd2a..bb9d2c15af2c 100644 --- a/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md +++ b/worlds/mmbn3/docs/en_MegaMan Battle Network 3.md @@ -1,8 +1,8 @@ # MegaMan Battle Network 3 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/mmbn3/docs/setup_en.md b/worlds/mmbn3/docs/setup_en.md index e9181ea54881..44a6b9c14448 100644 --- a/worlds/mmbn3/docs/setup_en.md +++ b/worlds/mmbn3/docs/setup_en.md @@ -53,8 +53,8 @@ an experience customized for their taste, and different players in the same mult ### Where do I get a YAML file? -You can customize your settings by visiting the -[MegaMan Battle Network 3 Player Settings Page](/games/MegaMan%20Battle%20Network%203/player-settings) +You can customize your options by visiting the +[MegaMan Battle Network 3 Player Options Page](/games/MegaMan%20Battle%20Network%203/player-options) ## Joining a MultiWorld Game diff --git a/worlds/musedash/docs/en_Muse Dash.md b/worlds/musedash/docs/en_Muse Dash.md index 008fd4d2df0c..29d1465ed098 100644 --- a/worlds/musedash/docs/en_Muse Dash.md +++ b/worlds/musedash/docs/en_Muse Dash.md @@ -2,10 +2,10 @@ ## Quick Links - [Setup Guide](../../../tutorial/Muse%20Dash/setup/en) -- [Settings Page](../player-settings) +- [Options Page](../player-options) ## What Does Randomization do to this Game? -- You will be given a number of starting songs. The number of which depends on your settings. +- You will be given a number of starting songs. The number of which depends on your options. - Completing any song will give you 1 or 2 rewards. - The rewards for completing songs will range from songs to traps and **Music Sheets**. diff --git a/worlds/musedash/docs/setup_en.md b/worlds/musedash/docs/setup_en.md index ebf165c7dd78..312cdbd1958f 100644 --- a/worlds/musedash/docs/setup_en.md +++ b/worlds/musedash/docs/setup_en.md @@ -2,7 +2,7 @@ ## Quick Links - [Main Page](../../../../games/Muse%20Dash/info/en) -- [Settings Page](../../../../games/Muse%20Dash/player-settings) +- [Options Page](../../../../games/Muse%20Dash/player-options) ## Required Software @@ -27,7 +27,7 @@ If you've successfully installed everything, a button will appear in the bottom right which will allow you to log into an Archipelago server. ## Generating a MultiWorld Game -1. Visit the [Player Settings](/games/Muse%20Dash/player-settings) page and configure the game-specific settings to your taste. +1. Visit the [Player Options](/games/Muse%20Dash/player-options) page and configure the game-specific options to your taste. 2. Export your yaml file and use it to generate a new randomized game - (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](/tutorial/Archipelago/setup/en)) diff --git a/worlds/noita/docs/en_Noita.md b/worlds/noita/docs/en_Noita.md index b1480068e96c..1e560cfcb748 100644 --- a/worlds/noita/docs/en_Noita.md +++ b/worlds/noita/docs/en_Noita.md @@ -1,15 +1,15 @@ # Noita -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? Noita is a procedurally generated action roguelike. During runs in Noita you will find potions, wands, spells, perks, chests, etc. Shop items, chests/hearts hidden in the environment, and pedestal items will be replaced with location -checks. Orbs and boss drops can give location checks as well, if they are enabled in the settings. +checks. Orbs and boss drops can give location checks as well, if their respective options are enabled. Noita items that can be found in other players' games include specific perks, orbs (optional), wands, hearts, gold, potions, and other items. If traps are enabled, some randomized negative effects can affect your game when found. @@ -50,9 +50,9 @@ Traps consist of all "Bad" and "Awful" events from Noita's native stream integra ## How many location checks are there? -When using the default settings, there are 109 location checks. The number of checks in the game is dependent on the settings that you choose. -Please check the information boxes next to the settings when setting up your YAML to see how many checks the individual options add. -There are always 42 Holy Mountain checks and 4 Secret Shop checks in the pool which are not affected by your YAML settings. +When using the default options, there are 109 location checks. The number of checks in the game is dependent on the options that you choose. +Please check the information boxes next to the options when setting up your YAML to see how many checks the individual options add. +There are always 42 Holy Mountain checks and 4 Secret Shop checks in the pool which are not affected by your YAML options. ## What does another world's item look like in Noita? diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md index b67e840bb94c..25c6cbc948bc 100644 --- a/worlds/noita/docs/setup_en.md +++ b/worlds/noita/docs/setup_en.md @@ -44,7 +44,7 @@ Please note that Noita only allows you to type certain characters for your slot These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/`` ### Where do I get a YAML? -You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to +You can use the [game options page for Noita](/games/Noita/player-options) here on the Archipelago website to generate a YAML using a graphical interface. ## Poptracker Pack diff --git a/worlds/oot/docs/en_Ocarina of Time.md b/worlds/oot/docs/en_Ocarina of Time.md index fa8e148957c7..5a480d864124 100644 --- a/worlds/oot/docs/en_Ocarina of Time.md +++ b/worlds/oot/docs/en_Ocarina of Time.md @@ -1,8 +1,8 @@ # Ocarina of Time -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -37,4 +37,4 @@ business! The following commands are only available when using the OoTClient to play with Archipelago. - `/n64` Check N64 Connection State -- `/deathlink` Toggle deathlink from client. Overrides default setting. +- `/deathlink` Toggle deathlink from client. Overrides default option. diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md index 4d27019fa771..553f1820c3ea 100644 --- a/worlds/oot/docs/setup_en.md +++ b/worlds/oot/docs/setup_en.md @@ -50,8 +50,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Ocarina of Time Player Settings Page](/games/Ocarina%20of%20Time/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Ocarina of Time Player Options Page](/games/Ocarina%20of%20Time/player-options) ### Verifying your config file diff --git a/worlds/overcooked2/docs/en_Overcooked! 2.md b/worlds/overcooked2/docs/en_Overcooked! 2.md index 298c33683ce7..d4cb6fba1f9a 100644 --- a/worlds/overcooked2/docs/en_Overcooked! 2.md +++ b/worlds/overcooked2/docs/en_Overcooked! 2.md @@ -2,7 +2,7 @@ ## Quick Links - [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en) -- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [Options Page](../../../../games/Overcooked!%202/player-options) - [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) ## How Does Randomizer Work in the Kitchen? @@ -55,7 +55,7 @@ The following items were invented for Randomizer: - Ramp Buttons (x7) - Bonus Star (Filler Item*) -**Note: Bonus star count varies with settings* +**Note: Bonus star count varies with options* ## Other Game Modifications diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index 1b21642cfe03..9f9eae5fc1ae 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -2,7 +2,7 @@ ## Quick Links - [Main Page](../../../../games/Overcooked!%202/info/en) -- [Settings Page](../../../../games/Overcooked!%202/player-settings) +- [Options Page](../../../../games/Overcooked!%202/player-options) - [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding) ## Required Software @@ -49,9 +49,9 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder ## Generate a MultiWorld Game -1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste +1. Visit the [Player Options](../../../../games/Overcooked!%202/player-options) page and configure the game-specific options to taste -*By default, these settings will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Settings](../../../../weighted-settings) page* +*By default, these options will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Options](../../../../weighted-options) page* 2. Export your yaml file and use it to generate a new randomized game @@ -84,11 +84,11 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved. -To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting. +To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired option. ## Overworld Sequence Breaking -In the world's settings, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes: +In the world's options, there is an option called "Overworld Tricks" which allows the generator to make games which require doing tricks with the food truck to complete. This includes: - Dashing across gaps diff --git a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md index dc55aca0f09f..1e5c14eb99f5 100644 --- a/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md +++ b/worlds/pokemon_rb/docs/en_Pokemon Red and Blue.md @@ -1,8 +1,8 @@ # Pokémon Red and Blue -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -12,7 +12,7 @@ always able to be completed, but because of the item shuffle the player may need would in the vanilla game. A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc., -depending on your yaml settings. +depending on your yaml options. Many baseline changes are made to the game, including: @@ -21,13 +21,13 @@ Many baseline changes are made to the game, including: * You can hold B to run (or bike extra fast!). * You can hold select while talking to a trainer to re-battle them. * You can select "Pallet Warp" below the "Continue" option to warp to Pallet Town as you load your save. -* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings. +* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your options. * The S.S. Anne will never depart. * Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia City. You also cannot Surf onto the water from the end of Seafoam Islands going backwards if you have not yet dropped the boulders. * After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab -fossil scientist. This may require reviving a number of fossils, depending on your settings. +fossil scientist. This may require reviving a number of fossils, depending on your options. * Obedience depends on the total number of badges you have obtained instead of depending on specific badges. * Pokémon that evolve by trading can also evolve by reaching level 35. * Evolution stones are reusable key items. diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index c9344959f6b9..45b0175eac9d 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -47,7 +47,7 @@ an experience customized for their taste, and different players in the same mult ### Where do I get a YAML file? -You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon%20Red%20and%20Blue/player-settings) +You can generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Options Page](/games/Pokemon%20Red%20and%20Blue/player-options) It is important to note that the `game_version` option determines the ROM file that will be patched. Both the player and the person generating (if they are generating locally) will need the corresponding ROM file. @@ -72,7 +72,7 @@ And the following special characters (these each count as one character): ### Generating and Patching a Game -1. Create your settings file (YAML). +1. Create your options file (YAML). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). This will generate an output file for you. Your patch file will have a `.apred` or `.apblue` file extension. 3. Open `ArchipelagoLauncher.exe` @@ -114,5 +114,5 @@ Pokémon Red and Blue has a fully functional map tracker that supports auto-trac 3. Click on the "AP" symbol at the top. 4. Enter the AP address, slot name and password. -The rest should take care of itself! Items and checks will be marked automatically, and it even knows your settings - It +The rest should take care of itself! Items and checks will be marked automatically, and it even knows your options - It will hide checks & adjust logic accordingly. diff --git a/worlds/raft/docs/en_Raft.md b/worlds/raft/docs/en_Raft.md index 385377d45608..0c68e23d0019 100644 --- a/worlds/raft/docs/en_Raft.md +++ b/worlds/raft/docs/en_Raft.md @@ -1,7 +1,7 @@ # Raft -## Where is the settings page? -The player settings page for this game is located
here. It contains all the options +## Where is the options page? +The player options page for this game is located here. It contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/rogue_legacy/docs/en_Rogue Legacy.md b/worlds/rogue_legacy/docs/en_Rogue Legacy.md index c91dc0de6f7a..dd203c73ac26 100644 --- a/worlds/rogue_legacy/docs/en_Rogue Legacy.md +++ b/worlds/rogue_legacy/docs/en_Rogue Legacy.md @@ -1,9 +1,9 @@ # Rogue Legacy (PC) -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains most of the options you need to -configure and export a config file. Some settings can only be made in YAML, but an explanation can be found in the +The [player options page for this game](../player-options) contains most of the options you need to +configure and export a config file. Some options can only be made in YAML, but an explanation can be found in the [template yaml here](../../../static/generated/configs/Rogue%20Legacy.yaml). ## What does randomization do to this game? diff --git a/worlds/rogue_legacy/docs/rogue-legacy_en.md b/worlds/rogue_legacy/docs/rogue-legacy_en.md index e513d0f0ca18..fc9f6920178d 100644 --- a/worlds/rogue_legacy/docs/rogue-legacy_en.md +++ b/worlds/rogue_legacy/docs/rogue-legacy_en.md @@ -21,7 +21,7 @@ an experience customized for their taste, and different players in the same mult ### Where do I get a YAML file? -you can customize your settings by visiting the [Rogue Legacy Settings Page](/games/Rogue%20Legacy/player-settings). +you can customize your options by visiting the [Rogue Legacy Options Page](/games/Rogue%20Legacy/player-options). ### Connect to the MultiServer diff --git a/worlds/ror2/docs/en_Risk of Rain 2.md b/worlds/ror2/docs/en_Risk of Rain 2.md index d30edf888944..b2210e348d50 100644 --- a/worlds/ror2/docs/en_Risk of Rain 2.md +++ b/worlds/ror2/docs/en_Risk of Rain 2.md @@ -1,8 +1,8 @@ # Risk of Rain 2 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -23,7 +23,7 @@ Explore Mode: - Chests will continue to work as they did in Classic Mode, the difference being that each environment will have a set amount of items that can be sent out. In addition, shrines, radio scanners, newt altars, - and scavenger bags will need to be checked, depending on your settings. + and scavenger bags will need to be checked, depending on your options. This mode also makes each environment an item. In order to access a particular stage, you'll need it to be sent in the multiworld. @@ -32,7 +32,7 @@ Explore Mode: Just like in the original game, any way to "beat the game" counts as a win. This means beating one of the bosses on Commencement, The Planetarium, or A Moment, Whole. Alternatively, if you are new to the game and aren't very confident in being able to "beat the game", you can set **Final Stage Death is Win** to true -(You can turn this on in your player settings.) This will make it so dying on either Commencement or The Planetarium, +(You can turn this on in your player options.) This will make it so dying on either Commencement or The Planetarium, or **obliterating yourself in A Moment, Fractured** will count as your goal. **You do not need to complete all the location checks** to win; any item you don't collect may be released if the server options allow. @@ -48,12 +48,12 @@ then finish a normal mode run while keeping the items you received via the multi ## Can you play multiplayer? Yes! You can have a single multiplayer instance as one world in the multiworld. All the players involved need to have -the Archipelago mod, but only the host needs to configure the Archipelago settings. When someone finds an item for your +the Archipelago mod, but only the host needs to configure the Archipelago options. When someone finds an item for your world, all the connected players will receive a copy of the item, and the location check bar will increase whenever any player finds an item in Risk of Rain. You cannot have players with different player slots in the same co-op game instance. Only the host's Archipelago -settings apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could, +options apply, so each Risk of Rain 2 player slot in the multiworld needs to be a separate game instance. You could, for example, have two players trade off hosting and making progress on each other's player slot, but a single co-op instance can't make progress towards multiple player slots in the multiworld. @@ -69,7 +69,7 @@ The Risk of Rain items are: * `Legendary Item` (Red items) * `Lunar Item` (Blue items) * `Equipment` (Orange items) -* `Dio's Best Friend` (Used if you set the YAML setting `total_revives_available` above `0`) +* `Dio's Best Friend` (Used if you set the YAML option `total_revives_available` above `0`) * `Void Item` (Purple items) (needs dlc_sotv: enabled) Each item grants you a random in-game item from the category it belongs to. @@ -127,7 +127,7 @@ what item you sent out. If the message does not appear, this likely means that a ## What is the item pickup step? -The item pickup step is a setting in the YAML which allows you to set how many items you need to spawn before the _next_ item +The item pickup step is an option in the YAML which allows you to set how many items you need to spawn before the _next_ item that is spawned disappears (in a poof of smoke) and goes out to the multiworld. For instance, an item step of **1** means that every other chest will send an item to the multiworld. An item step of **2** means that every third chest sends out an item just as an item step of **0** would send an item on **each chest.** diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index 0fa99c071b9c..6acf2654a8b2 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -29,7 +29,7 @@ You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) h about why Archipelago uses YAML files and what they're for. ### Where do I get a YAML? -You can use the [game settings page](/games/Risk%20of%20Rain%202/player-settings) here on the Archipelago +You can use the [game options page](/games/Risk%20of%20Rain%202/player-options) here on the Archipelago website to generate a YAML using a graphical interface. diff --git a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md index 12ccc50ccd2c..e2f732ffe585 100644 --- a/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md +++ b/worlds/sa2b/docs/en_Sonic Adventure 2 Battle.md @@ -1,8 +1,8 @@ # Sonic Adventure 2: Battle -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/shivers/docs/en_Shivers.md b/worlds/shivers/docs/en_Shivers.md index 51730057b034..a92f8a6b7911 100644 --- a/worlds/shivers/docs/en_Shivers.md +++ b/worlds/shivers/docs/en_Shivers.md @@ -1,8 +1,8 @@ # Shivers -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a configuration file. ## What does randomization do to this game? diff --git a/worlds/shivers/docs/setup_en.md b/worlds/shivers/docs/setup_en.md index ee33bb70408e..187382ef643c 100644 --- a/worlds/shivers/docs/setup_en.md +++ b/worlds/shivers/docs/setup_en.md @@ -33,8 +33,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Shivers Player Settings Page](/games/Shivers/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Shivers Player Options Page](/games/Shivers/player-options) ### Verifying your config file diff --git a/worlds/sm/docs/en_Super Metroid.md b/worlds/sm/docs/en_Super Metroid.md index 5c87e026f634..c8c1d0faabee 100644 --- a/worlds/sm/docs/en_Super Metroid.md +++ b/worlds/sm/docs/en_Super Metroid.md @@ -1,8 +1,8 @@ # Super Metroid -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/sm/docs/multiworld_en.md b/worlds/sm/docs/multiworld_en.md index abd9f42f887c..8f30630bc96d 100644 --- a/worlds/sm/docs/multiworld_en.md +++ b/worlds/sm/docs/multiworld_en.md @@ -46,8 +46,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Super Metroid Player Options Page](/games/Super%20Metroid/player-options) ### Verifying your config file @@ -56,8 +56,8 @@ validator page: [YAML Validation page](/check) ## Generating a Single-Player Game -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [Super Metroid Player Options Page](/games/Super%20Metroid/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. diff --git a/worlds/sm64ex/docs/en_Super Mario 64.md b/worlds/sm64ex/docs/en_Super Mario 64.md index def6e2a37536..3d182a422081 100644 --- a/worlds/sm64ex/docs/en_Super Mario 64.md +++ b/worlds/sm64ex/docs/en_Super Mario 64.md @@ -1,9 +1,9 @@ # Super Mario 64 EX -## Where is the settings page? +## Where is the options page? -The player settings page for this game contains all the options you need to configure and export a config file. Player -settings page link: [SM64EX Player Settings Page](../player-settings). +The player options page for this game contains all the options you need to configure and export a config file. Player +options page link: [SM64EX Player Options Page](../player-options). ## What does randomization do to this game? All 120 Stars, the 3 Cap Switches, the Basement and Secound Floor Key are now Location Checks and may contain Items for different games as well diff --git a/worlds/sm64ex/docs/setup_en.md b/worlds/sm64ex/docs/setup_en.md index 2817d3c324c0..5983057f7d7a 100644 --- a/worlds/sm64ex/docs/setup_en.md +++ b/worlds/sm64ex/docs/setup_en.md @@ -70,7 +70,7 @@ After the compliation was successful, there will be a binary in your `sm64ex/bui ### Joining a MultiWorld Game To join, set the following launch options: `--sm64ap_name YourName --sm64ap_ip ServerIP:Port`. -For example, if you are hosting a game using the website, `YourName` will be the name from the Settings Page, `ServerIP` is `archipelago.gg` and `Port` the port given on the Archipelago room page. +For example, if you are hosting a game using the website, `YourName` will be the name from the Options Page, `ServerIP` is `archipelago.gg` and `Port` the port given on the Archipelago room page. Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requires a password. Should your name or password have spaces, enclose it in quotes: `"YourPassword"` and `"YourName"`. @@ -82,7 +82,7 @@ Failing to use a new file may make some locations unavailable. However, this can ### Playing offline -To play offline, first generate a seed on the game's settings page. +To play offline, first generate a seed on the game's options page. Create a room and download the `.apsm64ex` file, and start the game with the `--sm64ap_file "path/to/FileName"` launch argument. ### Optional: Using Batch Files to play offline and MultiWorld games diff --git a/worlds/smw/docs/en_Super Mario World.md b/worlds/smw/docs/en_Super Mario World.md index 87a96e558b65..26623bac83d3 100644 --- a/worlds/smw/docs/en_Super Mario World.md +++ b/worlds/smw/docs/en_Super Mario World.md @@ -1,8 +1,8 @@ # Super Mario World -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/smw/docs/setup_en.md b/worlds/smw/docs/setup_en.md index c8f408d6e256..825f0954c8f1 100644 --- a/worlds/smw/docs/setup_en.md +++ b/worlds/smw/docs/setup_en.md @@ -44,8 +44,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Super Mario World Player Settings Page](/games/Super%20Mario%20World/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Super Mario World Player Options Page](/games/Super%20Mario%20World/player-options) ### Verifying your config file diff --git a/worlds/smz3/docs/en_SMZ3.md b/worlds/smz3/docs/en_SMZ3.md index f0302d12f3a1..2116432ea7a1 100644 --- a/worlds/smz3/docs/en_SMZ3.md +++ b/worlds/smz3/docs/en_SMZ3.md @@ -1,8 +1,8 @@ # SMZ3 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/smz3/docs/multiworld_en.md b/worlds/smz3/docs/multiworld_en.md index 5e226798a30b..38c410faee02 100644 --- a/worlds/smz3/docs/multiworld_en.md +++ b/worlds/smz3/docs/multiworld_en.md @@ -43,8 +43,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [SMZ3 Player Options Page](/games/SMZ3/player-options) ### Verifying your config file @@ -53,8 +53,8 @@ validator page: [YAML Validation page](/check) ## Generating a Single-Player Game -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [SMZ3 Player Settings Page](/games/SMZ3/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [SMZ3 Player Options Page](/games/SMZ3/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. diff --git a/worlds/soe/docs/en_Secret of Evermore.md b/worlds/soe/docs/en_Secret of Evermore.md index 215a5387bb9f..98882b6ff7e3 100644 --- a/worlds/soe/docs/en_Secret of Evermore.md +++ b/worlds/soe/docs/en_Secret of Evermore.md @@ -1,8 +1,8 @@ # Secret of Evermore -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/soe/docs/multiworld_en.md b/worlds/soe/docs/multiworld_en.md index 89b1ff9fd931..065d43fc3a1b 100644 --- a/worlds/soe/docs/multiworld_en.md +++ b/worlds/soe/docs/multiworld_en.md @@ -25,8 +25,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Secret%20of%20Evermore/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [Secret of Evermore Player Options Page](/games/Secret%20of%20Evermore/player-options) ### Verifying your config file @@ -38,8 +38,8 @@ page: [YAML Validation page](/check) Stand-alone "Evermizer" has a way of balancing single-player games, but may not always be on par feature-wise. Head over to the [Evermizer Website](https://evermizer.com) if you want to try the official stand-alone, otherwise read below. -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [Secret of Evermore Player Settings Page](/games/Secret%20of%20Evermore/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [Secret of Evermore Player Options Page](/games/Secret%20of%20Evermore/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. diff --git a/worlds/spire/docs/en_Slay the Spire.md b/worlds/spire/docs/en_Slay the Spire.md index f4519455fa83..4591db58dc51 100644 --- a/worlds/spire/docs/en_Slay the Spire.md +++ b/worlds/spire/docs/en_Slay the Spire.md @@ -1,8 +1,8 @@ # Slay the Spire (PC) -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/spire/docs/slay-the-spire_en.md b/worlds/spire/docs/slay-the-spire_en.md index d85a17d987b2..daeb65415196 100644 --- a/worlds/spire/docs/slay-the-spire_en.md +++ b/worlds/spire/docs/slay-the-spire_en.md @@ -43,8 +43,8 @@ an experience customized for their taste, and different players in the same mult ### Where do I get a YAML file? -you can customize your settings by visiting -the [Slay the Spire Settings Page](/games/Slay%20the%20Spire/player-settings). +you can customize your options by visiting +the [Slay the Spire Options Page](/games/Slay%20the%20Spire/player-options). ### Connect to the MultiServer diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index 06c41a2f0563..789c12020c18 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -1,28 +1,35 @@ # Stardew Valley -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? -A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the player can customize their experience in their YAML file. +A vast number of objectives in Stardew Valley can be shuffled around the multiworld. Most of these are optional, and the +player can customize their experience in their YAML file. -For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that may be useful to the player. +For these objectives, if they have a vanilla reward, this reward will instead be an item in the multiworld. For the remaining +number of such objectives, there are a number of "Resource Pack" items, which are simply an item or a stack of items that +may be useful to the player. ## What is the goal of Stardew Valley? -The player can choose from a number of goals, using their YAML settings. +The player can choose from a number of goals, using their YAML options. - Complete the [Community Center](https://stardewvalleywiki.com/Bundles) - Succeed [Grandpa's Evaluation](https://stardewvalleywiki.com/Grandpa) with 4 lit candles - Reach the bottom of the [Pelican Town Mineshaft](https://stardewvalleywiki.com/The_Mines) -- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on floor 100 of the Skull Cavern +- Complete the [Cryptic Note](https://stardewvalleywiki.com/Secret_Notes#Secret_Note_.2310) quest, by meeting Mr Qi on +floor 100 of the Skull Cavern - Become a [Master Angler](https://stardewvalleywiki.com/Fish), which requires catching every fish in your slot -- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and minerals to the museum +- Restore [A Complete Collection](https://stardewvalleywiki.com/Museum), which requires donating all the artifacts and +minerals to the museum - Get the achievement [Full House](https://stardewvalleywiki.com/Children), which requires getting married and having two kids -- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires finding all 130 golden walnuts on ginger island -- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by completing all the monster slayer goals at the Adventure Guild +- Get recognized as the [Greatest Walnut Hunter](https://stardewvalleywiki.com/Golden_Walnut) by Mr Qi, which requires +finding all 130 golden walnuts on ginger island +- Become the [Protector of the Valley](https://stardewvalleywiki.com/Adventurer%27s_Guild#Monster_Eradication_Goals) by +completing all the monster slayer goals at the Adventure Guild - Complete a [Full Shipment](https://stardewvalleywiki.com/Shipping#Collection) by shipping every item in your slot - Become a [Gourmet Chef](https://stardewvalleywiki.com/Cooking) by cooking every recipe in your slot - Become a [Craft Master](https://stardewvalleywiki.com/Crafting) by crafting every item @@ -31,7 +38,9 @@ The player can choose from a number of goals, using their YAML settings. - Finish 100% of your randomizer slot with Allsanity: Complete every check in your slot - Achieve [Perfection](https://stardewvalleywiki.com/Perfection) in your save file -The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt to other settings in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal. +The following goals [Community Center, Master Angler, Protector of the Valley, Full Shipment and Gourmet Chef] will adapt +to other options in your slots, and are therefore customizable in duration and difficulty. For example, if you set "Fishsanity" +to "Exclude Legendaries", and pick the Master Angler goal, you will not need to catch the legendaries to complete the goal. ## What are location checks in Stardew Valley? @@ -39,7 +48,9 @@ Location checks in Stardew Valley always include: - [Community Center Bundles](https://stardewvalleywiki.com/Bundles) - [Mineshaft Chest Rewards](https://stardewvalleywiki.com/The_Mines#Remixed_Rewards) - [Traveling Merchant Items](https://stardewvalleywiki.com/Traveling_Cart) -- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), [Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), [Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc +- Isolated objectives such as the [beach bridge](https://stardewvalleywiki.com/The_Beach#Tide_Pools), +[Old Master Cannoli](https://stardewvalleywiki.com/Secret_Woods#Old_Master_Cannoli), +[Grim Reaper Statue](https://stardewvalleywiki.com/Golden_Scythe), etc There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: - [Tools and Fishing Rod Upgrades](https://stardewvalleywiki.com/Tools) @@ -51,7 +62,8 @@ There also are a number of location checks that are optional, and individual pla - [Story Quests](https://stardewvalleywiki.com/Quests#List_of_Story_Quests) - [Help Wanted Quests](https://stardewvalleywiki.com/Quests#Help_Wanted_Quests) - Participating in [Festivals](https://stardewvalleywiki.com/Festivals) -- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from [Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) +- [Special Orders](https://stardewvalleywiki.com/Quests#List_of_Special_Orders) from the town board, or from +[Mr Qi](https://stardewvalleywiki.com/Quests#List_of_Mr._Qi.27s_Special_Orders) - [Cropsanity](https://stardewvalleywiki.com/Crops): Growing and Harvesting individual crop types - [Fishsanity](https://stardewvalleywiki.com/Fish): Catching individual fish - [Museumsanity](https://stardewvalleywiki.com/Museum): Donating individual items, or reaching milestones for museum donations @@ -67,16 +79,21 @@ There also are a number of location checks that are optional, and individual pla Every normal reward from the above locations can be in another player's world. For the locations which do not include a normal reward, Resource Packs and traps are instead added to the pool. Traps are optional. -A player can enable some settings that will add some items to the pool that are relevant to progression +A player can enable some options that will add some items to the pool that are relevant to progression - Seasons Randomizer: * All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. - * At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. + * At the end of each month, the player can choose the next season, instead of following the vanilla season order. On +Seasons Randomizer, they can only choose from the seasons they have received. - Cropsanity: - * Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check - * The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. + * Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received + as multiworld items. Growing each seed and harvesting the resulting crop sends a location check + * The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells + unlimited seeds but in huge discount packs, not individually. - Museumsanity: - * The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. - * The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + * The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the + magic rock candy, are duplicated for convenience. + * The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. + She will sell these items as the player receives "Traveling Merchant Metal Detector" items. - TV Channels - Babies * Only if Friendsanity is enabled @@ -96,18 +113,22 @@ And lastly, some Archipelago-exclusive items exist in the pool, which are design ## When the player receives an item, what happens? -Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where it was found. -If an item is received while offline, it will be in the mailbox as soon as the player logs in. +Since Pelican Town is a remote area, it takes one business day for every item to reach the player. If an item is received +while online, it will appear in the player's mailbox the next morning, with a message from the sender telling them where +it was found. If an item is received while offline, it will be in the mailbox as soon as the player logs in. -Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter only serves to tell the player about it. +Some items will be directly attached to the letter, while some others will instead be a world-wide unlock, and the letter +only serves to tell the player about it. -In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the building that they have received, so they can choose its position. This construction will be completely free. +In some cases, like receiving Carpenter and Wizard buildings, the player will still need to go ask Robin to construct the +building that they have received, so they can choose its position. This construction will be completely free. ## Mods Some Stardew Valley mods unrelated to Archipelago are officially "supported". -This means that, for these specific mods, if you decide to include them in your yaml settings, the multiworld will be generated with the assumption that you will install and play with these mods. -The multiworld will contain related items and locations for these mods, the specifics will vary from mod to mod +This means that, for these specific mods, if you decide to include them in your yaml options, the multiworld will be generated +with the assumption that you will install and play with these mods. The multiworld will contain related items and locations +for these mods, the specifics will vary from mod to mod [Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) @@ -141,10 +162,12 @@ List of supported mods: * [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) * [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) -Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found [here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) +Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found +[here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) ## Multiplayer You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. -You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. +You can, however, send Stardew Valley objects as gifts from one Stardew Player to another Stardew player, using in-game +Joja Prime delivery, for a fee. This exclusive feature can be turned off if you don't want to send and receive gifts. diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 02d6979b7aee..3b51622d8d0a 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -5,15 +5,18 @@ - Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) - [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. + - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases + can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) * (Only for the TextClient) - Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley) - * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) that you can add to your yaml to include them with the Archipelago randomization + * There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md) + that you can add to your yaml to include them with the Archipelago randomization - * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. Mod interactions can be unpredictable, and no support will be offered for related bugs. + * It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so. + Mod interactions can be unpredictable, and no support will be offered for related bugs. * The more unsupported mods you have, and the bigger they are, the more likely things are to break. ## Configuring your YAML file @@ -25,14 +28,15 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew%20Valley/player-settings) +You can customize your options by visiting the [Stardew Valley Player Options Page](/games/Stardew%20Valley/player-options) ## Joining a MultiWorld Game ### Installing the mod - Install [SMAPI](https://smapi.io/) by following the instructions on their website -- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder +- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into +your Stardew Valley "Mods" folder - *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: - "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command% - Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly @@ -69,14 +73,20 @@ If the room's ip or port **does** change, you can follow these instructions to m ### Interacting with the MultiWorld from in-game -When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other Stardew-exclusive chat commands you can use. +When you connect, you should see a message in the chat informing you of the `!!help` command. This command will list other +Stardew-exclusive chat commands you can use. -Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game that supports chatting. +Furthermore, you can use the in-game chat box to talk to other players in the multiworld, assuming they are using a game +that supports chatting. -Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain items, or check missing locations. +Lastly, you can also run Archipelago commands `!help` from the in game chat box, allowing you to request hints on certain +items, or check missing locations. -It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the full history as well and may be better suited to read older messages. -For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run Stardew-exclusive commands. +It is important to note that the Stardew Valley chat is fairly limited in its capabilities. For example, it doesn't allow +scrolling up to see history that has been pushed off screen. The SMAPI console running alonside your game will have the +full history as well and may be better suited to read older messages. +For a better chat experience, you can also use the official Archipelago Text Client, altough it will not allow you to run +Stardew-exclusive commands. ### Playing with supported mods @@ -84,4 +94,4 @@ See the [Supported mods documentation](https://github.com/agilbert1412/StardewAr ### Multiplayer -You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. +You cannot play an Archipelago Slot in multiplayer at the moment. There are no short-term plans to support that feature. \ No newline at end of file diff --git a/worlds/subnautica/docs/en_Subnautica.md b/worlds/subnautica/docs/en_Subnautica.md index 5e99208b5f44..50004de5a003 100644 --- a/worlds/subnautica/docs/en_Subnautica.md +++ b/worlds/subnautica/docs/en_Subnautica.md @@ -1,8 +1,8 @@ # Subnautica -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/subnautica/docs/setup_en.md b/worlds/subnautica/docs/setup_en.md index 83f4186bdfaf..7fc637df2639 100644 --- a/worlds/subnautica/docs/setup_en.md +++ b/worlds/subnautica/docs/setup_en.md @@ -19,7 +19,7 @@ Use the connect form in Subnautica's main menu to enter your connection information to connect to an Archipelago multiworld. Connection information consists of: - Host: the full url that you're trying to connect to, such as `archipelago.gg:38281`. - - PlayerName: your name in the multiworld. Can also be called "slot name" and is the name you entered when creating your settings. + - PlayerName: your name in the multiworld. Can also be called "slot name" and is the name you entered when creating your options. - Password: optional password, leave blank if no password was set. After the connection is made, start a new game. You should start to see Archipelago chat messages to appear, such as a message announcing that you joined the multiworld. diff --git a/worlds/terraria/docs/en_Terraria.md b/worlds/terraria/docs/en_Terraria.md index b0a8529ba76b..26428f75789d 100644 --- a/worlds/terraria/docs/en_Terraria.md +++ b/worlds/terraria/docs/en_Terraria.md @@ -1,14 +1,14 @@ # Terraria -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? Boss/event flags are randomized. So, defeating Empress of Light could give you Post-Skeletron, which allows you to enter -the Dungeon, for example. In your player settings, you may also add item rewards and achievements to the pool. +the Dungeon, for example. In your player options, you may also add item rewards and achievements to the pool. ## What Terraria items can appear in other players' worlds? diff --git a/worlds/terraria/docs/setup_en.md b/worlds/terraria/docs/setup_en.md index b69af591fa5c..55a4df1df30d 100644 --- a/worlds/terraria/docs/setup_en.md +++ b/worlds/terraria/docs/setup_en.md @@ -43,7 +43,7 @@ files are, and how they are used. ### Where do I get a YAML? -You can use the [game settings page for Terraria](/games/Terraria/player-settings) here +You can use the [game options page for Terraria](/games/Terraria/player-options) here on the Archipelago website to generate a YAML using a graphical interface. ## Joining an Archipelago Game in Terraria diff --git a/worlds/timespinner/docs/en_Timespinner.md b/worlds/timespinner/docs/en_Timespinner.md index 6a9e7fa4c039..a5b1419b94a7 100644 --- a/worlds/timespinner/docs/en_Timespinner.md +++ b/worlds/timespinner/docs/en_Timespinner.md @@ -1,8 +1,8 @@ # Timespinner -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/timespinner/docs/setup_en.md b/worlds/timespinner/docs/setup_en.md index c47c639cd20d..7ee51f91323a 100644 --- a/worlds/timespinner/docs/setup_en.md +++ b/worlds/timespinner/docs/setup_en.md @@ -33,8 +33,8 @@ randomized mode. For more info see the [ReadMe](https://github.com/Jarno458/TsRa ## Where do I get a config file? -The [Player Settings](/games/Timespinner/player-settings) page on the website allows you to -configure your personal settings and export them into a config file +The [Player Options](/games/Timespinner/player-options) page on the website allows you to +configure your personal options and export them into a config file * The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds * The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md index 7c2e6deda5bd..938496a161ae 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -1,8 +1,8 @@ # The Legend of Zelda (NES) -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index df857f16df5b..366531e2e43a 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -39,8 +39,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The Player Settings page on the website allows you to configure your personal settings and export a config file from -them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) +The Player Options page on the website allows you to configure your personal options and export a config file from +them. Player options page: [The Legend of Zelda Player Sptions Page](/games/The%20Legend%20of%20Zelda/player-options) ### Verifying your config file @@ -49,8 +49,8 @@ validator page: [YAML Validation page](/check) ## Generating a Single-Player Game -1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. - - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legend%20of%20Zelda/player-settings) +1. Navigate to the Player Options page, configure your options, and click the "Generate Game" button. + - Player Options page: [The Legend of Zelda Player Options Page](/games/The%20Legend%20of%20Zelda/player-options) 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. 4. You will be presented with a server page, from which you can download your patch file. diff --git a/worlds/undertale/docs/en_Undertale.md b/worlds/undertale/docs/en_Undertale.md index 7ff5d55edad9..02fc32f0abc6 100644 --- a/worlds/undertale/docs/en_Undertale.md +++ b/worlds/undertale/docs/en_Undertale.md @@ -1,8 +1,8 @@ # Undertale -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What is considered a location check in Undertale? diff --git a/worlds/undertale/docs/setup_en.md b/worlds/undertale/docs/setup_en.md index 3c20b614d3fa..f1f740959127 100644 --- a/worlds/undertale/docs/setup_en.md +++ b/worlds/undertale/docs/setup_en.md @@ -61,4 +61,4 @@ gameplay differences at the bottom. ### Where do I get a YAML file? -You can customize your settings by visiting the [Undertale Player Settings Page](/games/Undertale/player-settings) +You can customize your options by visiting the [Undertale Player Options Page](/games/Undertale/player-options) diff --git a/worlds/v6/docs/en_VVVVVV.md b/worlds/v6/docs/en_VVVVVV.md index 5c2aa8fec957..c5790e01c5dd 100644 --- a/worlds/v6/docs/en_VVVVVV.md +++ b/worlds/v6/docs/en_VVVVVV.md @@ -1,9 +1,9 @@ # VVVVVV -## Where is the settings page? +## Where is the options page? -The player settings page for this game contains all the options you need to configure and export a config file. Player -settings page link: [VVVVVV Player Settings Page](../player-settings). +The player options page for this game contains all the options you need to configure and export a config file. Player +options page link: [VVVVVV Player Options Page](../player-options). ## What does randomization do to this game? All 20 Trinkets are now Location Checks and may not actually contain Trinkets, but Items for different games. diff --git a/worlds/v6/docs/setup_en.md b/worlds/v6/docs/setup_en.md index 7adf5948c7e4..a23b6c5b252a 100644 --- a/worlds/v6/docs/setup_en.md +++ b/worlds/v6/docs/setup_en.md @@ -30,7 +30,7 @@ If everything worked out, you will see a textbox informing you the connection ha # Playing offline -To play offline, first generate a seed on the game's settings page. +To play offline, first generate a seed on the game's options page. Create a room and download the `.apv6` file, include the offline single-player launch option described above. ## Installation Troubleshooting diff --git a/worlds/wargroove/docs/en_Wargroove.md b/worlds/wargroove/docs/en_Wargroove.md index f08902535d4b..31fd8c81301c 100644 --- a/worlds/wargroove/docs/en_Wargroove.md +++ b/worlds/wargroove/docs/en_Wargroove.md @@ -1,8 +1,8 @@ # Wargroove (Steam, Windows) -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? diff --git a/worlds/wargroove/docs/wargroove_en.md b/worlds/wargroove/docs/wargroove_en.md index 1954dc013924..9c2645178aa2 100644 --- a/worlds/wargroove/docs/wargroove_en.md +++ b/worlds/wargroove/docs/wargroove_en.md @@ -38,7 +38,7 @@ This should install the mod and campaign for you. ## Starting a Multiworld game 1. Start the Wargroove Client and connect to the server. Enter your username from your -[settings file.](/games/Wargroove/player-settings) +[options file.](/games/Wargroove/player-options) 2. Start Wargroove and play the Archipelago campaign by going to `Story->Campaign->Custom->Archipelago`. ## Ending a Multiworld game diff --git a/worlds/witness/docs/en_The Witness.md b/worlds/witness/docs/en_The Witness.md index 4d00ecaae451..6882ed3fdedf 100644 --- a/worlds/witness/docs/en_The Witness.md +++ b/worlds/witness/docs/en_The Witness.md @@ -1,8 +1,8 @@ # The Witness -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -16,7 +16,7 @@ Panels with puzzle symbols on them are now locked initially. ## What is a "check" in The Witness? Solving the last panel in a row of panels or an important standalone panel will count as a check, and send out an item. -It is also possible to add Environmental Puzzles into the location pool via the "Shuffle Environmental Puzzles" setting. +It is also possible to add Environmental Puzzles into the location pool via the "Shuffle Environmental Puzzles" option. ## What "items" can you unlock in The Witness? @@ -25,7 +25,7 @@ This includes symbols such as "Dots", "Black/White Squares", "Colored Squares", Alternatively (or additionally), you can play "Door shuffle", where some doors won't open until you receive their "key". -Receiving lasers as items is also a possible setting. +You can also set lasers to be items you can receive. ## What else can I find in the world? diff --git a/worlds/zillion/docs/en_Zillion.md b/worlds/zillion/docs/en_Zillion.md index 06a11b7d7993..697a9b7dadbe 100644 --- a/worlds/zillion/docs/en_Zillion.md +++ b/worlds/zillion/docs/en_Zillion.md @@ -4,9 +4,9 @@ Zillion is a metroidvania-style game released in 1987 for the 8-bit Sega Master It's based on the anime Zillion (赤い光弾ジリオン, Akai Koudan Zillion). -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What changes are made to this game? diff --git a/worlds/zillion/docs/setup_en.md b/worlds/zillion/docs/setup_en.md index 79f7912dd4fd..c8e29fc36cde 100644 --- a/worlds/zillion/docs/setup_en.md +++ b/worlds/zillion/docs/setup_en.md @@ -47,7 +47,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [player settings page](/games/Zillion/player-settings) on the website allows you to configure your personal settings and export a config file from +The [player options page](/games/Zillion/player-options) on the website allows you to configure your personal options and export a config file from them. ### Verifying your config file @@ -56,7 +56,7 @@ If you would like to validate your config file to make sure it works, you may do ## Generating a Single-Player Game -1. Navigate to the [player settings page](/games/Zillion/player-settings), configure your options, and click the "Generate Game" button. +1. Navigate to the [player options page](/games/Zillion/player-options), configure your options, and click the "Generate Game" button. 2. A "Seed Info" page will appear. 3. Click the "Create New Room" link. 4. A server page will appear. Download your patch file from this page. From 44988d430dc7d91eaeac7aad681dc024bc19ccce Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Fri, 22 Mar 2024 15:28:41 -0500 Subject: [PATCH 20/74] Lingo: Add trap weights option (#2837) --- worlds/lingo/__init__.py | 23 ++++++++++++++++++----- worlds/lingo/items.py | 24 +++--------------------- worlds/lingo/options.py | 14 +++++++++++++- worlds/lingo/player_logic.py | 19 +++++++++++++++++-- 4 files changed, 51 insertions(+), 29 deletions(-) diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index c92e53069edc..b749418368d1 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -6,7 +6,7 @@ from BaseClasses import Item, ItemClassification, Tutorial from worlds.AutoWorld import WebWorld, World from .datatypes import Room, RoomEntrance -from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, LingoItem +from .items import ALL_ITEM_TABLE, ITEMS_BY_GROUP, TRAP_ITEMS, LingoItem from .locations import ALL_LOCATION_TABLE, LOCATIONS_BY_GROUP from .options import LingoOptions from .player_logic import LingoPlayerLogic @@ -91,10 +91,23 @@ def create_items(self): pool.append(self.create_item("Puzzle Skip")) if traps: - traps_list = ["Slowness Trap", "Iceland Trap", "Atbash Trap"] - - for i in range(0, traps): - pool.append(self.create_item(traps_list[i % len(traps_list)])) + total_weight = sum(self.options.trap_weights.values()) + + if total_weight == 0: + raise Exception("Sum of trap weights must be at least one.") + + trap_counts = {name: int(weight * traps / total_weight) + for name, weight in self.options.trap_weights.items()} + + trap_difference = traps - sum(trap_counts.values()) + if trap_difference > 0: + allowed_traps = [name for name in TRAP_ITEMS if self.options.trap_weights[name] > 0] + for i in range(0, trap_difference): + trap_counts[allowed_traps[i % len(allowed_traps)]] += 1 + + for name, count in trap_counts.items(): + for i in range(0, count): + pool.append(self.create_item(name)) self.multiworld.itempool += pool diff --git a/worlds/lingo/items.py b/worlds/lingo/items.py index b9c4eb7909f9..7c7928cbab68 100644 --- a/worlds/lingo/items.py +++ b/worlds/lingo/items.py @@ -1,13 +1,9 @@ from typing import Dict, List, NamedTuple, Optional, TYPE_CHECKING from BaseClasses import Item, ItemClassification -from .options import ShuffleDoors from .static_logic import DOORS_BY_ROOM, PROGRESSION_BY_ROOM, PROGRESSIVE_ITEMS, get_door_group_item_id, \ get_door_item_id, get_progressive_item_id, get_special_item_id -if TYPE_CHECKING: - from . import LingoWorld - class ItemData(NamedTuple): """ @@ -19,20 +15,6 @@ class ItemData(NamedTuple): has_doors: bool painting_ids: List[str] - def should_include(self, world: "LingoWorld") -> bool: - if self.mode == "colors": - return world.options.shuffle_colors > 0 - elif self.mode == "doors": - return world.options.shuffle_doors != ShuffleDoors.option_none - elif self.mode == "complex door": - return world.options.shuffle_doors == ShuffleDoors.option_complex - elif self.mode == "door group": - return world.options.shuffle_doors == ShuffleDoors.option_simple - elif self.mode == "special": - return False - else: - return True - class LingoItem(Item): """ @@ -44,6 +26,8 @@ class LingoItem(Item): ALL_ITEM_TABLE: Dict[str, ItemData] = {} ITEMS_BY_GROUP: Dict[str, List[str]] = {} +TRAP_ITEMS: List[str] = ["Slowness Trap", "Iceland Trap", "Atbash Trap"] + def load_item_data(): global ALL_ITEM_TABLE, ITEMS_BY_GROUP @@ -87,9 +71,7 @@ def load_item_data(): "The Feeling of Being Lost": ItemClassification.filler, "Wanderlust": ItemClassification.filler, "Empty White Hallways": ItemClassification.filler, - "Slowness Trap": ItemClassification.trap, - "Iceland Trap": ItemClassification.trap, - "Atbash Trap": ItemClassification.trap, + **{trap_name: ItemClassification.trap for trap_name in TRAP_ITEMS}, "Puzzle Skip": ItemClassification.useful, } diff --git a/worlds/lingo/options.py b/worlds/lingo/options.py index ed1426450eb7..293992ab91d6 100644 --- a/worlds/lingo/options.py +++ b/worlds/lingo/options.py @@ -1,6 +1,9 @@ from dataclasses import dataclass -from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool +from schema import And, Schema + +from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict +from worlds.lingo.items import TRAP_ITEMS class ShuffleDoors(Choice): @@ -107,6 +110,14 @@ class TrapPercentage(Range): default = 20 +class TrapWeights(OptionDict): + """Specify the distribution of traps that should be placed into the pool. + If you don't want a specific type of trap, set the weight to zero.""" + display_name = "Trap Weights" + schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS}) + default = {trap_name: 1 for trap_name in TRAP_ITEMS} + + class PuzzleSkipPercentage(Range): """Replaces junk items with puzzle skips, at the specified rate.""" display_name = "Puzzle Skip Percentage" @@ -134,6 +145,7 @@ class LingoOptions(PerGameCommonOptions): level_2_requirement: Level2Requirement early_color_hallways: EarlyColorHallways trap_percentage: TrapPercentage + trap_weights: TrapWeights puzzle_skip_percentage: PuzzleSkipPercentage death_link: DeathLink start_inventory_from_pool: StartInventoryPool diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b3cefa539534..966f5a163762 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -2,7 +2,7 @@ from typing import Dict, List, NamedTuple, Optional, Set, Tuple, TYPE_CHECKING from .datatypes import Door, RoomAndDoor, RoomAndPanel -from .items import ALL_ITEM_TABLE +from .items import ALL_ITEM_TABLE, ItemData from .locations import ALL_LOCATION_TABLE, LocationClassification from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, PAINTINGS, PAINTING_ENTRANCES, PAINTING_EXITS, \ @@ -58,6 +58,21 @@ def should_split_progression(progression_name: str, world: "LingoWorld") -> Prog return ProgressiveItemBehavior.PROGRESSIVE +def should_include_item(item: ItemData, world: "LingoWorld") -> bool: + if item.mode == "colors": + return world.options.shuffle_colors > 0 + elif item.mode == "doors": + return world.options.shuffle_doors != ShuffleDoors.option_none + elif item.mode == "complex door": + return world.options.shuffle_doors == ShuffleDoors.option_complex + elif item.mode == "door group": + return world.options.shuffle_doors == ShuffleDoors.option_simple + elif item.mode == "special": + return False + else: + return True + + class LingoPlayerLogic: """ Defines logic after a player's options have been applied @@ -212,7 +227,7 @@ def __init__(self, world: "LingoWorld"): # Instantiate all real items. for name, item in ALL_ITEM_TABLE.items(): - if item.should_include(world): + if should_include_item(item, world): self.real_items.append(name) # Calculate the requirements for the fake pilgrimage. From ca549df20a0a07c30ee2e1bbc2498492b919604d Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:29:24 -0500 Subject: [PATCH 21/74] CommonClient: fix hint tab overlapping (#2957) Co-authored-by: Remy Jette --- kvui.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kvui.py b/kvui.py index bf1f0541a358..fba32049295a 100644 --- a/kvui.py +++ b/kvui.py @@ -705,6 +705,12 @@ def refresh_hints(self, hints): def hint_sorter(element: dict) -> str: return "" + def fix_heights(self): + """Workaround fix for divergent texture and layout heights""" + for element in self.children[0].children: + max_height = max(child.texture_size[1] for child in element.children) + element.height = max_height + class E(ExceptionHandler): logger = logging.getLogger("Client") From 96d93c1ae313bb031e983c0d40d8be199b302df1 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:30:23 -0500 Subject: [PATCH 22/74] A Short Hike: Add option to customize filler coin count (#3004) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/shorthike/Options.py | 16 ++++++++++++++++ worlds/shorthike/__init__.py | 5 +---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/worlds/shorthike/Options.py b/worlds/shorthike/Options.py index 2f378f18ff6e..1ac0ff52f974 100644 --- a/worlds/shorthike/Options.py +++ b/worlds/shorthike/Options.py @@ -61,6 +61,21 @@ class CostMultiplier(Range): range_end = 200 default = 100 +class FillerCoinAmount(Choice): + """The number of coins that will be in each filler coin item.""" + display_name = "Coins per Filler Item" + option_7_coins = 0 + option_13_coins = 1 + option_15_coins = 2 + option_18_coins = 3 + option_21_coins = 4 + option_25_coins = 5 + option_27_coins = 6 + option_32_coins = 7 + option_33_coins = 8 + option_50_coins = 9 + default = 1 + @dataclass class ShortHikeOptions(PerGameCommonOptions): start_inventory_from_pool: StartInventoryPool @@ -71,3 +86,4 @@ class ShortHikeOptions(PerGameCommonOptions): buckets: Buckets golden_feather_progression: GoldenFeatherProgression cost_multiplier: CostMultiplier + filler_coin_amount: FillerCoinAmount diff --git a/worlds/shorthike/__init__.py b/worlds/shorthike/__init__.py index 8a4ef932336d..3e0430f024ca 100644 --- a/worlds/shorthike/__init__.py +++ b/worlds/shorthike/__init__.py @@ -41,11 +41,8 @@ class ShortHikeWorld(World): required_client_version = (0, 4, 4) - def __init__(self, multiworld, player): - super(ShortHikeWorld, self).__init__(multiworld, player) - def get_filler_item_name(self) -> str: - return "13 Coins" + return self.options.filler_coin_amount.current_option_name def create_item(self, name: str) -> "ShortHikeItem": item_id: int = self.item_name_to_id[name] From aaa3472d5d8d8a7a710bd38386d9eb34046a5578 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:30:51 +0100 Subject: [PATCH 23/74] The Witness: Fix seed bleed issue (#3008) --- worlds/witness/player_logic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 099a3a64e611..6bc263b9cc68 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -652,8 +652,8 @@ def __init__(self, world: "WitnessWorld", disabled_locations: Set[str], start_in elif self.DIFFICULTY == "none": self.REFERENCE_LOGIC = StaticWitnessLogic.vanilla - self.CONNECTIONS_BY_REGION_NAME = copy.copy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) - self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.copy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) + self.CONNECTIONS_BY_REGION_NAME = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_CONNECTIONS_BY_REGION_NAME) + self.DEPENDENT_REQUIREMENTS_BY_HEX = copy.deepcopy(self.REFERENCE_LOGIC.STATIC_DEPENDENT_REQUIREMENTS_BY_HEX) self.REQUIREMENTS_BY_HEX = dict() # Determining which panels need to be events is a difficult process. From 355223b8f0af1ee729ffa8b53eb717aa5bf283a4 Mon Sep 17 00:00:00 2001 From: PinkSwitch <52474902+PinkSwitch@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:35:00 -0500 Subject: [PATCH 24/74] Yoshi's Island: Implement New Game (#2141) Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- README.md | 1 + docs/CODEOWNERS | 3 + inno_setup.iss | 5 + worlds/yoshisisland/Client.py | 144 ++ worlds/yoshisisland/Items.py | 122 ++ worlds/yoshisisland/Locations.py | 355 +++++ worlds/yoshisisland/Options.py | 296 ++++ worlds/yoshisisland/Regions.py | 248 ++++ worlds/yoshisisland/Rom.py | 1230 +++++++++++++++++ worlds/yoshisisland/Rules.py | 612 ++++++++ worlds/yoshisisland/__init__.py | 388 ++++++ worlds/yoshisisland/docs/en_Yoshi's Island.md | 71 + worlds/yoshisisland/docs/setup_en.md | 123 ++ worlds/yoshisisland/level_logic.py | 482 +++++++ worlds/yoshisisland/setup_bosses.py | 19 + worlds/yoshisisland/setup_game.py | 460 ++++++ 16 files changed, 4559 insertions(+) create mode 100644 worlds/yoshisisland/Client.py create mode 100644 worlds/yoshisisland/Items.py create mode 100644 worlds/yoshisisland/Locations.py create mode 100644 worlds/yoshisisland/Options.py create mode 100644 worlds/yoshisisland/Regions.py create mode 100644 worlds/yoshisisland/Rom.py create mode 100644 worlds/yoshisisland/Rules.py create mode 100644 worlds/yoshisisland/__init__.py create mode 100644 worlds/yoshisisland/docs/en_Yoshi's Island.md create mode 100644 worlds/yoshisisland/docs/setup_en.md create mode 100644 worlds/yoshisisland/level_logic.py create mode 100644 worlds/yoshisisland/setup_bosses.py create mode 100644 worlds/yoshisisland/setup_game.py diff --git a/README.md b/README.md index 18b1651bb039..905c731b643a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Currently, the following games are supported: * Zork Grand Inquisitor * Castlevania 64 * A Short Hike +* Yoshi's Island For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index a67d5883007e..dc814aee2fa2 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -191,6 +191,9 @@ # The Witness /worlds/witness/ @NewSoupVi @blastron +# Yoshi's Island +/worlds/yoshisisland/ @PinkSwitch + # Zillion /worlds/zillion/ @beauxq diff --git a/inno_setup.iss b/inno_setup.iss index 9f4c9d1678ef..05bb27beca15 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -189,6 +189,11 @@ Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Arc Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: ".apyi"; ValueData: "{#MyAppName}yipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}yipatch"; ValueData: "Archipelago Yoshi's Island Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}yipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; +Root: HKCR; Subkey: "{#MyAppName}yipatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; + Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py new file mode 100644 index 000000000000..c512a8316ab5 --- /dev/null +++ b/worlds/yoshisisland/Client.py @@ -0,0 +1,144 @@ +import logging +import struct +import typing +import time +from struct import pack + +from NetUtils import ClientStatus, color +from worlds.AutoSNIClient import SNIClient + +if typing.TYPE_CHECKING: + from SNIClient import SNIContext + +snes_logger = logging.getLogger("SNES") + +ROM_START = 0x000000 +WRAM_START = 0xF50000 +WRAM_SIZE = 0x20000 +SRAM_START = 0xE00000 + +YOSHISISLAND_ROMHASH_START = 0x007FC0 +ROMHASH_SIZE = 0x15 + +ITEMQUEUE_HIGH = WRAM_START + 0x1465 +ITEM_RECEIVED = WRAM_START + 0x1467 +DEATH_RECEIVED = WRAM_START + 0x7E23B0 +GAME_MODE = WRAM_START + 0x0118 +YOSHI_STATE = SRAM_START + 0x00AC +DEATHLINK_ADDR = ROM_START + 0x06FC8C +DEATHMUSIC_FLAG = WRAM_START + 0x004F +DEATHFLAG = WRAM_START + 0x00DB +DEATHLINKRECV = WRAM_START + 0x00E0 +GOALFLAG = WRAM_START + 0x14B6 + +VALID_GAME_STATES = [0x0F, 0x10, 0x2C] + + +class YoshisIslandSNIClient(SNIClient): + game = "Yoshi's Island" + + async def deathlink_kill_player(self, ctx: "SNIContext") -> None: + from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read + game_state = await snes_read(ctx, GAME_MODE, 0x1) + if game_state[0] != 0x0F: + return + + yoshi_state = await snes_read(ctx, YOSHI_STATE, 0x1) + if yoshi_state[0] != 0x00: + return + + snes_buffered_write(ctx, WRAM_START + 0x026A, bytes([0x01])) + snes_buffered_write(ctx, WRAM_START + 0x00E0, bytes([0x01])) + await snes_flush_writes(ctx) + ctx.death_state = DeathState.dead + ctx.last_death_link = time.time() + + async def validate_rom(self, ctx: "SNIContext") -> bool: + from SNIClient import snes_read + + rom_name = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE) + if rom_name is None or rom_name[:7] != b"YOSHIAP": + return False + + ctx.game = self.game + ctx.items_handling = 0b111 # remote items + ctx.rom = rom_name + + death_link = await snes_read(ctx, DEATHLINK_ADDR, 1) + if death_link: + await ctx.update_death_link(bool(death_link[0] & 0b1)) + return True + + async def game_watcher(self, ctx: "SNIContext") -> None: + from SNIClient import snes_buffered_write, snes_flush_writes, snes_read + + game_mode = await snes_read(ctx, GAME_MODE, 0x1) + item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1) + game_music = await snes_read(ctx, DEATHMUSIC_FLAG, 0x1) + goal_flag = await snes_read(ctx, GOALFLAG, 0x1) + + if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time(): + death_flag = await snes_read(ctx, DEATHFLAG, 0x1) + deathlink_death = await snes_read(ctx, DEATHLINKRECV, 0x1) + currently_dead = (game_music[0] == 0x07 or game_mode[0] == 0x12 or + (death_flag[0] == 0x00 and game_mode[0] == 0x11)) and deathlink_death[0] == 0x00 + await ctx.handle_deathlink_state(currently_dead) + + if game_mode is None: + return + elif goal_flag[0] != 0x00: + await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}]) + ctx.finished_game = True + elif game_mode[0] not in VALID_GAME_STATES: + return + elif item_received[0] > 0x00: + return + + from .Rom import item_values + rom = await snes_read(ctx, YOSHISISLAND_ROMHASH_START, ROMHASH_SIZE) + if rom != ctx.rom: + ctx.rom = None + return + + new_checks = [] + from .Rom import location_table + + location_ram_data = await snes_read(ctx, WRAM_START + 0x1440, 0x80) + for loc_id, loc_data in location_table.items(): + if loc_id not in ctx.locations_checked: + data = location_ram_data[loc_data[0] - 0x1440] + masked_data = data & (1 << loc_data[1]) + bit_set = masked_data != 0 + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if bit_set != invert_bit: + new_checks.append(loc_id) + + for new_check_id in new_checks: + ctx.locations_checked.add(new_check_id) + location = ctx.location_names[new_check_id] + total_locations = len(ctx.missing_locations) + len(ctx.checked_locations) + snes_logger.info(f"New Check: {location} ({len(ctx.locations_checked)}/{total_locations})") + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [new_check_id]}]) + + recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2) + recv_index = struct.unpack("H", recv_count)[0] + if recv_index < len(ctx.items_received): + item = ctx.items_received[recv_index] + recv_index += 1 + logging.info("Received %s from %s (%s) (%d/%d in list)" % ( + color(ctx.item_names[item.item], "red", "bold"), + color(ctx.player_names[item.player], "yellow"), + ctx.location_names[item.location], recv_index, len(ctx.items_received))) + + snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index)) + if item.item in item_values: + item_count = await snes_read(ctx, WRAM_START + item_values[item.item][0], 0x1) + increment = item_values[item.item][1] + new_item_count = item_count[0] + if increment > 1: + new_item_count = increment + else: + new_item_count += increment + + snes_buffered_write(ctx, WRAM_START + item_values[item.item][0], bytes([new_item_count])) + await snes_flush_writes(ctx) diff --git a/worlds/yoshisisland/Items.py b/worlds/yoshisisland/Items.py new file mode 100644 index 000000000000..c97678ed4ed4 --- /dev/null +++ b/worlds/yoshisisland/Items.py @@ -0,0 +1,122 @@ +from typing import Dict, Set, Tuple, NamedTuple, Optional +from BaseClasses import ItemClassification + +class ItemData(NamedTuple): + category: str + code: Optional[int] + classification: ItemClassification + amount: Optional[int] = 1 + +item_table: Dict[str, ItemData] = { + "! Switch": ItemData("Items", 0x302050, ItemClassification.progression), + "Dashed Platform": ItemData("Items", 0x302051, ItemClassification.progression), + "Dashed Stairs": ItemData("Items", 0x302052, ItemClassification.progression), + "Beanstalk": ItemData("Items", 0x302053, ItemClassification.progression), + "Helicopter Morph": ItemData("Morphs", 0x302054, ItemClassification.progression), + "Spring Ball": ItemData("Items", 0x302055, ItemClassification.progression), + "Large Spring Ball": ItemData("Items", 0x302056, ItemClassification.progression), + "Arrow Wheel": ItemData("Items", 0x302057, ItemClassification.progression), + "Vanishing Arrow Wheel": ItemData("Items", 0x302058, ItemClassification.progression), + "Mole Tank Morph": ItemData("Morphs", 0x302059, ItemClassification.progression), + "Watermelon": ItemData("Items", 0x30205A, ItemClassification.progression), + "Ice Melon": ItemData("Items", 0x30205B, ItemClassification.progression), + "Fire Melon": ItemData("Items", 0x30205C, ItemClassification.progression), + "Super Star": ItemData("Items", 0x30205D, ItemClassification.progression), + "Car Morph": ItemData("Morphs", 0x30205E, ItemClassification.progression), + "Flashing Eggs": ItemData("Items", 0x30205F, ItemClassification.progression), + "Giant Eggs": ItemData("Items", 0x302060, ItemClassification.progression), + "Egg Launcher": ItemData("Items", 0x302061, ItemClassification.progression), + "Egg Plant": ItemData("Items", 0x302062, ItemClassification.progression), + "Submarine Morph": ItemData("Morphs", 0x302063, ItemClassification.progression), + "Chomp Rock": ItemData("Items", 0x302064, ItemClassification.progression), + "Poochy": ItemData("Items", 0x302065, ItemClassification.progression), + "Platform Ghost": ItemData("Items", 0x302066, ItemClassification.progression), + "Skis": ItemData("Items", 0x302067, ItemClassification.progression), + "Train Morph": ItemData("Morphs", 0x302068, ItemClassification.progression), + "Key": ItemData("Items", 0x302069, ItemClassification.progression), + "Middle Ring": ItemData("Items", 0x30206A, ItemClassification.progression), + "Bucket": ItemData("Items", 0x30206B, ItemClassification.progression), + "Tulip": ItemData("Items", 0x30206C, ItemClassification.progression), + "Egg Capacity Upgrade": ItemData("Items", 0x30206D, ItemClassification.progression, 5), + "Secret Lens": ItemData("Items", 0x302081, ItemClassification.progression), + + "World 1 Gate": ItemData("Gates", 0x30206E, ItemClassification.progression), + "World 2 Gate": ItemData("Gates", 0x30206F, ItemClassification.progression), + "World 3 Gate": ItemData("Gates", 0x302070, ItemClassification.progression), + "World 4 Gate": ItemData("Gates", 0x302071, ItemClassification.progression), + "World 5 Gate": ItemData("Gates", 0x302072, ItemClassification.progression), + "World 6 Gate": ItemData("Gates", 0x302073, ItemClassification.progression), + + "Extra 1": ItemData("Panels", 0x302074, ItemClassification.progression), + "Extra 2": ItemData("Panels", 0x302075, ItemClassification.progression), + "Extra 3": ItemData("Panels", 0x302076, ItemClassification.progression), + "Extra 4": ItemData("Panels", 0x302077, ItemClassification.progression), + "Extra 5": ItemData("Panels", 0x302078, ItemClassification.progression), + "Extra 6": ItemData("Panels", 0x302079, ItemClassification.progression), + "Extra Panels": ItemData("Panels", 0x30207A, ItemClassification.progression), + + "Bonus 1": ItemData("Panels", 0x30207B, ItemClassification.progression), + "Bonus 2": ItemData("Panels", 0x30207C, ItemClassification.progression), + "Bonus 3": ItemData("Panels", 0x30207D, ItemClassification.progression), + "Bonus 4": ItemData("Panels", 0x30207E, ItemClassification.progression), + "Bonus 5": ItemData("Panels", 0x30207F, ItemClassification.progression), + "Bonus 6": ItemData("Panels", 0x302080, ItemClassification.progression), + "Bonus Panels": ItemData("Panels", 0x302082, ItemClassification.progression), + + "Anytime Egg": ItemData("Consumable", 0x302083, ItemClassification.useful, 0), + "Anywhere Pow": ItemData("Consumable", 0x302084, ItemClassification.filler, 0), + "Winged Cloud Maker": ItemData("Consumable", 0x302085, ItemClassification.filler, 0), + "Pocket Melon": ItemData("Consumable", 0x302086, ItemClassification.filler, 0), + "Pocket Fire Melon": ItemData("Consumable", 0x302087, ItemClassification.filler, 0), + "Pocket Ice Melon": ItemData("Consumable", 0x302088, ItemClassification.filler, 0), + "Magnifying Glass": ItemData("Consumable", 0x302089, ItemClassification.filler, 0), + "+10 Stars": ItemData("Consumable", 0x30208A, ItemClassification.useful, 0), + "+20 Stars": ItemData("Consumable", 0x30208B, ItemClassification.useful, 0), + "1-Up": ItemData("Lives", 0x30208C, ItemClassification.filler, 0), + "2-Up": ItemData("Lives", 0x30208D, ItemClassification.filler, 0), + "3-Up": ItemData("Lives", 0x30208E, ItemClassification.filler, 0), + "10-Up": ItemData("Lives", 0x30208F, ItemClassification.filler, 5), + "Bonus Consumables": ItemData("Events", None, ItemClassification.progression, 0), + "Bandit Consumables": ItemData("Events", None, ItemClassification.progression, 0), + "Bandit Watermelons": ItemData("Events", None, ItemClassification.progression, 0), + + "Fuzzy Trap": ItemData("Traps", 0x302090, ItemClassification.trap, 0), + "Reversal Trap": ItemData("Traps", 0x302091, ItemClassification.trap, 0), + "Darkness Trap": ItemData("Traps", 0x302092, ItemClassification.trap, 0), + "Freeze Trap": ItemData("Traps", 0x302093, ItemClassification.trap, 0), + + "Boss Clear": ItemData("Events", None, ItemClassification.progression, 0), + "Piece of Luigi": ItemData("Items", 0x302095, ItemClassification.progression, 0), + "Saved Baby Luigi": ItemData("Events", None, ItemClassification.progression, 0) +} + +filler_items: Tuple[str, ...] = ( + "Anytime Egg", + "Anywhere Pow", + "Winged Cloud Maker", + "Pocket Melon", + "Pocket Fire Melon", + "Pocket Ice Melon", + "Magnifying Glass", + "+10 Stars", + "+20 Stars", + "1-Up", + "2-Up", + "3-Up" +) + +trap_items: Tuple[str, ...] = ( + "Fuzzy Trap", + "Reversal Trap", + "Darkness Trap", + "Freeze Trap" +) + +def get_item_names_per_category() -> Dict[str, Set[str]]: + categories: Dict[str, Set[str]] = {} + + for name, data in item_table.items(): + if data.category != "Events": + categories.setdefault(data.category, set()).add(name) + + return categories diff --git a/worlds/yoshisisland/Locations.py b/worlds/yoshisisland/Locations.py new file mode 100644 index 000000000000..bc0855260eb4 --- /dev/null +++ b/worlds/yoshisisland/Locations.py @@ -0,0 +1,355 @@ +from typing import List, Optional, NamedTuple, TYPE_CHECKING + +from .Options import PlayerGoal, MinigameChecks +from worlds.generic.Rules import CollectionRule + +if TYPE_CHECKING: + from . import YoshisIslandWorld +from .level_logic import YoshiLogic + + +class LocationData(NamedTuple): + region: str + name: str + code: Optional[int] + LevelID: int + rule: CollectionRule = lambda state: True + + +def get_locations(world: Optional["YoshisIslandWorld"]) -> List[LocationData]: + if world: + logic = YoshiLogic(world) + + location_table: List[LocationData] = [ + LocationData("1-1", "Make Eggs, Throw Eggs: Red Coins", 0x305020, 0x00), + LocationData("1-1", "Make Eggs, Throw Eggs: Flowers", 0x305021, 0x00), + LocationData("1-1", "Make Eggs, Throw Eggs: Stars", 0x305022, 0x00), + LocationData("1-1", "Make Eggs, Throw Eggs: Level Clear", 0x305023, 0x00), + + LocationData("1-2", "Watch Out Below!: Red Coins", 0x305024, 0x01), + LocationData("1-2", "Watch Out Below!: Flowers", 0x305025, 0x01), + LocationData("1-2", "Watch Out Below!: Stars", 0x305026, 0x01), + LocationData("1-2", "Watch Out Below!: Level Clear", 0x305027, 0x01), + + LocationData("1-3", "The Cave Of Chomp Rock: Red Coins", 0x305028, 0x02), + LocationData("1-3", "The Cave Of Chomp Rock: Flowers", 0x305029, 0x02), + LocationData("1-3", "The Cave Of Chomp Rock: Stars", 0x30502A, 0x02), + LocationData("1-3", "The Cave Of Chomp Rock: Level Clear", 0x30502B, 0x02), + + LocationData("1-4", "Burt The Bashful's Fort: Red Coins", 0x30502C, 0x03), + LocationData("1-4", "Burt The Bashful's Fort: Flowers", 0x30502D, 0x03), + LocationData("1-4", "Burt The Bashful's Fort: Stars", 0x30502E, 0x03), + LocationData("1-4", "Burt The Bashful's Fort: Level Clear", 0x30502F, 0x03, lambda state: logic._14CanFightBoss(state)), + LocationData("Burt The Bashful's Boss Room", "Burt The Bashful's Boss Room", None, 0x03, lambda state: logic._14Boss(state)), + + LocationData("1-5", "Hop! Hop! Donut Lifts: Red Coins", 0x305031, 0x04), + LocationData("1-5", "Hop! Hop! Donut Lifts: Flowers", 0x305032, 0x04), + LocationData("1-5", "Hop! Hop! Donut Lifts: Stars", 0x305033, 0x04), + LocationData("1-5", "Hop! Hop! Donut Lifts: Level Clear", 0x305034, 0x04), + + LocationData("1-6", "Shy-Guys On Stilts: Red Coins", 0x305035, 0x05), + LocationData("1-6", "Shy-Guys On Stilts: Flowers", 0x305036, 0x05), + LocationData("1-6", "Shy-Guys On Stilts: Stars", 0x305037, 0x05), + LocationData("1-6", "Shy-Guys On Stilts: Level Clear", 0x305038, 0x05), + + LocationData("1-7", "Touch Fuzzy Get Dizzy: Red Coins", 0x305039, 0x06), + LocationData("1-7", "Touch Fuzzy Get Dizzy: Flowers", 0x30503A, 0x06), + LocationData("1-7", "Touch Fuzzy Get Dizzy: Stars", 0x30503B, 0x06), + LocationData("1-7", "Touch Fuzzy Get Dizzy: Level Clear", 0x30503C, 0x06), + LocationData("1-7", "Touch Fuzzy Get Dizzy: Gather Coins", None, 0x06, lambda state: logic._17Game(state)), + + LocationData("1-8", "Salvo The Slime's Castle: Red Coins", 0x30503D, 0x07), + LocationData("1-8", "Salvo The Slime's Castle: Flowers", 0x30503E, 0x07), + LocationData("1-8", "Salvo The Slime's Castle: Stars", 0x30503F, 0x07), + LocationData("1-8", "Salvo The Slime's Castle: Level Clear", 0x305040, 0x07, lambda state: logic._18CanFightBoss(state)), + LocationData("Salvo The Slime's Boss Room", "Salvo The Slime's Boss Room", None, 0x07, lambda state: logic._18Boss(state)), + + LocationData("1-Bonus", "Flip Cards", None, 0x09), + ############################################################################################ + LocationData("2-1", "Visit Koopa And Para-Koopa: Red Coins", 0x305041, 0x0C), + LocationData("2-1", "Visit Koopa And Para-Koopa: Flowers", 0x305042, 0x0C), + LocationData("2-1", "Visit Koopa And Para-Koopa: Stars", 0x305043, 0x0C), + LocationData("2-1", "Visit Koopa And Para-Koopa: Level Clear", 0x305044, 0x0C), + + LocationData("2-2", "The Baseball Boys: Red Coins", 0x305045, 0x0D), + LocationData("2-2", "The Baseball Boys: Flowers", 0x305046, 0x0D), + LocationData("2-2", "The Baseball Boys: Stars", 0x305047, 0x0D), + LocationData("2-2", "The Baseball Boys: Level Clear", 0x305048, 0x0D), + + LocationData("2-3", "What's Gusty Taste Like?: Red Coins", 0x305049, 0x0E), + LocationData("2-3", "What's Gusty Taste Like?: Flowers", 0x30504A, 0x0E), + LocationData("2-3", "What's Gusty Taste Like?: Stars", 0x30504B, 0x0E), + LocationData("2-3", "What's Gusty Taste Like?: Level Clear", 0x30504C, 0x0E), + + LocationData("2-4", "Bigger Boo's Fort: Red Coins", 0x30504D, 0x0F), + LocationData("2-4", "Bigger Boo's Fort: Flowers", 0x30504E, 0x0F), + LocationData("2-4", "Bigger Boo's Fort: Stars", 0x30504F, 0x0F), + LocationData("2-4", "Bigger Boo's Fort: Level Clear", 0x305050, 0x0F, lambda state: logic._24CanFightBoss(state)), + LocationData("Bigger Boo's Boss Room", "Bigger Boo's Boss Room", None, 0x0F, lambda state: logic._24Boss(state)), + + LocationData("2-5", "Watch Out For Lakitu: Red Coins", 0x305051, 0x10), + LocationData("2-5", "Watch Out For Lakitu: Flowers", 0x305052, 0x10), + LocationData("2-5", "Watch Out For Lakitu: Stars", 0x305053, 0x10), + LocationData("2-5", "Watch Out For Lakitu: Level Clear", 0x305054, 0x10), + + LocationData("2-6", "The Cave Of The Mystery Maze: Red Coins", 0x305055, 0x11), + LocationData("2-6", "The Cave Of The Mystery Maze: Flowers", 0x305056, 0x11), + LocationData("2-6", "The Cave Of The Mystery Maze: Stars", 0x305057, 0x11), + LocationData("2-6", "The Cave Of The Mystery Maze: Level Clear", 0x305058, 0x11), + LocationData("2-6", "The Cave Of the Mystery Maze: Seed Spitting Contest", None, 0x11, lambda state: logic._26Game(state)), + + LocationData("2-7", "Lakitu's Wall: Red Coins", 0x305059, 0x12), + LocationData("2-7", "Lakitu's Wall: Flowers", 0x30505A, 0x12), + LocationData("2-7", "Lakitu's Wall: Stars", 0x30505B, 0x12), + LocationData("2-7", "Lakitu's Wall: Level Clear", 0x30505C, 0x12), + LocationData("2-7", "Lakitu's Wall: Gather Coins", None, 0x12, lambda state: logic._27Game(state)), + + LocationData("2-8", "The Potted Ghost's Castle: Red Coins", 0x30505D, 0x13), + LocationData("2-8", "The Potted Ghost's Castle: Flowers", 0x30505E, 0x13), + LocationData("2-8", "The Potted Ghost's Castle: Stars", 0x30505F, 0x13), + LocationData("2-8", "The Potted Ghost's Castle: Level Clear", 0x305060, 0x13, lambda state: logic._28CanFightBoss(state)), + LocationData("Roger The Ghost's Boss Room", "Roger The Ghost's Boss Room", None, 0x13, lambda state: logic._28Boss(state)), + ############################################################################################### + LocationData("3-1", "Welcome To Monkey World!: Red Coins", 0x305061, 0x18), + LocationData("3-1", "Welcome To Monkey World!: Flowers", 0x305062, 0x18), + LocationData("3-1", "Welcome To Monkey World!: Stars", 0x305063, 0x18), + LocationData("3-1", "Welcome To Monkey World!: Level Clear", 0x305064, 0x18), + + LocationData("3-2", "Jungle Rhythm...: Red Coins", 0x305065, 0x19), + LocationData("3-2", "Jungle Rhythm...: Flowers", 0x305066, 0x19), + LocationData("3-2", "Jungle Rhythm...: Stars", 0x305067, 0x19), + LocationData("3-2", "Jungle Rhythm...: Level Clear", 0x305068, 0x19), + + LocationData("3-3", "Nep-Enuts' Domain: Red Coins", 0x305069, 0x1A), + LocationData("3-3", "Nep-Enuts' Domain: Flowers", 0x30506A, 0x1A), + LocationData("3-3", "Nep-Enuts' Domain: Stars", 0x30506B, 0x1A), + LocationData("3-3", "Nep-Enuts' Domain: Level Clear", 0x30506C, 0x1A), + + LocationData("3-4", "Prince Froggy's Fort: Red Coins", 0x30506D, 0x1B), + LocationData("3-4", "Prince Froggy's Fort: Flowers", 0x30506E, 0x1B), + LocationData("3-4", "Prince Froggy's Fort: Stars", 0x30506F, 0x1B), + LocationData("3-4", "Prince Froggy's Fort: Level Clear", 0x305070, 0x1B, lambda state: logic._34CanFightBoss(state)), + LocationData("Prince Froggy's Boss Room", "Prince Froggy's Boss Room", None, 0x1B, lambda state: logic._34Boss(state)), + + LocationData("3-5", "Jammin' Through The Trees: Red Coins", 0x305071, 0x1C), + LocationData("3-5", "Jammin' Through The Trees: Flowers", 0x305072, 0x1C), + LocationData("3-5", "Jammin' Through The Trees: Stars", 0x305073, 0x1C), + LocationData("3-5", "Jammin' Through The Trees: Level Clear", 0x305074, 0x1C), + + LocationData("3-6", "The Cave Of Harry Hedgehog: Red Coins", 0x305075, 0x1D), + LocationData("3-6", "The Cave Of Harry Hedgehog: Flowers", 0x305076, 0x1D), + LocationData("3-6", "The Cave Of Harry Hedgehog: Stars", 0x305077, 0x1D), + LocationData("3-6", "The Cave Of Harry Hedgehog: Level Clear", 0x305078, 0x1D), + + LocationData("3-7", "Monkeys' Favorite Lake: Red Coins", 0x305079, 0x1E), + LocationData("3-7", "Monkeys' Favorite Lake: Flowers", 0x30507A, 0x1E), + LocationData("3-7", "Monkeys' Favorite Lake: Stars", 0x30507B, 0x1E), + LocationData("3-7", "Monkeys' Favorite Lake: Level Clear", 0x30507C, 0x1E), + + LocationData("3-8", "Naval Piranha's Castle: Red Coins", 0x30507D, 0x1F), + LocationData("3-8", "Naval Piranha's Castle: Flowers", 0x30507E, 0x1F), + LocationData("3-8", "Naval Piranha's Castle: Stars", 0x30507F, 0x1F), + LocationData("3-8", "Naval Piranha's Castle: Level Clear", 0x305080, 0x1F, lambda state: logic._38CanFightBoss(state)), + LocationData("Naval Piranha's Boss Room", "Naval Piranha's Boss Room", None, 0x1F, lambda state: logic._38Boss(state)), + + LocationData("3-Bonus", "Drawing Lots", None, 0x21), + ############################################################################################## + LocationData("4-1", "GO! GO! MARIO!!: Red Coins", 0x305081, 0x24), + LocationData("4-1", "GO! GO! MARIO!!: Flowers", 0x305082, 0x24), + LocationData("4-1", "GO! GO! MARIO!!: Stars", 0x305083, 0x24), + LocationData("4-1", "GO! GO! MARIO!!: Level Clear", 0x305084, 0x24), + + LocationData("4-2", "The Cave Of The Lakitus: Red Coins", 0x305085, 0x25), + LocationData("4-2", "The Cave Of The Lakitus: Flowers", 0x305086, 0x25), + LocationData("4-2", "The Cave Of The Lakitus: Stars", 0x305087, 0x25), + LocationData("4-2", "The Cave Of The Lakitus: Level Clear", 0x305088, 0x25), + + LocationData("4-3", "Don't Look Back!: Red Coins", 0x305089, 0x26), + LocationData("4-3", "Don't Look Back!: Flowers", 0x30508A, 0x26), + LocationData("4-3", "Don't Look Back!: Stars", 0x30508B, 0x26), + LocationData("4-3", "Don't Look Back!: Level Clear", 0x30508C, 0x26), + + LocationData("4-4", "Marching Milde's Fort: Red Coins", 0x30508D, 0x27), + LocationData("4-4", "Marching Milde's Fort: Flowers", 0x30508E, 0x27), + LocationData("4-4", "Marching Milde's Fort: Stars", 0x30508F, 0x27), + LocationData("4-4", "Marching Milde's Fort: Level Clear", 0x305090, 0x27, lambda state: logic._44CanFightBoss(state)), + LocationData("Marching Milde's Boss Room", "Marching Milde's Boss Room", None, 0x27, lambda state: logic._44Boss(state)), + + LocationData("4-5", "Chomp Rock Zone: Red Coins", 0x305091, 0x28), + LocationData("4-5", "Chomp Rock Zone: Flowers", 0x305092, 0x28), + LocationData("4-5", "Chomp Rock Zone: Stars", 0x305093, 0x28), + LocationData("4-5", "Chomp Rock Zone: Level Clear", 0x305094, 0x28), + + LocationData("4-6", "Lake Shore Paradise: Red Coins", 0x305095, 0x29), + LocationData("4-6", "Lake Shore Paradise: Flowers", 0x305096, 0x29), + LocationData("4-6", "Lake Shore Paradise: Stars", 0x305097, 0x29), + LocationData("4-6", "Lake Shore Paradise: Level Clear", 0x305098, 0x29), + + LocationData("4-7", "Ride Like The Wind: Red Coins", 0x305099, 0x2A), + LocationData("4-7", "Ride Like The Wind: Flowers", 0x30509A, 0x2A), + LocationData("4-7", "Ride Like The Wind: Stars", 0x30509B, 0x2A), + LocationData("4-7", "Ride Like The Wind: Level Clear", 0x30509C, 0x2A), + LocationData("4-7", "Ride Like The Wind: Gather Coins", None, 0x2A, lambda state: logic._47Game(state)), + + LocationData("4-8", "Hookbill The Koopa's Castle: Red Coins", 0x30509D, 0x2B), + LocationData("4-8", "Hookbill The Koopa's Castle: Flowers", 0x30509E, 0x2B), + LocationData("4-8", "Hookbill The Koopa's Castle: Stars", 0x30509F, 0x2B), + LocationData("4-8", "Hookbill The Koopa's Castle: Level Clear", 0x3050A0, 0x2B, lambda state: logic._48CanFightBoss(state)), + LocationData("Hookbill The Koopa's Boss Room", "Hookbill The Koopa's Boss Room", None, 0x2B, lambda state: logic._48Boss(state)), + + LocationData("4-Bonus", "Match Cards", None, 0x2D), + ###################################################################################################### + LocationData("5-1", "BLIZZARD!!!: Red Coins", 0x3050A1, 0x30), + LocationData("5-1", "BLIZZARD!!!: Flowers", 0x3050A2, 0x30), + LocationData("5-1", "BLIZZARD!!!: Stars", 0x3050A3, 0x30), + LocationData("5-1", "BLIZZARD!!!: Level Clear", 0x3050A4, 0x30), + + LocationData("5-2", "Ride The Ski Lifts: Red Coins", 0x3050A5, 0x31), + LocationData("5-2", "Ride The Ski Lifts: Flowers", 0x3050A6, 0x31), + LocationData("5-2", "Ride The Ski Lifts: Stars", 0x3050A7, 0x31), + LocationData("5-2", "Ride The Ski Lifts: Level Clear", 0x3050A8, 0x31), + + LocationData("5-3", "Danger - Icy Conditions Ahead: Red Coins", 0x3050A9, 0x32), + LocationData("5-3", "Danger - Icy Conditions Ahead: Flowers", 0x3050AA, 0x32), + LocationData("5-3", "Danger - Icy Conditions Ahead: Stars", 0x3050AB, 0x32), + LocationData("5-3", "Danger - Icy Conditions Ahead: Level Clear", 0x3050AC, 0x32), + + LocationData("5-4", "Sluggy The Unshaven's Fort: Red Coins", 0x3050AD, 0x33), + LocationData("5-4", "Sluggy The Unshaven's Fort: Flowers", 0x3050AE, 0x33), + LocationData("5-4", "Sluggy The Unshaven's Fort: Stars", 0x3050AF, 0x33), + LocationData("5-4", "Sluggy The Unshaven's Fort: Level Clear", 0x3050B0, 0x33, lambda state: logic._54CanFightBoss(state)), + LocationData("Sluggy The Unshaven's Boss Room", "Sluggy The Unshaven's Boss Room", None, 0x33, lambda state: logic._54Boss(state)), + + LocationData("5-5", "Goonie Rides!: Red Coins", 0x3050B1, 0x34), + LocationData("5-5", "Goonie Rides!: Flowers", 0x3050B2, 0x34), + LocationData("5-5", "Goonie Rides!: Stars", 0x3050B3, 0x34), + LocationData("5-5", "Goonie Rides!: Level Clear", 0x3050B4, 0x34), + + LocationData("5-6", "Welcome To Cloud World: Red Coins", 0x3050B5, 0x35), + LocationData("5-6", "Welcome To Cloud World: Flowers", 0x3050B6, 0x35), + LocationData("5-6", "Welcome To Cloud World: Stars", 0x3050B7, 0x35), + LocationData("5-6", "Welcome To Cloud World: Level Clear", 0x3050B8, 0x35), + + LocationData("5-7", "Shifting Platforms Ahead: Red Coins", 0x3050B9, 0x36), + LocationData("5-7", "Shifting Platforms Ahead: Flowers", 0x3050BA, 0x36), + LocationData("5-7", "Shifting Platforms Ahead: Stars", 0x3050BB, 0x36), + LocationData("5-7", "Shifting Platforms Ahead: Level Clear", 0x3050BC, 0x36), + + LocationData("5-8", "Raphael The Raven's Castle: Red Coins", 0x3050BD, 0x37), + LocationData("5-8", "Raphael The Raven's Castle: Flowers", 0x3050BE, 0x37), + LocationData("5-8", "Raphael The Raven's Castle: Stars", 0x3050BF, 0x37), + LocationData("5-8", "Raphael The Raven's Castle: Level Clear", 0x3050C0, 0x37, lambda state: logic._58CanFightBoss(state)), + LocationData("Raphael The Raven's Boss Room", "Raphael The Raven's Boss Room", None, 0x37, lambda state: logic._58Boss(state)), + ###################################################################################################### + + LocationData("6-1", "Scary Skeleton Goonies!: Red Coins", 0x3050C1, 0x3C), + LocationData("6-1", "Scary Skeleton Goonies!: Flowers", 0x3050C2, 0x3C), + LocationData("6-1", "Scary Skeleton Goonies!: Stars", 0x3050C3, 0x3C), + LocationData("6-1", "Scary Skeleton Goonies!: Level Clear", 0x3050C4, 0x3C), + + LocationData("6-2", "The Cave Of The Bandits: Red Coins", 0x3050C5, 0x3D), + LocationData("6-2", "The Cave Of The Bandits: Flowers", 0x3050C6, 0x3D), + LocationData("6-2", "The Cave Of The Bandits: Stars", 0x3050C7, 0x3D), + LocationData("6-2", "The Cave Of The Bandits: Level Clear", 0x3050C8, 0x3D), + + LocationData("6-3", "Beware The Spinning Logs: Red Coins", 0x3050C9, 0x3E), + LocationData("6-3", "Beware The Spinning Logs: Flowers", 0x3050CA, 0x3E), + LocationData("6-3", "Beware The Spinning Logs: Stars", 0x3050CB, 0x3E), + LocationData("6-3", "Beware The Spinning Logs: Level Clear", 0x3050CC, 0x3E), + + LocationData("6-4", "Tap-Tap The Red Nose's Fort: Red Coins", 0x3050CD, 0x3F), + LocationData("6-4", "Tap-Tap The Red Nose's Fort: Flowers", 0x3050CE, 0x3F), + LocationData("6-4", "Tap-Tap The Red Nose's Fort: Stars", 0x3050CF, 0x3F), + LocationData("6-4", "Tap-Tap The Red Nose's Fort: Level Clear", 0x3050D0, 0x3F, lambda state: logic._64CanFightBoss(state)), + LocationData("Tap-Tap The Red Nose's Boss Room", "Tap-Tap The Red Nose's Boss Room", None, 0x3F, lambda state: logic._64Boss(state)), + + LocationData("6-5", "The Very Loooooong Cave: Red Coins", 0x3050D1, 0x40), + LocationData("6-5", "The Very Loooooong Cave: Flowers", 0x3050D2, 0x40), + LocationData("6-5", "The Very Loooooong Cave: Stars", 0x3050D3, 0x40), + LocationData("6-5", "The Very Loooooong Cave: Level Clear", 0x3050D4, 0x40), + + LocationData("6-6", "The Deep, Underground Maze: Red Coins", 0x3050D5, 0x41), + LocationData("6-6", "The Deep, Underground Maze: Flowers", 0x3050D6, 0x41), + LocationData("6-6", "The Deep, Underground Maze: Stars", 0x3050D7, 0x41), + LocationData("6-6", "The Deep, Underground Maze: Level Clear", 0x3050D8, 0x41), + + LocationData("6-7", "KEEP MOVING!!!!: Red Coins", 0x3050D9, 0x42), + LocationData("6-7", "KEEP MOVING!!!!: Flowers", 0x3050DA, 0x42), + LocationData("6-7", "KEEP MOVING!!!!: Stars", 0x3050DB, 0x42), + LocationData("6-7", "KEEP MOVING!!!!: Level Clear", 0x3050DC, 0x42), + + LocationData("6-8", "King Bowser's Castle: Red Coins", 0x3050DD, 0x43), + LocationData("6-8", "King Bowser's Castle: Flowers", 0x3050DE, 0x43), + LocationData("6-8", "King Bowser's Castle: Stars", 0x3050DF, 0x43) + ] + + if not world or world.options.extras_enabled: + location_table += [ + LocationData("1-Extra", "Poochy Ain't Stupid: Red Coins", 0x3050E0, 0x08), + LocationData("1-Extra", "Poochy Ain't Stupid: Flowers", 0x3050E1, 0x08), + LocationData("1-Extra", "Poochy Ain't Stupid: Stars", 0x3050E2, 0x08), + LocationData("1-Extra", "Poochy Ain't Stupid: Level Clear", 0x3050E3, 0x08), + + LocationData("2-Extra", "Hit That Switch!!: Red Coins", 0x3050E4, 0x14), + LocationData("2-Extra", "Hit That Switch!!: Flowers", 0x3050E5, 0x14), + LocationData("2-Extra", "Hit That Switch!!: Stars", 0x3050E6, 0x14), + LocationData("2-Extra", "Hit That Switch!!: Level Clear", 0x3050E7, 0x14), + + LocationData("3-Extra", "More Monkey Madness: Red Coins", 0x3050E8, 0x20), + LocationData("3-Extra", "More Monkey Madness: Flowers", 0x3050E9, 0x20), + LocationData("3-Extra", "More Monkey Madness: Stars", 0x3050EA, 0x20), + LocationData("3-Extra", "More Monkey Madness: Level Clear", 0x3050EB, 0x20), + + LocationData("4-Extra", "The Impossible? Maze: Red Coins", 0x3050EC, 0x2C), + LocationData("4-Extra", "The Impossible? Maze: Flowers", 0x3050ED, 0x2C), + LocationData("4-Extra", "The Impossible? Maze: Stars", 0x3050EE, 0x2C), + LocationData("4-Extra", "The Impossible? Maze: Level Clear", 0x3050EF, 0x2C), + + LocationData("5-Extra", "Kamek's Revenge: Red Coins", 0x3050F0, 0x38), + LocationData("5-Extra", "Kamek's Revenge: Flowers", 0x3050F1, 0x38), + LocationData("5-Extra", "Kamek's Revenge: Stars", 0x3050F2, 0x38), + LocationData("5-Extra", "Kamek's Revenge: Level Clear", 0x3050F3, 0x38), + + LocationData("6-Extra", "Castles - Masterpiece Set: Red Coins", 0x3050F4, 0x44), + LocationData("6-Extra", "Castles - Masterpiece Set: Flowers", 0x3050F5, 0x44), + LocationData("6-Extra", "Castles - Masterpiece Set: Stars", 0x3050F6, 0x44), + LocationData("6-Extra", "Castles - Masterpiece Set: Level Clear", 0x3050F7, 0x44), + ] + + if not world or world.options.minigame_checks in {MinigameChecks.option_bandit_games, MinigameChecks.option_both}: + location_table += [ + LocationData("1-3", "The Cave Of Chomp Rock: Bandit Game", 0x3050F8, 0x02, lambda state: logic._13Game(state)), + LocationData("1-7", "Touch Fuzzy Get Dizzy: Bandit Game", 0x3050F9, 0x06, lambda state: logic._17Game(state)), + LocationData("2-1", "Visit Koopa And Para-Koopa: Bandit Game", 0x3050FA, 0x0C, lambda state: logic._21Game(state)), + LocationData("2-3", "What's Gusty Taste Like?: Bandit Game", 0x3050FB, 0x0E, lambda state: logic._23Game(state)), + LocationData("2-6", "The Cave Of The Mystery Maze: Bandit Game", 0x3050FC, 0x11, lambda state: logic._26Game(state)), + LocationData("2-7", "Lakitu's Wall: Bandit Game", 0x3050FD, 0x12, lambda state: logic._27Game(state)), + LocationData("3-2", "Jungle Rhythm...: Bandit Game", 0x3050FE, 0x19, lambda state: logic._32Game(state)), + LocationData("3-7", "Monkeys' Favorite Lake: Bandit Game", 0x3050FF, 0x1E, lambda state: logic._37Game(state)), + LocationData("4-2", "The Cave Of The Lakitus: Bandit Game", 0x305100, 0x25, lambda state: logic._42Game(state)), + LocationData("4-6", "Lake Shore Paradise: Bandit Game", 0x305101, 0x29, lambda state: logic._46Game(state)), + LocationData("4-7", "Ride Like The Wind: Bandit Game", 0x305102, 0x2A, lambda state: logic._47Game(state)), + LocationData("5-1", "BLIZZARD!!!: Bandit Game", 0x305103, 0x30, lambda state: logic._51Game(state)), + LocationData("6-1", "Scary Skeleton Goonies!: Bandit Game", 0x305104, 0x3C, lambda state: logic._61Game(state)), + LocationData("6-7", "KEEP MOVING!!!!: Bandit Game", 0x305105, 0x42, lambda state: logic._67Game(state)), + ] + + if not world or world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: + location_table += [ + LocationData("1-Bonus", "Flip Cards: Victory", 0x305106, 0x09), + LocationData("2-Bonus", "Scratch And Match: Victory", 0x305107, 0x15), + LocationData("3-Bonus", "Drawing Lots: Victory", 0x305108, 0x21), + LocationData("4-Bonus", "Match Cards: Victory", 0x305109, 0x2D), + LocationData("5-Bonus", "Roulette: Victory", 0x30510A, 0x39), + LocationData("6-Bonus", "Slot Machine: Victory", 0x30510B, 0x45), + ] + if not world or world.options.goal == PlayerGoal.option_luigi_hunt: + location_table += [ + LocationData("Overworld", "Reconstituted Luigi", None, 0x00, lambda state: logic.reconstitute_luigi(state)), + ] + if not world or world.options.goal == PlayerGoal.option_bowser: + location_table += [ + LocationData("Bowser's Room", "King Bowser's Castle: Level Clear", None, 0x43, lambda state: logic._68Clear(state)), + ] + + return location_table diff --git a/worlds/yoshisisland/Options.py b/worlds/yoshisisland/Options.py new file mode 100644 index 000000000000..d02999309f61 --- /dev/null +++ b/worlds/yoshisisland/Options.py @@ -0,0 +1,296 @@ +from dataclasses import dataclass +from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, PerGameCommonOptions + + +class ExtrasEnabled(Toggle): + """If enabled, the more difficult Extra stages will be added into logic. Otherwise, they will be inaccessible.""" + display_name = "Include Extra Stages" + + +class SplitExtras(Toggle): + """If enabled, Extra stages will be unlocked individually. Otherwise, there will be a single 'Extra Panels' item that unlocks all of them.""" + display_name = "Split Extra Stages" + + +class SplitBonus(Toggle): + """If enabled, Bonus Games will be unlocked individually. Otherwise, there will be a single 'Bonus Panels' item that unlocks all of them.""" + display_name = "Split Bonus Games" + + +class ObjectVis(Choice): + """This will determine the default visibility of objects revealed by the Magnifying Glass. + Strict Logic will expect the Secret Lens or a Magnifying Glass to interact with hidden clouds containing stars if they are not set to visible by default.""" + display_name = "Hidden Object Visibility" + option_none = 0 + option_coins_only = 1 + option_clouds_only = 2 + option_full = 3 + default = 1 + + +class SoftlockPrevention(DefaultOnToggle): + """If enabled, hold R + X to warp to the last used Middle Ring, or the start of the level if none have been activated.""" + display_name = "Softlock Prevention Code" + + +class StageLogic(Choice): + """This determines what logic mode the stages will use. + Strict: Best for casual players or those new to playing Yoshi's Island in AP. Level requirements won't expect anything too difficult of the player. + Loose: Recommended for veterans of the original game. Won't expect anything too difficult, but may expect unusual platforming or egg throws. + Expert: Logic may expect advanced knowledge or memorization of level layouts, as well as jumps the player may only have one chance to make without restarting.""" + display_name = "Stage Logic" + option_strict = 0 + option_loose = 1 + option_expert = 2 + # option_glitched = 3 + default = 0 + + +class ShuffleMiddleRings(Toggle): + """If enabled, Middle Rings will be added to the item pool.""" + display_name = "Shuffle Middle Rings" + + +class ShuffleSecretLens(Toggle): + """If enabled, the Secret Lens will be added to the item pool. + The Secret Lens will act as a permanent Magnifying Glass.""" + display_name = "Add Secret Lens" + + +class DisableAutoScrollers(Toggle): + """If enabled, will disable autoscrolling during levels, except during levels which cannot function otherwise.""" + display_name = "Disable Autoscrolling" + + +class ItemLogic(Toggle): + """This will enable logic to expect consumables to be used from the inventory in place of some major items. + Logic will expect you to have access to an Overworld bonus game, or a bandit game to get the necessary items. + Logic will NOT expect grinding end-of-level bonus games, or any inventory consumables received from checks. + Casual logic will only expect consumables from Overworld games; Loose and Expert may expect them from bandit games.""" + display_name = "Consumable Logic" + + +class MinigameChecks(Choice): + """This will set minigame victories to give Archipelago checks. + This will not randomize minigames amongst themselves, and is compatible with item logic. + Bonus games will be expected to be cleared from the Overworld, not the end of levels. + Additionally, 1-Up bonus games will accept any profit as a victory.""" + display_name = "Minigame Reward Checks" + option_none = 0 + option_bandit_games = 1 + option_bonus_games = 2 + option_both = 3 + default = 0 + + +class StartingWorld(Choice): + """This sets which world you start in. Other worlds can be accessed by receiving a Gate respective to that world.""" + display_name = "Starting World" + option_world_1 = 0 + option_world_2 = 1 + option_world_3 = 2 + option_world_4 = 3 + option_world_5 = 4 + option_world_6 = 5 + default = 0 + + +class StartingLives(Range): + """This sets the amount of lives Yoshi will have upon loading the game.""" + display_name = "Starting Life Count" + range_start = 1 + range_end = 999 + default = 3 + + +class PlayerGoal(Choice): + """This sets the goal. Bowser goal requires defeating Bowser at the end of 6-8, while Luigi Hunt requires collecting all required Luigi Pieces.""" + display_name = "Goal" + option_bowser = 0 + option_luigi_hunt = 1 + default = 0 + + +class LuigiPiecesReq(Range): + """This will set how many Luigi Pieces are required to trigger a victory.""" + display_name = "Luigi Pieces Required" + range_start = 1 + range_end = 100 + default = 25 + + +class LuigiPiecesAmt(Range): + """This will set how many Luigi Pieces are in the item pool. + If the number in the pool is lower than the number required, + the amount in the pool will be randomized, with the minimum being the amount required.""" + display_name = "Amount of Luigi Pieces" + range_start = 1 + range_end = 100 + default = 50 + + +class FinalLevelBosses(Range): + """This sets how many bosses need to be defeated to access 6-8. + You can check this in-game by pressing SELECT while in any level.""" + display_name = "Bosses Required for 6-8 Unlock" + range_start = 0 + range_end = 11 + default = 5 + + +class FinalBossBosses(Range): + """This sets how many bosses need to be defeated to access the boss of 6-8. + You can check this in-game by pressing SELECT while in any level.""" + display_name = "Bosses Required for 6-8 Clear" + range_start = 0 + range_end = 11 + default = 0 + + +class BowserDoor(Choice): + """This will set which route you take through 6-8. + Manual: You go through the door that you hit with an egg, as normal. + Doors: Route will be forced to be the door chosen here, regardless of which door you hit. + Gauntlet: You will be forced to go through all 4 routes in order before the final hallway.""" + display_name = "Bowser's Castle Doors" + option_manual = 0 + option_door_1 = 1 + option_door_2 = 2 + option_door_3 = 3 + option_door_4 = 4 + option_gauntlet = 5 + default = 0 + + +class BossShuffle(Toggle): + """This whill shuffle which boss each boss door will lead to. Each boss can only appear once, and Baby Bowser is left alone.""" + display_name = "Boss Shuffle" + + +class LevelShuffle(Choice): + """Disabled: All levels will appear in their normal location. + Bosses Guranteed: All worlds will have a boss on -4 and -8. + Full: Worlds may have more than 2 or no bosses in them. + Regardless of the setting, 6-8 and Extra stages are not shuffled.""" + display_name = "Level Shuffle" + option_disabled = 0 + option_bosses_guranteed = 1 + option_full = 2 + default = 0 + + +class YoshiColors(Choice): + """Sets the Yoshi color for each level. + Normal will use the vanilla colors. + Random order will generate a random order of colors that will be used in each level. The stage 1 color will be used for Extra stages, and 6-8. + Random color will generate a random color for each stage. + Singularity will use a single color defined under 'Singularity Yoshi Color' for use in all stages.""" + display_name = "Yoshi Colors" + option_normal = 0 + option_random_order = 1 + option_random_color = 2 + option_singularity = 3 + default = 0 + + +class SinguColor(Choice): + """Sets which color Yoshi will be if Yoshi Colors is set to singularity.""" + display_name = "Singularity Yoshi Color" + option_green = 0 + option_pink = 1 + option_cyan = 3 + option_yellow = 2 + option_purple = 4 + option_brown = 5 + option_red = 6 + option_blue = 7 + default = 0 + + +class BabySound(Choice): + """Change the sound that Baby Mario makes when not on Yoshi.""" + display_name = "Mario Sound Effect" + option_normal = 0 + option_disabled = 1 + option_random_sound_effect = 2 + default = 0 + + +class TrapsEnabled(Toggle): + """Will place traps into the item pool. + Traps have a variety of negative effects, and will only replace filler items.""" + display_name = "Traps Enabled" + + +class TrapPercent(Range): + """Percentage of the item pool that becomes replaced with traps.""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 10 + +# class EnableScrets(Range): + # """This sets the amount of lives Yoshi will have upon loading the game.""" + # display_name = "Starting Life Count" + # range_start = 1 + # range_end = 255 + # default = 3 + +# class BackgroundColors(Range): + # """This sets the amount of lives Yoshi will have upon loading the game.""" + # display_name = "Starting Life Count" + # range_start = 1 + # range_end = 255 + # default = 3 + +# class Foreground Colors(Range): + # """This sets the amount of lives Yoshi will have upon loading the game.""" + # display_name = "Starting Life Count" + # range_start = 1 + # range_end = 255 + # default = 3 + +# class Music Shuffle(Range): + # """This sets the amount of lives Yoshi will have upon loading the game.""" + # display_name = "Starting Life Count" + # range_start = 1 + # range_end = 255 + # default = 3 + +# class Star Loss Rate(Range): + # """This sets the amount of lives Yoshi will have upon loading the game.""" + # display_name = "Starting Life Count" + # range_start = 1 + # range_end = 255 + # default = 3 + + +@dataclass +class YoshisIslandOptions(PerGameCommonOptions): + starting_world: StartingWorld + starting_lives: StartingLives + goal: PlayerGoal + luigi_pieces_required: LuigiPiecesReq + luigi_pieces_in_pool: LuigiPiecesAmt + extras_enabled: ExtrasEnabled + minigame_checks: MinigameChecks + split_extras: SplitExtras + split_bonus: SplitBonus + hidden_object_visibility: ObjectVis + add_secretlens: ShuffleSecretLens + shuffle_midrings: ShuffleMiddleRings + stage_logic: StageLogic + item_logic: ItemLogic + disable_autoscroll: DisableAutoScrollers + softlock_prevention: SoftlockPrevention + castle_open_condition: FinalLevelBosses + castle_clear_condition: FinalBossBosses + bowser_door_mode: BowserDoor + level_shuffle: LevelShuffle + boss_shuffle: BossShuffle + yoshi_colors: YoshiColors + yoshi_singularity_color: SinguColor + baby_mario_sound: BabySound + traps_enabled: TrapsEnabled + trap_percent: TrapPercent + death_link: DeathLink diff --git a/worlds/yoshisisland/Regions.py b/worlds/yoshisisland/Regions.py new file mode 100644 index 000000000000..59e93cfe7979 --- /dev/null +++ b/worlds/yoshisisland/Regions.py @@ -0,0 +1,248 @@ +from typing import List, Dict, TYPE_CHECKING +from BaseClasses import Region, Location +from .Locations import LocationData +from .Options import MinigameChecks +from .level_logic import YoshiLogic +from .setup_bosses import BossReqs +if TYPE_CHECKING: + from . import YoshisIslandWorld + + +class YoshisIslandLocation(Location): + game: str = "Yoshi's Island" + level_id: int + + def __init__(self, player: int, name: str = " ", address: int = None, parent=None, level_id: int = None): + super().__init__(player, name, address, parent) + self.level_id = level_id + + +def init_areas(world: "YoshisIslandWorld", locations: List[LocationData]) -> None: + multiworld = world.multiworld + player = world.player + logic = YoshiLogic(world) + + locations_per_region = get_locations_per_region(locations) + + regions = [ + create_region(world, player, locations_per_region, "Menu"), + create_region(world, player, locations_per_region, "Overworld"), + create_region(world, player, locations_per_region, "World 1"), + create_region(world, player, locations_per_region, "World 2"), + create_region(world, player, locations_per_region, "World 3"), + create_region(world, player, locations_per_region, "World 4"), + create_region(world, player, locations_per_region, "World 5"), + create_region(world, player, locations_per_region, "World 6"), + + create_region(world, player, locations_per_region, "1-1"), + create_region(world, player, locations_per_region, "1-2"), + create_region(world, player, locations_per_region, "1-3"), + create_region(world, player, locations_per_region, "1-4"), + create_region(world, player, locations_per_region, "Burt The Bashful's Boss Room"), + create_region(world, player, locations_per_region, "1-5"), + create_region(world, player, locations_per_region, "1-6"), + create_region(world, player, locations_per_region, "1-7"), + create_region(world, player, locations_per_region, "1-8"), + create_region(world, player, locations_per_region, "Salvo The Slime's Boss Room"), + + create_region(world, player, locations_per_region, "2-1"), + create_region(world, player, locations_per_region, "2-2"), + create_region(world, player, locations_per_region, "2-3"), + create_region(world, player, locations_per_region, "2-4"), + create_region(world, player, locations_per_region, "Bigger Boo's Boss Room"), + create_region(world, player, locations_per_region, "2-5"), + create_region(world, player, locations_per_region, "2-6"), + create_region(world, player, locations_per_region, "2-7"), + create_region(world, player, locations_per_region, "2-8"), + create_region(world, player, locations_per_region, "Roger The Ghost's Boss Room"), + + create_region(world, player, locations_per_region, "3-1"), + create_region(world, player, locations_per_region, "3-2"), + create_region(world, player, locations_per_region, "3-3"), + create_region(world, player, locations_per_region, "3-4"), + create_region(world, player, locations_per_region, "Prince Froggy's Boss Room"), + create_region(world, player, locations_per_region, "3-5"), + create_region(world, player, locations_per_region, "3-6"), + create_region(world, player, locations_per_region, "3-7"), + create_region(world, player, locations_per_region, "3-8"), + create_region(world, player, locations_per_region, "Naval Piranha's Boss Room"), + + create_region(world, player, locations_per_region, "4-1"), + create_region(world, player, locations_per_region, "4-2"), + create_region(world, player, locations_per_region, "4-3"), + create_region(world, player, locations_per_region, "4-4"), + create_region(world, player, locations_per_region, "Marching Milde's Boss Room"), + create_region(world, player, locations_per_region, "4-5"), + create_region(world, player, locations_per_region, "4-6"), + create_region(world, player, locations_per_region, "4-7"), + create_region(world, player, locations_per_region, "4-8"), + create_region(world, player, locations_per_region, "Hookbill The Koopa's Boss Room"), + + create_region(world, player, locations_per_region, "5-1"), + create_region(world, player, locations_per_region, "5-2"), + create_region(world, player, locations_per_region, "5-3"), + create_region(world, player, locations_per_region, "5-4"), + create_region(world, player, locations_per_region, "Sluggy The Unshaven's Boss Room"), + create_region(world, player, locations_per_region, "5-5"), + create_region(world, player, locations_per_region, "5-6"), + create_region(world, player, locations_per_region, "5-7"), + create_region(world, player, locations_per_region, "5-8"), + create_region(world, player, locations_per_region, "Raphael The Raven's Boss Room"), + + create_region(world, player, locations_per_region, "6-1"), + create_region(world, player, locations_per_region, "6-2"), + create_region(world, player, locations_per_region, "6-3"), + create_region(world, player, locations_per_region, "6-4"), + create_region(world, player, locations_per_region, "Tap-Tap The Red Nose's Boss Room"), + create_region(world, player, locations_per_region, "6-5"), + create_region(world, player, locations_per_region, "6-6"), + create_region(world, player, locations_per_region, "6-7"), + create_region(world, player, locations_per_region, "6-8"), + create_region(world, player, locations_per_region, "Bowser's Room"), + ] + + if world.options.extras_enabled: + regions.insert(68, create_region(world, player, locations_per_region, "6-Extra")) + regions.insert(58, create_region(world, player, locations_per_region, "5-Extra")) + regions.insert(48, create_region(world, player, locations_per_region, "4-Extra")) + regions.insert(38, create_region(world, player, locations_per_region, "3-Extra")) + regions.insert(28, create_region(world, player, locations_per_region, "2-Extra")) + regions.insert(18, create_region(world, player, locations_per_region, "1-Extra")) + + if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: + regions.insert(74, create_region(world, player, locations_per_region, "6-Bonus")) + regions.insert(63, create_region(world, player, locations_per_region, "5-Bonus")) + regions.insert(52, create_region(world, player, locations_per_region, "4-Bonus")) + regions.insert(41, create_region(world, player, locations_per_region, "3-Bonus")) + regions.insert(29, create_region(world, player, locations_per_region, "2-Bonus")) + regions.insert(19, create_region(world, player, locations_per_region, "1-Bonus")) + + multiworld.regions += regions + + connect_starting_region(world) + + bosses = BossReqs(world) + + multiworld.get_region("Overworld", player).add_exits( + ["World 1", "World 2", "World 3", "World 4", "World 5", "World 6"], + { + "World 1": lambda state: state.has("World 1 Gate", player), + "World 2": lambda state: state.has("World 2 Gate", player), + "World 3": lambda state: state.has("World 3 Gate", player), + "World 4": lambda state: state.has("World 4 Gate", player), + "World 5": lambda state: state.has("World 5 Gate", player), + "World 6": lambda state: state.has("World 6 Gate", player) + } + ) + + for cur_world in range(1, 7): + for cur_level in range(8): + if cur_world != 6 or cur_level != 7: + multiworld.get_region(f"World {cur_world}", player).add_exits( + [world.level_location_list[(cur_world - 1) * 8 + cur_level]] + ) + + multiworld.get_region("1-4", player).add_exits([world.boss_order[0]],{world.boss_order[0]: lambda state: logic._14Clear(state)}) + multiworld.get_region("1-8", player).add_exits([world.boss_order[1]],{world.boss_order[1]: lambda state: logic._18Clear(state)}) + multiworld.get_region("2-4", player).add_exits([world.boss_order[2]],{world.boss_order[2]: lambda state: logic._24Clear(state)}) + multiworld.get_region("2-8", player).add_exits([world.boss_order[3]],{world.boss_order[3]: lambda state: logic._28Clear(state)}) + multiworld.get_region("3-4", player).add_exits([world.boss_order[4]],{world.boss_order[4]: lambda state: logic._34Clear(state)}) + multiworld.get_region("3-8", player).add_exits([world.boss_order[5]],{world.boss_order[5]: lambda state: logic._38Clear(state)}) + multiworld.get_region("4-4", player).add_exits([world.boss_order[6]],{world.boss_order[6]: lambda state: logic._44Clear(state)}) + multiworld.get_region("4-8", player).add_exits([world.boss_order[7]],{world.boss_order[7]: lambda state: logic._48Clear(state)}) + multiworld.get_region("5-4", player).add_exits([world.boss_order[8]],{world.boss_order[8]: lambda state: logic._54Clear(state)}) + multiworld.get_region("5-8", player).add_exits([world.boss_order[9]],{world.boss_order[9]: lambda state: logic._58Clear(state)}) + multiworld.get_region("World 6", player).add_exits(["6-8"],{"6-8": lambda state: bosses.castle_access(state)}) + multiworld.get_region("6-4", player).add_exits([world.boss_order[10]],{world.boss_order[10]: lambda state: logic._64Clear(state)}) + multiworld.get_region("6-8", player).add_exits(["Bowser's Room"],{"Bowser's Room": lambda state: bosses.castle_clear(state)}) + + if world.options.extras_enabled: + multiworld.get_region("World 1", player).add_exits( + ["1-Extra"], + {"1-Extra": lambda state: state.has_any({"Extra Panels", "Extra 1"}, player)} + ) + multiworld.get_region("World 2", player).add_exits( + ["2-Extra"], + {"2-Extra": lambda state: state.has_any({"Extra Panels", "Extra 2"}, player)} + ) + multiworld.get_region( + "World 3", player).add_exits(["3-Extra"], + {"3-Extra": lambda state: state.has_any({"Extra Panels", "Extra 3"}, player)} + ) + multiworld.get_region("World 4", player).add_exits( + ["4-Extra"], + {"4-Extra": lambda state: state.has_any({"Extra Panels", "Extra 4"}, player)} + ) + multiworld.get_region("World 5", player).add_exits( + ["5-Extra"], + {"5-Extra": lambda state: state.has_any({"Extra Panels", "Extra 5"}, player)} + ) + multiworld.get_region("World 6", player).add_exits( + ["6-Extra"], + {"6-Extra": lambda state: state.has_any({"Extra Panels", "Extra 6"}, player)} + ) + + if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: + multiworld.get_region("World 1", player).add_exits( + ["1-Bonus"], + {"1-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 1"}, player)} + ) + multiworld.get_region("World 2", player).add_exits( + ["2-Bonus"], + {"2-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 2"}, player)} + ) + multiworld.get_region("World 3", player).add_exits( + ["3-Bonus"], + {"3-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 3"}, player)} + ) + multiworld.get_region("World 4", player).add_exits( + ["4-Bonus"], + {"4-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 4"}, player)} + ) + multiworld.get_region("World 5", player).add_exits( + ["5-Bonus"], + {"5-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 5"}, player)} + ) + multiworld.get_region("World 6", player).add_exits( + ["6-Bonus"], + {"6-Bonus": lambda state: state.has_any({"Bonus Panels", "Bonus 6"}, player)} + ) + + +def create_location(player: int, location_data: LocationData, region: Region) -> Location: + location = YoshisIslandLocation(player, location_data.name, location_data.code, region) + location.access_rule = location_data.rule + location.level_id = location_data.LevelID + + return location + + +def create_region(world: "YoshisIslandWorld", player: int, locations_per_region: Dict[str, List[LocationData]], name: str) -> Region: + region = Region(name, player, world.multiworld) + + if name in locations_per_region: + for location_data in locations_per_region[name]: + location = create_location(player, location_data, region) + region.locations.append(location) + + return region + +def connect_starting_region(world: "YoshisIslandWorld") -> None: + multiworld = world.multiworld + player = world.player + menu = multiworld.get_region("Menu", player) + world_main = multiworld.get_region("Overworld", player) + + starting_region = multiworld.get_region(f"World {world.options.starting_world + 1}", player) + + menu.connect(world_main, "Start Game") + world_main.connect(starting_region, "Overworld") + + +def get_locations_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]: + per_region: Dict[str, List[LocationData]] = {} + + for location in locations: + per_region.setdefault(location.region, []).append(location) + + return per_region diff --git a/worlds/yoshisisland/Rom.py b/worlds/yoshisisland/Rom.py new file mode 100644 index 000000000000..fa3006afcf9f --- /dev/null +++ b/worlds/yoshisisland/Rom.py @@ -0,0 +1,1230 @@ +import hashlib +import os +import Utils +from worlds.Files import APDeltaPatch +from settings import get_settings +from typing import TYPE_CHECKING + +from .Options import YoshiColors, BowserDoor, PlayerGoal, MinigameChecks + +if TYPE_CHECKING: + from . import YoshisIslandWorld +USHASH = "cb472164c5a71ccd3739963390ec6a50" + +item_values = { + 0x302050: [0x1467, 0x01], # ! Switch + 0x302051: [0x1467, 0x02], # Dashed Platform + 0x302052: [0x1467, 0x03], # Dashed Stairs + 0x302053: [0x1467, 0x04], # Beanstalk + 0x302054: [0x1467, 0x05], # Helicopter + 0x302059: [0x1467, 0x06], # Mole Tank + 0x302068: [0x1467, 0x07], # Train + 0x30205E: [0x1467, 0x08], # Car + 0x302063: [0x1467, 0x09], # Submarine + 0x302055: [0x1467, 0x0A], # Spring Ball + 0x302056: [0x1467, 0x0B], # Large Spring Ball + 0x302057: [0x1467, 0x0C], # Arrow Wheel + 0x302058: [0x1467, 0x0D], # Vanishing Arrow Wheel + 0x30205A: [0x1467, 0x0E], # Watermelon + 0x30205B: [0x1467, 0x0F], # Ice Melon + 0x30205C: [0x1467, 0x10], # Fire Melon + 0x30205D: [0x1467, 0x11], # Super Star + 0x30205F: [0x1467, 0x12], # Flashing Eggs + 0x302060: [0x1467, 0x13], # Giant Eggs + 0x302061: [0x1467, 0x14], # Egg Launcher + 0x302062: [0x1467, 0x15], # Egg Plant + 0x302064: [0x1467, 0x16], # Chomp Rock + 0x302065: [0x1467, 0x17], # Poochy + 0x302066: [0x1467, 0x18], # Platform Ghost + 0x302067: [0x1467, 0x19], # Skis + 0x302069: [0x1467, 0x1A], # Key + 0x30206A: [0x1467, 0x1B], # Middle Ring + 0x30206B: [0x1467, 0x1C], # Bucket + 0x30206C: [0x1467, 0x1D], # Tulip + 0x302081: [0x1467, 0x1E], # Secret Lens + + 0x30206D: [0x1467, 0x1F], # Egg Capacity Upgrade + + 0x30206E: [0x1467, 0x20], # World 1 Gate + 0x30206F: [0x1467, 0x21], # World 2 Gate + 0x302070: [0x1467, 0x22], # World 3 Gate + 0x302071: [0x1467, 0x23], # World 4 Gate + 0x302072: [0x1467, 0x24], # World 5 Gate + 0x302073: [0x1467, 0x25], # World 6 Gate + + 0x302074: [0x1467, 0x26], # Extra 1 + 0x302075: [0x1467, 0x27], # Extra 2 + 0x302076: [0x1467, 0x28], # Extra 3 + 0x302077: [0x1467, 0x29], # Extra 4 + 0x302078: [0x1467, 0x2A], # Extra 5 + 0x302079: [0x1467, 0x2B], # Extra 6 + 0x30207A: [0x1467, 0x2C], # Extra Panels + + 0x30207B: [0x1467, 0x2D], # Bonus 1 + 0x30207C: [0x1467, 0x2E], # Bonus 2 + 0x30207D: [0x1467, 0x2F], # Bonus 3 + 0x30207E: [0x1467, 0x30], # Bonus 4 + 0x30207F: [0x1467, 0x31], # Bonus 5 + 0x302080: [0x1467, 0x32], # Bonus 6 + 0x302082: [0x1467, 0x33], # Bonus Panels + + 0x302083: [0x1467, 0x34], # Anytime Egg + 0x302084: [0x1467, 0x35], # Anywhere Pow + 0x302085: [0x1467, 0x36], # Cloud + 0x302086: [0x1467, 0x37], # Pocket Melon + 0x302088: [0x1467, 0x38], # Ice Melon + 0x302087: [0x1467, 0x39], # Fire Melon + 0x302089: [0x1467, 0x3A], # Magnifying Glass + 0x30208A: [0x1467, 0x3B], # 10 Stars + 0x30208B: [0x1467, 0x3C], # 20 Stars + + 0x30208C: [0x1467, 0x3D], # 1up + 0x30208D: [0x1467, 0x3E], # 2up + 0x30208E: [0x1467, 0x3F], # 3up + 0x30208F: [0x1467, 0x40], # 10up + + 0x302090: [0x1467, 0x41], # Fuzzy Trap + 0x302093: [0x1467, 0x42], # Freeze Trap + 0x302091: [0x1467, 0x43], # Reverse Trap + 0x302092: [0x1467, 0x44], # Dark Trap + 0x302094: [0x1467, 0x00], # Boss clear, local handling + + 0x302095: [0x1467, 0x45] # Luigi Piece + +} + +location_table = { + # 1-1 + 0x305020: [0x146D, 0], # Red Coins + 0x305021: [0x146D, 1], # Flowers + 0x305022: [0x146D, 2], # Stars + 0x305023: [0x146D, 3], # Level Clear + # 1-2 + 0x305024: [0x146E, 0], + 0x305025: [0x146E, 1], + 0x305026: [0x146E, 2], + 0x305027: [0x146E, 3], + # 1-3 + 0x305028: [0x146F, 0], + 0x305029: [0x146F, 1], + 0x30502A: [0x146F, 2], + 0x30502B: [0x146F, 3], + 0x3050F8: [0x146F, 4], + # 1-4 + 0x30502C: [0x1470, 0], + 0x30502D: [0x1470, 1], + 0x30502E: [0x1470, 2], + 0x30502F: [0x1470, 3], + # 1-5 + 0x305031: [0x1471, 0], + 0x305032: [0x1471, 1], + 0x305033: [0x1471, 2], + 0x305034: [0x1471, 3], + # 1-6 + 0x305035: [0x1472, 0], + 0x305036: [0x1472, 1], + 0x305037: [0x1472, 2], + 0x305038: [0x1472, 3], + # 1-7 + 0x305039: [0x1473, 0], + 0x30503A: [0x1473, 1], + 0x30503B: [0x1473, 2], + 0x30503C: [0x1473, 3], + 0x3050F9: [0x1473, 4], + # 1-8 + 0x30503D: [0x1474, 0], + 0x30503E: [0x1474, 1], + 0x30503F: [0x1474, 2], + 0x305040: [0x1474, 3], + # 1-E + 0x3050E0: [0x1475, 0], + 0x3050E1: [0x1475, 1], + 0x3050E2: [0x1475, 2], + 0x3050E3: [0x1475, 3], + # 1-B + 0x305106: [0x1476, 4], + ###################### + # 2-1 + 0x305041: [0x1479, 0], + 0x305042: [0x1479, 1], + 0x305043: [0x1479, 2], + 0x305044: [0x1479, 3], + 0x3050FA: [0x1479, 4], + # 2-2 + 0x305045: [0x147A, 0], + 0x305046: [0x147A, 1], + 0x305047: [0x147A, 2], + 0x305048: [0x147A, 3], + # 2-3 + 0x305049: [0x147B, 0], + 0x30504A: [0x147B, 1], + 0x30504B: [0x147B, 2], + 0x30504C: [0x147B, 3], + 0x3050FB: [0x147B, 4], + # 2-4 + 0x30504D: [0x147C, 0], + 0x30504E: [0x147C, 1], + 0x30504F: [0x147C, 2], + 0x305050: [0x147C, 3], + # 2-5 + 0x305051: [0x147D, 0], + 0x305052: [0x147D, 1], + 0x305053: [0x147D, 2], + 0x305054: [0x147D, 3], + # 2-6 + 0x305055: [0x147E, 0], + 0x305056: [0x147E, 1], + 0x305057: [0x147E, 2], + 0x305058: [0x147E, 3], + 0x3050FC: [0x147E, 4], + # 2-7 + 0x305059: [0x147F, 0], + 0x30505A: [0x147F, 1], + 0x30505B: [0x147F, 2], + 0x30505C: [0x147F, 3], + 0x3050FD: [0x147F, 4], + # 2-8 + 0x30505D: [0x1480, 0], + 0x30505E: [0x1480, 1], + 0x30505F: [0x1480, 2], + 0x305060: [0x1480, 3], + # 2-E + 0x3050E4: [0x1481, 0], + 0x3050E5: [0x1481, 1], + 0x3050E6: [0x1481, 2], + 0x3050E7: [0x1481, 3], + # 2-B + 0x305107: [0x1482, 4], + ###################### + # 3-1 + 0x305061: [0x1485, 0], + 0x305062: [0x1485, 1], + 0x305063: [0x1485, 2], + 0x305064: [0x1485, 3], + # 3-2 + 0x305065: [0x1486, 0], + 0x305066: [0x1486, 1], + 0x305067: [0x1486, 2], + 0x305068: [0x1486, 3], + 0x3050FE: [0x1486, 4], + # 3-3 + 0x305069: [0x1487, 0], + 0x30506A: [0x1487, 1], + 0x30506B: [0x1487, 2], + 0x30506C: [0x1487, 3], + # 3-4 + 0x30506D: [0x1488, 0], + 0x30506E: [0x1488, 1], + 0x30506F: [0x1488, 2], + 0x305070: [0x1488, 3], + # 3-5 + 0x305071: [0x1489, 0], + 0x305072: [0x1489, 1], + 0x305073: [0x1489, 2], + 0x305074: [0x1489, 3], + # 3-6 + 0x305075: [0x148A, 0], + 0x305076: [0x148A, 1], + 0x305077: [0x148A, 2], + 0x305078: [0x148A, 3], + # 3-7 + 0x305079: [0x148B, 0], + 0x30507A: [0x148B, 1], + 0x30507B: [0x148B, 2], + 0x30507C: [0x148B, 3], + 0x3050FF: [0x148B, 4], + # 3-8 + 0x30507D: [0x148C, 0], + 0x30507E: [0x148C, 1], + 0x30507F: [0x148C, 2], + 0x305080: [0x148C, 3], + # 3-E + 0x3050E8: [0x148D, 0], + 0x3050E9: [0x148D, 1], + 0x3050EA: [0x148D, 2], + 0x3050EB: [0x148D, 3], + # 3-B + 0x305108: [0x148E, 4], + ###################### + # 4-1 + 0x305081: [0x1491, 0], + 0x305082: [0x1491, 1], + 0x305083: [0x1491, 2], + 0x305084: [0x1491, 3], + # 4-2 + 0x305085: [0x1492, 0], + 0x305086: [0x1492, 1], + 0x305087: [0x1492, 2], + 0x305088: [0x1492, 3], + 0x305100: [0x1492, 4], + # 4-3 + 0x305089: [0x1493, 0], + 0x30508A: [0x1493, 1], + 0x30508B: [0x1493, 2], + 0x30508C: [0x1493, 3], + # 4-4 + 0x30508D: [0x1494, 0], + 0x30508E: [0x1494, 1], + 0x30508F: [0x1494, 2], + 0x305090: [0x1494, 3], + # 4-5 + 0x305091: [0x1495, 0], + 0x305092: [0x1495, 1], + 0x305093: [0x1495, 2], + 0x305094: [0x1495, 3], + # 4-6 + 0x305095: [0x1496, 0], + 0x305096: [0x1496, 1], + 0x305097: [0x1496, 2], + 0x305098: [0x1496, 3], + 0x305101: [0x1496, 4], + # 4-7 + 0x305099: [0x1497, 0], + 0x30509A: [0x1497, 1], + 0x30509B: [0x1497, 2], + 0x30509C: [0x1497, 3], + 0x305102: [0x1497, 4], + # 4-8 + 0x30509D: [0x1498, 0], + 0x30509E: [0x1498, 1], + 0x30509F: [0x1498, 2], + 0x3050A0: [0x1498, 3], + # 4-E + 0x3050EC: [0x1499, 0], + 0x3050ED: [0x1499, 1], + 0x3050EE: [0x1499, 2], + 0x3050EF: [0x1499, 3], + # 4-B + 0x305109: [0x149A, 4], + ###################### + # 5-1 + 0x3050A1: [0x149D, 0], + 0x3050A2: [0x149D, 1], + 0x3050A3: [0x149D, 2], + 0x3050A4: [0x149D, 3], + 0x305103: [0x149D, 4], + # 5-2 + 0x3050A5: [0x149E, 0], + 0x3050A6: [0x149E, 1], + 0x3050A7: [0x149E, 2], + 0x3050A8: [0x149E, 3], + # 5-3 + 0x3050A9: [0x149F, 0], + 0x3050AA: [0x149F, 1], + 0x3050AB: [0x149F, 2], + 0x3050AC: [0x149F, 3], + # 5-4 + 0x3050AD: [0x14A0, 0], + 0x3050AE: [0x14A0, 1], + 0x3050AF: [0x14A0, 2], + 0x3050B0: [0x14A0, 3], + # 5-5 + 0x3050B1: [0x14A1, 0], + 0x3050B2: [0x14A1, 1], + 0x3050B3: [0x14A1, 2], + 0x3050B4: [0x14A1, 3], + # 5-6 + 0x3050B5: [0x14A2, 0], + 0x3050B6: [0x14A2, 1], + 0x3050B7: [0x14A2, 2], + 0x3050B8: [0x14A2, 3], + # 5-7 + 0x3050B9: [0x14A3, 0], + 0x3050BA: [0x14A3, 1], + 0x3050BB: [0x14A3, 2], + 0x3050BC: [0x14A3, 3], + # 5-8 + 0x3050BD: [0x14A4, 0], + 0x3050BE: [0x14A4, 1], + 0x3050BF: [0x14A4, 2], + 0x3050C0: [0x14A4, 3], + # 5-E + 0x3050F0: [0x14A5, 0], + 0x3050F1: [0x14A5, 1], + 0x3050F2: [0x14A5, 2], + 0x3050F3: [0x14A5, 3], + # 5-B + 0x30510A: [0x14A6, 4], + ####################### + # 6-1 + 0x3050C1: [0x14A9, 0], + 0x3050C2: [0x14A9, 1], + 0x3050C3: [0x14A9, 2], + 0x3050C4: [0x14A9, 3], + 0x305104: [0x14A9, 4], + # 6-2 + 0x3050C5: [0x14AA, 0], + 0x3050C6: [0x14AA, 1], + 0x3050C7: [0x14AA, 2], + 0x3050C8: [0x14AA, 3], + # 6-3 + 0x3050C9: [0x14AB, 0], + 0x3050CA: [0x14AB, 1], + 0x3050CB: [0x14AB, 2], + 0x3050CC: [0x14AB, 3], + # 6-4 + 0x3050CD: [0x14AC, 0], + 0x3050CE: [0x14AC, 1], + 0x3050CF: [0x14AC, 2], + 0x3050D0: [0x14AC, 3], + # 6-5 + 0x3050D1: [0x14AD, 0], + 0x3050D2: [0x14AD, 1], + 0x3050D3: [0x14AD, 2], + 0x3050D4: [0x14AD, 3], + # 6-6 + 0x3050D5: [0x14AE, 0], + 0x3050D6: [0x14AE, 1], + 0x3050D7: [0x14AE, 2], + 0x3050D8: [0x14AE, 3], + # 6-7 + 0x3050D9: [0x14AF, 0], + 0x3050DA: [0x14AF, 1], + 0x3050DB: [0x14AF, 2], + 0x3050DC: [0x14AF, 3], + 0x305105: [0x14AF, 4], + # 6-8 + 0x3050DD: [0x14B0, 0], + 0x3050DE: [0x14B0, 1], + 0x3050DF: [0x14B0, 2], + # 6-E + 0x3050F4: [0x14B1, 0], + 0x3050F5: [0x14B1, 1], + 0x3050F6: [0x14B1, 2], + 0x3050F7: [0x14B1, 3], + # 6-B + 0x30510B: [0x14B2, 4] +} + +class LocalRom(object): + + def __init__(self, file: str) -> None: + self.name = None + self.hash = hash + self.orig_buffer = None + + with open(file, "rb") as stream: + self.buffer = Utils.read_snes_rom(stream) + + 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.buffer[address] + + def read_bytes(self, startaddress: int, length: int) -> bytes: + return self.buffer[startaddress:startaddress + length] + + def write_byte(self, address: int, value: int) -> None: + self.buffer[address] = value + + def write_bytes(self, startaddress: int, values: bytearray) -> None: + self.buffer[startaddress:startaddress + len(values)] = values + + def write_to_file(self, file: str) -> None: + with open(file, "wb") as outfile: + outfile.write(self.buffer) + + def read_from_file(self, file: str) -> None: + with open(file, "rb") as stream: + self.buffer = bytearray(stream.read()) + +def handle_items(rom: LocalRom) -> None: + rom.write_bytes(0x0077B0, bytearray([0xE2, 0x20, 0xAD, 0x40, 0x14, 0xC2, 0x20, 0xF0, 0x08, 0xBD, 0x82, 0x71, 0x18, 0x5C, 0x3B, 0xB6])) + rom.write_bytes(0x0077C0, bytearray([0x0E, 0x5C, 0x97, 0xB6, 0x0E, 0xA0, 0xFF, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xD0, 0x02, 0xA0])) + rom.write_bytes(0x0077D0, bytearray([0x05, 0x98, 0x9D, 0xA2, 0x74, 0x6B, 0xE2, 0x20, 0xBD, 0x60, 0x73, 0xDA, 0xC2, 0x20, 0xA2, 0x00])) + rom.write_bytes(0x0077E0, bytearray([0xDF, 0x70, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8, 0xE0, 0x08, 0xF0, 0x23, 0x80, 0xF2, 0xE2, 0x20])) + rom.write_bytes(0x0077F0, bytearray([0x8A, 0x4A, 0xAA, 0xBF, 0x78, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14, 0xC2, 0x20, 0xF0, 0x08, 0xFA])) + rom.write_bytes(0x007800, bytearray([0xA0, 0x05, 0x98, 0x9D, 0xA2, 0x74, 0x60, 0xFA, 0x22, 0xC5, 0xF7, 0x00, 0xEA, 0xEA, 0x60, 0xFA])) + rom.write_bytes(0x007810, bytearray([0x60, 0x22, 0x23, 0xAF, 0x03, 0x20, 0xD6, 0xF7, 0x6B, 0x20, 0x2F, 0xF8, 0xE2, 0x20, 0xC9, 0x00])) + rom.write_bytes(0x007820, bytearray([0xD0, 0x03, 0xC2, 0x20, 0x6B, 0xC2, 0x20, 0xBD, 0x60, 0x73, 0x38, 0x5C, 0xB1, 0xC9, 0x03, 0xDA])) + rom.write_bytes(0x007830, bytearray([0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF, 0x7C, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8, 0xE0, 0x0A, 0xF0])) + rom.write_bytes(0x007840, bytearray([0x13, 0x80, 0xF2, 0xE2, 0x20, 0x8A, 0x4A, 0xAA, 0xBF, 0x86, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14])) + rom.write_bytes(0x007850, bytearray([0xFA, 0xC2, 0x20, 0x60, 0xA9, 0x01, 0x00, 0xFA, 0x60, 0x20, 0x2F, 0xF8, 0xE2, 0x20, 0xC9, 0x00])) + rom.write_bytes(0x007860, bytearray([0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7, 0x00, 0xA5, 0x14])) + rom.write_bytes(0x007870, bytearray([0x29, 0x0F, 0x00, 0x5C, 0x9A, 0xC9, 0x03, 0x5A, 0xE2, 0x10, 0x20, 0x2F, 0xF8, 0xC2, 0x10, 0x7A])) + rom.write_bytes(0x007880, bytearray([0xE2, 0x20, 0xC9, 0x00, 0xC2, 0x20, 0xD0, 0x08, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xF0, 0x04])) + rom.write_bytes(0x007890, bytearray([0x22, 0x3C, 0xAA, 0x03, 0xE2, 0x10, 0x5C, 0x47, 0xC9, 0x03, 0x22, 0x23, 0xAF, 0x03, 0xBD, 0x60])) + rom.write_bytes(0x0078A0, bytearray([0x73, 0xC9, 0x6F, 0x00, 0xF0, 0x07, 0xE2, 0x20, 0xAD, 0x4A, 0x14, 0x80, 0x05, 0xE2, 0x20, 0xAD])) + rom.write_bytes(0x0078B0, bytearray([0x49, 0x14, 0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7, 0x00])) + rom.write_bytes(0x0078C0, bytearray([0x5C, 0x2D, 0x83, 0x05, 0xBD, 0x60, 0x73, 0xC9, 0x6F, 0x00, 0xF0, 0x07, 0xE2, 0x20, 0xAD, 0x4A])) + rom.write_bytes(0x0078D0, bytearray([0x14, 0x80, 0x05, 0xE2, 0x20, 0xAD, 0x49, 0x14, 0xC2, 0x20, 0xD0, 0x04, 0x5C, 0xA0, 0x83, 0x05])) + rom.write_bytes(0x0078E0, bytearray([0xAD, 0xAC, 0x60, 0x0D, 0xAE, 0x60, 0x5C, 0x84, 0x83, 0x05, 0x22, 0x52, 0xAA, 0x03, 0xBD, 0x60])) + rom.write_bytes(0x0078F0, bytearray([0x73, 0xC9, 0x1E, 0x01, 0xE2, 0x20, 0xF0, 0x05, 0xAD, 0x4C, 0x14, 0x80, 0x03, 0xAD, 0x4B, 0x14])) + rom.write_bytes(0x007900, bytearray([0xEA, 0xC2, 0x20, 0xF0, 0x08, 0x22, 0xCF, 0xF7, 0x00, 0x5C, 0xA6, 0xF0, 0x05, 0x22, 0xC5, 0xF7])) + rom.write_bytes(0x007910, bytearray([0x00, 0x5C, 0xA6, 0xF0, 0x05, 0xE2, 0x20, 0xAD, 0x1C, 0x01, 0xC9, 0x0E, 0xC2, 0x20, 0xF0, 0x18])) + rom.write_bytes(0x007920, bytearray([0x20, 0x59, 0xF9, 0xE2, 0x20, 0xC9, 0x00, 0xF0, 0x04, 0xA9, 0x10, 0x80, 0x2A, 0xA9, 0x02, 0x9D])) + rom.write_bytes(0x007930, bytearray([0x00, 0x6F, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0xA2, 0x0A, 0xA9, 0x2F, 0xCE, 0x5C, 0x22, 0x80])) + rom.write_bytes(0x007940, bytearray([0x04, 0x20, 0x59, 0xF9, 0xE2, 0x20, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x0A, 0xAD, 0x0E, 0x30, 0x29])) + rom.write_bytes(0x007950, bytearray([0x03, 0x00, 0x5C, 0x2E, 0x80, 0x04, 0x6B, 0x80, 0xD6, 0xDA, 0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF])) + rom.write_bytes(0x007960, bytearray([0x8B, 0xAF, 0x09, 0xF0, 0x04, 0xE8, 0xE8, 0x80, 0xF6, 0xE2, 0x20, 0x8A, 0x4A, 0xAA, 0xBF, 0x91])) + rom.write_bytes(0x007970, bytearray([0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14, 0xFA, 0xC2, 0x20, 0x60, 0x22, 0x2E, 0xAA, 0x03, 0xE2, 0x20])) + rom.write_bytes(0x007980, bytearray([0xAD, 0x50, 0x14, 0xC2, 0x20, 0xD0, 0x06, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0xCF, 0xF7])) + rom.write_bytes(0x007990, bytearray([0x00, 0x5C, 0x05, 0x99, 0x02, 0x69, 0x20, 0x00, 0xC9, 0x20, 0x01, 0xB0, 0x0D, 0xE2, 0x20, 0xAD])) + rom.write_bytes(0x0079A0, bytearray([0x50, 0x14, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x3E, 0x99, 0x02, 0x5C, 0x8C, 0x99, 0x02, 0x22, 0x23])) + rom.write_bytes(0x0079B0, bytearray([0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x1C, 0x01, 0xC9, 0x02, 0xC2, 0x20, 0xD0, 0x18, 0x20, 0x59, 0xF9])) + rom.write_bytes(0x0079C0, bytearray([0xE2, 0x20, 0xC9, 0x00, 0xD0, 0x13, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0xE2, 0x20, 0xA9, 0x02])) + rom.write_bytes(0x0079D0, bytearray([0x9D, 0x00, 0x6F, 0xC2, 0x20, 0x5C, 0x35, 0x80, 0x04, 0xC2, 0x20, 0x22, 0xCF, 0xF7, 0x00, 0x80])) + rom.write_bytes(0x0079E0, bytearray([0xF2, 0xE2, 0x20, 0xAD, 0x4E, 0x14, 0xC2, 0x20, 0xF0, 0x07, 0xA9, 0x14, 0x00, 0x5C, 0x9E, 0xF1])) + rom.write_bytes(0x0079F0, bytearray([0x07, 0xA9, 0x0E, 0x00, 0x80, 0xF7, 0xBD, 0x60, 0x73, 0xDA, 0xA2, 0x00, 0xDF, 0x94, 0xAF, 0x09])) + rom.write_bytes(0x007A00, bytearray([0xF0, 0x11, 0xE0, 0x08, 0xF0, 0x04, 0xE8, 0xE8, 0x80, 0xF2, 0xFA, 0x22, 0x57, 0xF9, 0x0C, 0x5C])) + rom.write_bytes(0x007A10, bytearray([0xBD, 0xBE, 0x03, 0x8A, 0x4A, 0xE2, 0x20, 0xAA, 0xBF, 0x9E, 0xAF, 0x09, 0xAA, 0xBD, 0x40, 0x14])) + rom.write_bytes(0x007A20, bytearray([0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x02, 0x80, 0xE2, 0x4C, 0x61, 0xFF, 0x00, 0x9D, 0x00, 0x6F, 0x74])) + rom.write_bytes(0x007A30, bytearray([0x78, 0x74, 0x18, 0x74, 0x76, 0x9E, 0x36, 0x7A, 0x9E, 0x38, 0x7A, 0x9E, 0x38, 0x7D, 0xBC, 0xC2])) + rom.write_bytes(0x007A40, bytearray([0x77, 0xB9, 0xB5, 0xBE, 0x9D, 0x20, 0x72, 0xA9, 0x00, 0xFC, 0x9D, 0x22, 0x72, 0xA9, 0x40, 0x00])) + rom.write_bytes(0x007A50, bytearray([0x9D, 0x42, 0x75, 0xA9, 0x90, 0x00, 0x22, 0xD2, 0x85, 0x00, 0x6B, 0x5A, 0xE2, 0x20, 0xAD, 0x51])) + rom.write_bytes(0x007A60, bytearray([0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x0D, 0x22, 0xCF, 0xF7, 0x00, 0x7A, 0x9B, 0xAD, 0x30, 0x00])) + rom.write_bytes(0x007A70, bytearray([0x5C, 0x62, 0xB7, 0x03, 0x22, 0xC5, 0xF7, 0x00, 0x7A, 0x9B, 0xA9, 0x03, 0x00, 0x5C, 0x62, 0xB7])) + rom.write_bytes(0x007A80, bytearray([0x03, 0x22, 0x23, 0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x53, 0x14, 0xF0, 0x07, 0xC2, 0x20, 0x22, 0xCF])) + rom.write_bytes(0x007A90, bytearray([0xF7, 0x00, 0x6B, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0x6B, 0xE2, 0x20, 0xAD, 0x53, 0x14, 0xF0])) + rom.write_bytes(0x007AA0, bytearray([0x07, 0xC2, 0x20, 0x22, 0x78, 0xBA, 0x07, 0x6B, 0xC2, 0x20, 0x6B, 0xC9, 0x06, 0x00, 0xB0, 0x0F])) + rom.write_bytes(0x007AB0, bytearray([0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x94, 0x81, 0x07, 0x5C])) + rom.write_bytes(0x007AC0, bytearray([0xFB, 0x81, 0x07, 0x22, 0x23, 0xAF, 0x03, 0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC2, 0x20, 0xF0, 0x08])) + rom.write_bytes(0x007AD0, bytearray([0x22, 0xCF, 0xF7, 0x00, 0x5C, 0xF7, 0x80, 0x07, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0xF7, 0x80, 0x07])) + rom.write_bytes(0x007AE0, bytearray([0x5A, 0xE2, 0x20, 0xAD, 0x55, 0x14, 0xC2, 0x20, 0xF0, 0x06, 0x22, 0xCF, 0xF7, 0x00, 0x80, 0x06])) + rom.write_bytes(0x007AF0, bytearray([0x22, 0xC5, 0xF7, 0x00, 0x80, 0x04, 0x22, 0x65, 0xC3, 0x0E, 0x7A, 0x5C, 0xFA, 0xBE, 0x0E, 0xE2])) + rom.write_bytes(0x007B00, bytearray([0x20, 0xAD, 0x56, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0x22, 0xCF, 0xF7, 0x00, 0x22, 0xB7, 0xA5, 0x03])) + rom.write_bytes(0x007B10, bytearray([0x80, 0x04, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x3D, 0x96, 0x07, 0xBD, 0x02, 0x79, 0x85, 0x0E, 0xE2])) + rom.write_bytes(0x007B20, bytearray([0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0, 0x05, 0x22, 0xCF, 0xF7, 0x00, 0x6B, 0x22, 0xC5, 0xF7])) + rom.write_bytes(0x007B30, bytearray([0x00, 0x6B, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xD0, 0x0C, 0xAD, 0x74, 0x79, 0x29, 0x01])) + rom.write_bytes(0x007B40, bytearray([0x00, 0xD0, 0x04, 0x5C, 0x4A, 0xF3, 0x06, 0xBD, 0xD6, 0x79, 0x38, 0xFD, 0xE2, 0x70, 0x5C, 0x45])) + rom.write_bytes(0x007B50, bytearray([0xF2, 0x06, 0xAD, 0xAA, 0x60, 0x48, 0x30, 0x0E, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0])) + rom.write_bytes(0x007B60, bytearray([0x05, 0x68, 0x5C, 0xA0, 0xF3, 0x06, 0x68, 0x5C, 0xE6, 0xF3, 0x06, 0xBD, 0x02, 0x79, 0x85, 0x0E])) + rom.write_bytes(0x007B70, bytearray([0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20, 0xD0, 0x08, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x35, 0xE5])) + rom.write_bytes(0x007B80, bytearray([0x06, 0x22, 0xCF, 0xF7, 0x00, 0x5C, 0x35, 0xE5, 0x06, 0xE2, 0x20, 0xAD, 0x57, 0x14, 0xC2, 0x20])) + rom.write_bytes(0x007B90, bytearray([0xD0, 0x0C, 0xAD, 0x74, 0x79, 0x29, 0x01, 0x00, 0xD0, 0x04, 0x5C, 0x48, 0xF3, 0x06, 0xBD, 0x36])) + rom.write_bytes(0x007BA0, bytearray([0x7A, 0x38, 0xE9, 0x08, 0x00, 0x5C, 0x63, 0xE8, 0x06, 0xAD, 0xAA, 0x60, 0x30, 0x0D, 0xE2, 0x20])) + rom.write_bytes(0x007BB0, bytearray([0xAD, 0x57, 0x14, 0xC2, 0x20, 0xF0, 0x04, 0x5C, 0x99, 0xE8, 0x06, 0x5C, 0xF1, 0xE8, 0x06, 0x9C])) + rom.write_bytes(0x007BC0, bytearray([0xB0, 0x61, 0x9C, 0x8C, 0x0C, 0xE2, 0x20, 0xAD, 0x58, 0x14, 0xC2, 0x20, 0xF0, 0x07, 0x9C, 0x8E])) + rom.write_bytes(0x007BD0, bytearray([0x0C, 0x5C, 0x9D, 0xA4, 0x02, 0xA9, 0x00, 0x00, 0x8F, 0xAE, 0x00, 0x70, 0x8F, 0xAC, 0x00, 0x70])) + rom.write_bytes(0x007BE0, bytearray([0xE2, 0x20, 0xA9, 0xFE, 0x9D, 0x78, 0x79, 0x8F, 0x49, 0x00, 0x7E, 0xC2, 0x20, 0x5C, 0x9D, 0xA4])) + rom.write_bytes(0x007BF0, bytearray([0x02, 0xE2, 0x20, 0xAF, 0x49, 0x00, 0x7E, 0xC2, 0x20, 0xF0, 0x0D, 0xA9, 0x00, 0x00, 0x9D, 0xD8])) + rom.write_bytes(0x007C00, bytearray([0x79, 0x9D, 0x78, 0x79, 0x8F, 0x49, 0x00, 0x7E, 0xBD, 0x16, 0x7C, 0x18, 0x5C, 0x51, 0xA3, 0x02])) + rom.write_bytes(0x007C10, bytearray([0xE2, 0x20, 0xAD, 0x59, 0x14, 0xC2, 0x20, 0xD0, 0x0D, 0x22, 0xC5, 0xF7, 0x00, 0xBD, 0x38, 0x7D])) + rom.write_bytes(0x007C20, bytearray([0xF0, 0x0A, 0x5C, 0x4F, 0xA0, 0x02, 0x22, 0xCF, 0xF7, 0x00, 0x80, 0xF1, 0x5C, 0x59, 0xA0, 0x02])) + rom.write_bytes(0x007C30, bytearray([0xE2, 0x20, 0xAD, 0x59, 0x14, 0xC2, 0x20, 0xF0, 0x09, 0xBB, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x8D])) + rom.write_bytes(0x007C40, bytearray([0xA3, 0x02, 0x5C, 0x81, 0xA3, 0x02, 0xE2, 0x20, 0xAD, 0x5A, 0x14, 0xC2, 0x20, 0xF0, 0x09, 0xB5])) + rom.write_bytes(0x007C50, bytearray([0x76, 0x29, 0xFF, 0x00, 0x5C, 0x9D, 0x93, 0x02, 0x8D, 0x04, 0x30, 0xA9, 0x00, 0x00, 0x8D, 0x08])) + rom.write_bytes(0x007C60, bytearray([0x30, 0x5C, 0xA5, 0x93, 0x02, 0xE2, 0x20, 0xAD, 0x5A, 0x14, 0xC2, 0x20, 0xD0, 0x01, 0x6B, 0x22])) + rom.write_bytes(0x007C70, bytearray([0x23, 0xAF, 0x03, 0x5C, 0xDA, 0x93, 0x02, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2, 0x20, 0xF0, 0x09])) + rom.write_bytes(0x007C80, bytearray([0x9B, 0xBD, 0xD6, 0x79, 0x0A, 0x5C, 0xCA, 0xC4, 0x05, 0x6B, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2])) + rom.write_bytes(0x007C90, bytearray([0x20, 0xF0, 0x09, 0x9B, 0xBD, 0xD6, 0x79, 0x0A, 0x5C, 0xC1, 0xC8, 0x05, 0x6B, 0x22, 0x52, 0xAA])) + rom.write_bytes(0x007CA0, bytearray([0x03, 0xE2, 0x20, 0xAD, 0x5B, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00])) + rom.write_bytes(0x007CB0, bytearray([0x5C, 0xD9, 0xC4, 0x05, 0x22, 0xC5, 0xF7, 0x00, 0x5C, 0x70, 0xC5, 0x05, 0x22, 0x23, 0xAF, 0x03])) + rom.write_bytes(0x007CC0, bytearray([0xE2, 0x20, 0xAD, 0x5C, 0x14, 0xC2, 0x20, 0xF0, 0x0A, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00, 0x5C])) + rom.write_bytes(0x007CD0, bytearray([0x24, 0xC9, 0x0C, 0x22, 0xC5, 0xF7, 0x00, 0x80, 0xF6, 0xE2, 0x20, 0xAD, 0x5C, 0x14, 0xC2, 0x20])) + rom.write_bytes(0x007CE0, bytearray([0xF0, 0x08, 0x8A, 0x8D, 0x02, 0x30, 0x5C, 0x4D, 0xCD, 0x0C, 0xFA, 0x5C, 0x3A, 0xCD, 0x0C, 0x48])) + rom.write_bytes(0x007CF0, bytearray([0xDA, 0xE2, 0x20, 0xAD, 0x5D, 0x14, 0xF0, 0x33, 0xAA, 0x4C, 0x53, 0xFF, 0xFF, 0x18, 0x4C, 0x71])) + rom.write_bytes(0x007D00, bytearray([0xFF, 0x8D, 0x5E, 0x14, 0xC2, 0x20, 0xFA, 0x68, 0x1A, 0x1A, 0xC9, 0x0E, 0x00, 0x90, 0x06, 0x80])) + rom.write_bytes(0x007D10, bytearray([0x16, 0x5C, 0x15, 0xBF, 0x03, 0xE2, 0x20, 0x48, 0xBD, 0x60, 0x73, 0xC9, 0x27, 0xF0, 0x12, 0x68])) + rom.write_bytes(0x007D20, bytearray([0xCD, 0x5E, 0x14, 0xC2, 0x20, 0x90, 0xEA, 0x5C, 0xE5, 0xFA, 0x0B, 0x1A, 0x8D, 0x5D, 0x14, 0x80])) + rom.write_bytes(0x007D30, bytearray([0xC0, 0x68, 0xC2, 0x20, 0xEE, 0xCC, 0x00, 0xEE, 0xCC, 0x00, 0x80, 0xD5, 0xA8, 0x5C, 0x20, 0xBF])) + rom.write_bytes(0x007D40, bytearray([0x03, 0x8B, 0xA9, 0x03, 0x8D, 0x4B, 0x09, 0x8D, 0x01, 0x21, 0x22, 0x39, 0xB4, 0x00, 0x22, 0x79])) + rom.write_bytes(0x007D50, bytearray([0x82, 0x10, 0xDA, 0xAD, 0x0E, 0x03, 0x4A, 0xAA, 0xBF, 0xF3, 0xFE, 0x06, 0xAA, 0xAD, 0x1A, 0x02])) + rom.write_bytes(0x007D60, bytearray([0x9F, 0x00, 0x7C, 0x70, 0x9C, 0x22, 0x02, 0xAF, 0x83, 0xFC, 0x0D, 0xAA, 0xBF, 0xB2, 0xAF, 0x09])) + rom.write_bytes(0x007D70, bytearray([0x0C, 0xCE, 0x00, 0xAD, 0x60, 0x14, 0x0C, 0xCE, 0x00, 0x5A, 0xC2, 0x10, 0xA2, 0xAA, 0xAF, 0xAD])) + rom.write_bytes(0x007D80, bytearray([0xCE, 0x00, 0x89, 0x01, 0xF0, 0x06, 0xA0, 0x22, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x02, 0xF0, 0x06])) + rom.write_bytes(0x007D90, bytearray([0xA0, 0x2E, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x04, 0xF0, 0x06, 0xA0, 0x3A, 0x02, 0x20, 0xCA, 0xFD])) + rom.write_bytes(0x007DA0, bytearray([0x89, 0x08, 0xF0, 0x06, 0xA0, 0x46, 0x02, 0x20, 0xCA, 0xFD, 0x89, 0x10, 0xF0, 0x06, 0xA0, 0x52])) + rom.write_bytes(0x007DB0, bytearray([0x02, 0x20, 0xCA, 0xFD, 0x89, 0x20, 0xF0, 0x06, 0xA0, 0x5E, 0x02, 0x20, 0xCA, 0xFD, 0x9C, 0x65])) + rom.write_bytes(0x007DC0, bytearray([0x02, 0xE2, 0x10, 0x7A, 0xFA, 0xAB, 0x5C, 0xB6, 0xA5, 0x17, 0xC2, 0x20, 0x48, 0xA9, 0x07, 0x00])) + rom.write_bytes(0x007DD0, bytearray([0xDA, 0x54, 0x00, 0x09, 0xFA, 0x68, 0xE2, 0x20, 0x60, 0xDA, 0x5A, 0x8B, 0xAD, 0x0E, 0x03, 0xC2])) + rom.write_bytes(0x007DE0, bytearray([0x20, 0xC2, 0x10, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2, 0x20, 0xA9, 0x00, 0xEB, 0xA9, 0x7F])) + rom.write_bytes(0x007DF0, bytearray([0xA2, 0xC0, 0x14, 0x54, 0x70, 0x7E, 0xA2, 0xC0, 0x14, 0xA0, 0x40, 0x14, 0xA9, 0x00, 0xEB, 0xA9])) + rom.write_bytes(0x007E00, bytearray([0x7F, 0x54, 0x7E, 0x7E, 0xE2, 0x10, 0xAB, 0x7A, 0xFA, 0xA9, 0x1E, 0x8D, 0x18, 0x01, 0xAF, 0x83])) + rom.write_bytes(0x007E10, bytearray([0xFC, 0x0D, 0xDA, 0xAA, 0xBF, 0xB8, 0xAF, 0x09, 0x8D, 0x18, 0x02, 0xAF, 0x88, 0xFC, 0x0D, 0x49])) + rom.write_bytes(0x007E20, bytearray([0x01, 0x8D, 0x5A, 0x14, 0xFA, 0x5C, 0x58, 0x99, 0x17, 0xAE, 0x15, 0x11, 0xAD, 0x60, 0x14, 0x89])) + rom.write_bytes(0x007E30, bytearray([0x01, 0xD0, 0x0D, 0xAF, 0x83, 0xFC, 0x0D, 0xF0, 0x07, 0x9E, 0x10, 0x00, 0x5C, 0xB1, 0xD8, 0x17])) + rom.write_bytes(0x007E40, bytearray([0xFE, 0x10, 0x00, 0x80, 0xF7, 0xA9, 0xF0, 0x85, 0x4D, 0x8D, 0x63, 0x14, 0xA9, 0x80, 0x8D, 0x20])) + rom.write_bytes(0x007E50, bytearray([0x02, 0x8D, 0x4A, 0x00, 0x5C, 0x59, 0xC1, 0x01, 0xE2, 0x20, 0xAD, 0x61, 0x14, 0x89, 0x01, 0xF0])) + rom.write_bytes(0x007E60, bytearray([0x08, 0x48, 0xA9, 0x09, 0x8F, 0x17, 0x03, 0x17, 0x68, 0x89, 0x02, 0xF0, 0x08, 0x48, 0xA9, 0x09])) + rom.write_bytes(0x007E70, bytearray([0x8F, 0x23, 0x03, 0x17, 0x68, 0x89, 0x04, 0xF0, 0x08, 0x48, 0xA9, 0x09, 0x8F, 0x2F, 0x03, 0x17])) + rom.write_bytes(0x007E80, bytearray([0x68, 0x89, 0x08, 0xF0, 0x08, 0x48, 0xA9, 0x09, 0x8F, 0x3B, 0x03, 0x17, 0x68, 0x89, 0x10, 0xF0])) + rom.write_bytes(0x007E90, bytearray([0x08, 0x48, 0xA9, 0x09, 0x8F, 0x47, 0x03, 0x17, 0x68, 0x89, 0x20, 0xF0, 0x08, 0x48, 0xA9, 0x09])) + rom.write_bytes(0x007EA0, bytearray([0x8F, 0x53, 0x03, 0x17, 0x68, 0xAD, 0x62, 0x14, 0x89, 0x01, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F])) + rom.write_bytes(0x007EB0, bytearray([0x18, 0x03, 0x17, 0x68, 0x89, 0x02, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x24, 0x03, 0x17, 0x68])) + rom.write_bytes(0x007EC0, bytearray([0x89, 0x04, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x30, 0x03, 0x17, 0x68, 0x89, 0x08, 0xF0, 0x08])) + rom.write_bytes(0x007ED0, bytearray([0x48, 0xA9, 0x0A, 0x8F, 0x3C, 0x03, 0x17, 0x68, 0x89, 0x10, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F])) + rom.write_bytes(0x007EE0, bytearray([0x48, 0x03, 0x17, 0x68, 0x89, 0x20, 0xF0, 0x08, 0x48, 0xA9, 0x0A, 0x8F, 0x54, 0x03, 0x17, 0x68])) + rom.write_bytes(0x007EF0, bytearray([0xC2, 0x20, 0x5C, 0x26, 0xDB, 0x17, 0xAD, 0x63, 0x14, 0xF0, 0x0E, 0xA9, 0x00, 0x8D, 0x63, 0x14])) + rom.write_bytes(0x007F00, bytearray([0xA9, 0x20, 0x8D, 0x18, 0x01, 0x5C, 0x04, 0xA9, 0x17, 0xA9, 0x25, 0x80, 0xF5, 0xAD, 0x06, 0x7E])) + rom.write_bytes(0x007F10, bytearray([0xD0, 0x15, 0xE2, 0x20, 0xAD, 0x64, 0x14, 0xD0, 0x0E, 0xAF, 0x84, 0xFC, 0x0D, 0x89, 0x01, 0xD0])) + rom.write_bytes(0x007F20, bytearray([0x06, 0xC2, 0x20, 0x5C, 0x49, 0xEA, 0x0C, 0xC2, 0x20, 0x5C, 0x47, 0xEA, 0x0C, 0xAD, 0x06, 0x7E])) + rom.write_bytes(0x007F30, bytearray([0xD0, 0x15, 0xE2, 0x20, 0xAD, 0x64, 0x14, 0xD0, 0x0E, 0xAF, 0x84, 0xFC, 0x0D, 0x89, 0x02, 0xD0])) + rom.write_bytes(0x007F40, bytearray([0x06, 0xC2, 0x20, 0x5C, 0x91, 0xC0, 0x03, 0xC2, 0x20, 0x5C, 0xCC, 0xC0, 0x03])) + rom.write_bytes(0x007F53, bytearray([0xBF, 0xA3, 0xAF, 0x09, 0xE0, 0x06, 0xF0, 0x03, 0x4C, 0xFD, 0xFC, 0x4C, 0x01, 0xFD])) + rom.write_bytes(0x007F61, bytearray([0xAF, 0xAE, 0x00, 0x70, 0xD0, 0x07, 0xFA, 0xA9, 0x0E, 0x00, 0x4C, 0x2C, 0xFA, 0x4C, 0x26, 0xFA])) + rom.write_bytes(0x007F71, bytearray([0x6D, 0xCC, 0x00, 0xC9, 0x0E, 0x90, 0x02, 0xA9, 0x0E, 0x4C, 0x01, 0xFD])) + + rom.write_bytes(0x077F82, bytearray([0xE2, 0x20, 0xAD, 0x40, 0x14, 0xD0, 0x08, 0xC2, 0x20, 0x22, 0xC5, 0xF7, 0x00, 0x80])) + rom.write_bytes(0x077F90, bytearray([0x06, 0xA0, 0x00, 0x22, 0xD1, 0xF7, 0x00, 0xC2, 0x20, 0x20, 0x1A, 0xB6, 0x60, 0xE2, 0x20, 0xAD])) + rom.write_bytes(0x077FA0, bytearray([0x55, 0x14, 0xC2, 0x20, 0xD0, 0x03, 0x4C, 0xFD, 0xBE, 0x20, 0xBB, 0xBF, 0x4C, 0xFD, 0xBE])) + + rom.write_bytes(0x01FEEE, bytearray([0xB9, 0x00])) + rom.write_bytes(0x01FEF0, bytearray([0x6F, 0x48, 0xDA, 0xBD, 0x60, 0x73, 0xA2, 0x00, 0xDF, 0x70, 0xAF, 0x09, 0xF0, 0x08, 0xE8, 0xE8])) + rom.write_bytes(0x01FF00, bytearray([0xE0, 0x08, 0xF0, 0x1A, 0x80, 0xF2, 0x8A, 0x4A, 0xE2, 0x20, 0xAA, 0xBF, 0x78, 0xAF, 0x09, 0xAA])) + rom.write_bytes(0x01FF10, bytearray([0xBD, 0x40, 0x14, 0xC2, 0x20, 0xD0, 0x07, 0xFA, 0x68, 0xC9, 0x00, 0x00, 0x80, 0x05, 0xFA, 0x68])) + rom.write_bytes(0x01FF20, bytearray([0xC9, 0x10, 0x00, 0x5C, 0x34, 0xC3, 0x03, 0xAE, 0x12, 0x98, 0xE2, 0x20, 0xAD, 0x5E, 0x14, 0xC9])) + rom.write_bytes(0x01FF30, bytearray([0x0E, 0xF0, 0x08, 0x3A, 0x3A, 0xA8, 0xC2, 0x20, 0x4C, 0x15, 0xBF, 0x98, 0x80, 0xF8])) + + rom.write_bytes(0x02FFC0, bytearray([0x0C, 0xA6, 0x12, 0x6B, 0xBD, 0x60, 0x73, 0xC9, 0x1E, 0x01, 0xE2, 0x20, 0xF0, 0x05, 0xAD, 0x4C])) + rom.write_bytes(0x02FFD0, bytearray([0x14, 0x80, 0x03, 0xAD, 0x4B, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0, 0x03, 0x20, 0xF6, 0xF1, 0x4C])) + rom.write_bytes(0x02FFE0, bytearray([0xB0, 0xF0])) + + rom.write_bytes(0x017FD7, bytearray([0xE2, 0x20, 0xAD, 0x4D, 0x14, 0xC2, 0x20, 0xF0, 0x10])) + rom.write_bytes(0x017FE0, bytearray([0xBD, 0x60, 0x73, 0xC9, 0xA9, 0x01, 0xF0, 0x04, 0xA9, 0x04, 0x00, 0x60, 0xA9, 0x0A, 0x00, 0x60])) + rom.write_bytes(0x017FF0, bytearray([0x68, 0x4C, 0x90, 0xAD])) + + rom.write_bytes(0x03FF48, bytearray([0xE2, 0x20, 0xAD, 0x56, 0x14, 0xC2, 0x20, 0xD0])) + rom.write_bytes(0x03FF50, bytearray([0x03, 0x4C, 0x5B, 0x96, 0x20, 0x3D, 0x9D, 0x4C, 0x4F, 0x96])) + + +def Item_Data(rom: LocalRom) -> None: + rom.write_bytes(0x04AF70, bytearray([0xBB, 0x00, 0xBA, 0x00, 0xC7, 0x00, 0xC8, 0x00, 0x01, 0x02, 0x03, 0x03, 0xB1, 0x00, 0xB0, 0x00])) + rom.write_bytes(0x04AF80, bytearray([0xB2, 0x00, 0xAF, 0x00, 0xB4, 0x00, 0x04, 0x05, 0x06, 0x07, 0x08, 0x07, 0x00, 0x05, 0x00, 0x09])) + rom.write_bytes(0x04AF90, bytearray([0x00, 0x0D, 0x0E, 0x0F, 0x22, 0x00, 0x26, 0x00, 0x29, 0x00, 0x2A, 0x00, 0x2B, 0x00, 0x11, 0x12])) + rom.write_bytes(0x04AFA0, bytearray([0x12, 0x12, 0x12, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01])) + rom.write_bytes(0x04AFB0, bytearray([0x01, 0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A])) + + +def Server_Data(rom: LocalRom) -> None: + rom.write_bytes(0x037EAA, bytearray([0x00, 0x00, 0x01, 0x02, 0x03, 0x04])) + rom.write_bytes(0x037EB0, bytearray([0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14])) + rom.write_bytes(0x037EC0, bytearray([0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x24, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x01])) + rom.write_bytes(0x037ED0, bytearray([0x02, 0x04, 0x08, 0x10, 0x20, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30])) + rom.write_bytes(0x037EE0, bytearray([0x31, 0x32, 0x33, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0xFF, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39])) + rom.write_bytes(0x037EF0, bytearray([0x3A, 0x3B, 0x3C, 0x02, 0x6A, 0xD2, 0x04, 0x03, 0x06, 0x07, 0x08, 0x09, 0x05, 0x01, 0x02, 0x3D])) + rom.write_bytes(0x037F00, bytearray([0x3E, 0x3F, 0x40, 0x01, 0x02, 0x03, 0x0A, 0x80, 0x7E, 0x00, 0x7F, 0x80, 0x7F])) + + +def Menu_Data(rom: LocalRom) -> None: + rom.write_bytes(0x115348, bytearray([0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80])) + rom.write_bytes(0x115350, bytearray([0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E])) + rom.write_bytes(0x115360, bytearray([0x80, 0x80, 0x4E, 0x80, 0x80, 0x4E, 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03])) + rom.write_bytes(0x115370, bytearray([0x03, 0x03, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08])) + rom.write_bytes(0x115380, bytearray([0x08, 0x09, 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x24, 0x2C, 0x00, 0x06, 0x1E, 0x00, 0x06, 0x24, 0x00])) + rom.write_bytes(0x115390, bytearray([0x02, 0x24, 0x00, 0x0E, 0x04, 0x00, 0x18, 0x26, 0x00, 0x26, 0x1A, 0x00, 0x04, 0x22, 0x00, 0x24])) + rom.write_bytes(0x1153A0, bytearray([0x18, 0x00, 0x24, 0x02, 0x00, 0x16, 0x24, 0x00, 0x00, 0x2C, 0x00, 0x2A, 0x2C, 0x00, 0x2C, 0x18])) + rom.write_bytes(0x1153B0, bytearray([0x00, 0x10, 0x18, 0x00, 0x0A, 0x18, 0x00, 0x24, 0x24, 0x00, 0x0A, 0x08, 0x00, 0x0C, 0x08, 0x00])) + rom.write_bytes(0x1153C0, bytearray([0x08, 0x16, 0x00, 0x08, 0x1E, 0x00, 0x04, 0x14, 0x00, 0x1E, 0x0E, 0x00, 0x1E, 0x0C, 0x00, 0x24])) + rom.write_bytes(0x1153D0, bytearray([0x14, 0x00, 0x14, 0x30, 0x00, 0x18, 0x22, 0x00, 0x02, 0x04, 0x00, 0x26, 0x16, 0x00, 0x24, 0x16])) + rom.write_bytes(0x1153E0, bytearray([0x00, 0x5C, 0x38, 0x60, 0x4E, 0x28, 0x1A, 0x16, 0x1C, 0x04, 0x14, 0x36, 0x36, 0x36, 0x80, 0x80])) + rom.write_bytes(0x1153F0, bytearray([0x34, 0x81, 0x81, 0x4E, 0x4E, 0x4E, 0x5C, 0x38, 0x60, 0x4E, 0x04, 0x16, 0x08, 0x00, 0x22, 0x36])) + rom.write_bytes(0x115400, bytearray([0x36, 0x36, 0x36, 0x80, 0x80, 0x34, 0x81, 0x81, 0x4E, 0x4E, 0x4E, 0x50, 0x52, 0x54, 0x56, 0x58])) + rom.write_bytes(0x115410, bytearray([0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x50, 0x52, 0x54, 0x09, 0x15, 0x21, 0x2D, 0x39, 0x45, 0x0C, 0x03])) + rom.write_bytes(0x115420, bytearray([0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04])) + rom.write_bytes(0x115430, bytearray([0x06, 0x06, 0x08, 0x08, 0x0A, 0x41, 0x00, 0x3C, 0x00, 0x33, 0x00, 0x25, 0x00, 0x1B, 0x00, 0x14])) + rom.write_bytes(0x115440, bytearray([0x00, 0x0B, 0x00, 0x02, 0x00, 0xF6, 0x3F, 0xEC, 0x3F, 0xDC, 0x3F])) + + rom.write_bytes(0x082660, bytearray([0x07])) + rom.write_bytes(0x082667, bytearray([0x05])) + rom.write_bytes(0x082677, bytearray([0x0A, 0x03, 0x05])) + rom.write_bytes(0x082688, bytearray([0x00])) + + rom.write_bytes(0x11548E, bytearray([0x60, 0x3d, 0x66, 0x3b, 0x60, 0x3f, 0x60, 0x39, 0x66, 0x39, 0x66, 0x3d, 0x66, 0x3f, 0x60, 0x3b])) + rom.write_bytes(0x11549E, bytearray([0x02, 0x06, 0x04, 0x00, 0x01, 0x03, 0x05, 0x07])) + + +def CodeHandler(rom: LocalRom) -> None: + rom.write_bytes(0x073637, bytearray([0x5C, 0xB0, 0xF7, 0x00])) # Check ! Switch + rom.write_bytes(0x07360B, bytearray([0x20, 0x82, 0xFF])) # Flash ! Switch + + rom.write_bytes(0x01C2F3, bytearray([0x22, 0x11, 0xF8, 0x00])) # Check visibility of winged clouds + rom.write_bytes(0x01C32E, bytearray([0x5C, 0xEE, 0xFE, 0x03])) # Check items in winged clouds + + rom.write_bytes(0x01C9AD, bytearray([0x5C, 0x19, 0xF8, 0x00])) # Check transformations + rom.write_bytes(0x01C995, bytearray([0x5C, 0x59, 0xF8, 0x00])) # Flash transformations + rom.write_bytes(0x01C943, bytearray([0x5C, 0x77, 0xF8, 0x00])) # Fixes a bug where transformation bubbles flashing would displace the sprite + + rom.write_bytes(0x028329, bytearray([0x5C, 0x9A, 0xF8, 0x00])) # Flash Spring Ball + rom.write_bytes(0x02837E, bytearray([0x5C, 0xC4, 0xF8, 0x00])) # Check Spring Ball + + rom.write_bytes(0x02F0A2, bytearray([0x5C, 0xEA, 0xF8, 0x00])) # Flash Arrow Wheel + rom.write_bytes(0x02F0AD, bytearray([0x4C, 0xC4, 0xFF])) # Check Arrow Wheel + + rom.write_bytes(0x02001D, bytearray([0x5C, 0x15, 0xF9, 0x00])) # Check Melon + rom.write_bytes(0x020028, bytearray([0x5C, 0x41, 0xF9, 0x00])) # Secondary check for melon used to overwrite visibility on the ground + rom.write_bytes(0x020031, bytearray([0x5C, 0xAE, 0xF9, 0x00])) # Check for melons that are spawned by objects which skips the initial check + rom.write_bytes(0x012DF7, bytearray([0x20, 0xD7, 0xFF])) # Check for monkeys holding melons + rom.write_bytes(0x012E07, bytearray([0x20, 0xD7, 0xFF])) # Check for monkeys holding melons + rom.write_bytes(0x03F17D, bytearray([0x5C, 0xE1, 0xF9, 0x00])) # Fixes a bug where balloons with ice melons will write to yoshi's mouth before deactivating the melon. + + rom.write_bytes(0x011901, bytearray([0x5C, 0x7A, 0xF9, 0x00])) # Flash Super Star + rom.write_bytes(0x01192A, bytearray([0x5C, 0x95, 0xF9, 0x00])) # Check Super Star + + rom.write_bytes(0x01BEB9, bytearray([0x5C, 0xF6, 0xF9, 0x00])) # Check egg-type items + rom.write_bytes(0x01B75E, bytearray([0x5C, 0x5B, 0xFA, 0x00])) # Flash flashing eggs and force them to purple + + rom.write_bytes(0x03BA31, bytearray([0x22, 0x81, 0xFA, 0x00])) # Flash Arrow Cloud + rom.write_bytes(0x03BA35, bytearray([0x22, 0x9A, 0xFA, 0x00])) # Check Arrow Cloud + rom.write_bytes(0x03BA3D, bytearray([0x22, 0x81, 0xFA, 0x00])) # Flash Arrow Cloud, rotating + rom.write_bytes(0x03BA5A, bytearray([0x22, 0x9A, 0xFA, 0x00])) # Check Arrow Cloud, rotating + + rom.write_bytes(0x03818F, bytearray([0x5C, 0xAB, 0xFA, 0x00])) # Check Egg Plant + rom.write_bytes(0x0380F3, bytearray([0x5C, 0xC3, 0xFA, 0x00])) # Flash Egg Plant + + rom.write_bytes(0x073EF6, bytearray([0x5C, 0xE0, 0xFA, 0x00])) # Flash Chomp Rock + rom.write_bytes(0x073EFA, bytearray([0x4C, 0x9D, 0xFF])) # Check Chomp Rock + + rom.write_bytes(0x039639, bytearray([0x5C, 0xFF, 0xFA, 0x00])) # Flash Poochy + rom.write_bytes(0x03964C, bytearray([0x4C, 0x48, 0xFF])) # Check Poochy + + rom.write_bytes(0x0370C2, bytearray([0x22, 0x1A, 0xFB, 0x00, 0xEA])) # Flash Platform Ghosts + rom.write_bytes(0x03723F, bytearray([0x5C, 0x32, 0xFB, 0x00])) # Fixes a bug where the eyes would assign to a random sprite while flashing + rom.write_bytes(0x03739B, bytearray([0x5C, 0x52, 0xFB, 0x00])) # Check Vertical Platform Ghost + rom.write_bytes(0x036530, bytearray([0x5C, 0x6B, 0xFB, 0x00])) # Flash horizontal ghost + rom.write_bytes(0x03685C, bytearray([0x5C, 0x89, 0xFB, 0x00])) # Fix flashing horizontal ghost + rom.write_bytes(0x036894, bytearray([0x5C, 0xA9, 0xFB, 0x00])) # Check horizontal ghost + + rom.write_bytes(0x012497, bytearray([0x5C, 0xBF, 0xFB, 0x00])) # Check Skis + rom.write_bytes(0x01234D, bytearray([0x5C, 0xF1, 0xFB, 0x00])) # Allow ski doors to be re-entered + + rom.write_bytes(0x01204A, bytearray([0x5C, 0x10, 0xFC, 0x00])) # Flash Key + rom.write_bytes(0x012388, bytearray([0x5C, 0x30, 0xFC, 0x00])) # Check Key + + rom.write_bytes(0x011398, bytearray([0x5C, 0x46, 0xFC, 0x00])) # Flash MidRing + rom.write_bytes(0x0113D6, bytearray([0x5C, 0x65, 0xFC, 0x00])) # Check MidRing + + rom.write_bytes(0x02C4C6, bytearray([0x5C, 0x77, 0xFC, 0x00])) # Check Bucket w/ Item + rom.write_bytes(0x02C8BD, bytearray([0x5C, 0x8A, 0xFC, 0x00])) # Check Bucket, ridable + rom.write_bytes(0x02C4D5, bytearray([0x5C, 0x9D, 0xFC, 0x00])) # Flash Bucket + + rom.write_bytes(0x064920, bytearray([0x5C, 0xBC, 0xFC, 0x00])) # Flash Tulip + rom.write_bytes(0x064D49, bytearray([0x5C, 0xD9, 0xFC, 0x00])) # Check Tulip + + rom.write_bytes(0x01BEC7, bytearray([0x5C, 0xEF, 0xFC, 0x00])) # Check Egg Capacity + rom.write_bytes(0x01BF12, bytearray([0x4C, 0x27, 0xFF])) # Set current egg max + rom.write_bytes(0x01BF1A, bytearray([0x5C, 0x3C, 0xFD, 0x00])) # Cap eggs + + rom.write_bytes(0x0BA5AE, bytearray([0x5C, 0x41, 0xFD, 0x00])) # Unlock Levels + + rom.write_bytes(0x0B9953, bytearray([0x5C, 0xD9, 0xFD, 0x00])) # File initialization + + rom.write_bytes(0x0BD8AB, bytearray([0x5C, 0x29, 0xFE, 0x00])) # Prevent the world 1 tab from being drawn without it being unlocked + + rom.write_bytes(0x00C155, bytearray([0x5C, 0x45, 0xFE, 0x00])) # Save between levels + + rom.write_bytes(0x0BDB20, bytearray([0x5C, 0x58, 0xFE, 0x00])) # Unlock extra and bonus stages + + rom.write_bytes(0x0BA8FF, bytearray([0x5C, 0xF6, 0xFE, 0x00])) # Skip the score animation if coming from start-select, but still save + + rom.write_bytes(0x0BA8A9, bytearray([0x80, 0x46])) # Prevent unlocking new levels + + rom.write_bytes(0x066A42, bytearray([0x5C, 0x0D, 0xFF, 0x00])) # Coin visibility + rom.write_bytes(0x01C08C, bytearray([0x5C, 0x2D, 0xFF, 0x00])) # Cloud visibility + + rom.write_bytes(0x00C0D9, bytearray([0x5C, 0xB8, 0xF3, 0x0B])) # Receive item from server + + rom.write_bytes(0x00C153, bytearray([0xEA, 0xEA])) # Always enable Start/Select + + rom.write_bytes(0x00C18B, bytearray([0x5C, 0x1B, 0xF5, 0x0B])) # Enable traps + + rom.write_bytes(0x01B365, bytearray([0x5C, 0x86, 0xF5, 0x0B])) # Red Coin checks + rom.write_bytes(0x0734C6, bytearray([0x5C, 0xCE, 0xF5, 0x0B])) # Flower checks + rom.write_bytes(0x00C0DE, bytearray([0x5C, 0xF5, 0xF5, 0x0B])) # Star checks + rom.write_bytes(0x00B580, bytearray([0x5C, 0xB1, 0xF5, 0x0B])) # Level Clear checks + + rom.write_bytes(0x0B9937, bytearray([0x5C, 0x23, 0xF6, 0x0B])) # Load AP data + rom.write_bytes(0x0BE14A, bytearray([0x5C, 0x58, 0xF6, 0x0B])) # Save AP data + + rom.write_bytes(0x00D09F, bytearray([0x5C, 0x8C, 0xF6, 0x0B])) # Clear Menu + rom.write_bytes(0x00BCB5, bytearray([0x5C, 0xAD, 0xF6, 0x0B])) # Clear Score for menu + rom.write_bytes(0x00D072, bytearray([0x5C, 0xC3, 0xF6, 0x0B])) # Loads the data for the AP menu + rom.write_bytes(0x00D07A, bytearray([0x5C, 0x5A, 0xF7, 0x0B])) # Draw the AP menu over the pause menu + rom.write_bytes(0x00D17A, bytearray([0x5C, 0xDA, 0xF7, 0x0B])) # Skip the flower counter in the AP menu + rom.write_bytes(0x00D0DE, bytearray([0x5C, 0xF1, 0xF7, 0x0B])) # Skip the coin counter in the AP menu + rom.write_bytes(0x00CFB4, bytearray([0x5C, 0x06, 0xF8, 0x0B])) # Get the number of bosses required to unlock 6-8 + rom.write_bytes(0x00CFD0, bytearray([0x5C, 0x2B, 0xF8, 0x0B])) # Get bosses for 6-8 clear + rom.write_bytes(0x00D203, bytearray([0x5C, 0xF0, 0xF8, 0x0B])) # Wipe total score line + rom.write_bytes(0x00D277, bytearray([0x5C, 0x04, 0xF9, 0x0B])) # Wipe high score line + rom.write_bytes(0x00C104, bytearray([0x5C, 0x18, 0xF9, 0x0B])) # Replace the pause menu with AP menu when SELECT is pressed + rom.write_bytes(0x00C137, bytearray([0x5C, 0x31, 0xF9, 0x0B])) # Prevent accidentally quitting out of a stage while opening the AP menu + rom.write_bytes(0x00CE48, bytearray([0x5C, 0x42, 0xF9, 0x0B])) # When closing the AP menu, reset the AP menu flag so the normal menu can be opened. + + rom.write_bytes(0x0BA5B6, bytearray([0x5C, 0x4E, 0xF9, 0x0B])) # Unlock 6-8 if the current number of defeated bosses is higher than the number of bosses required. If 6-8 is marked 'cleared', skip boss checks + rom.write_bytes(0x01209E, bytearray([0x5C, 0x92, 0xF9, 0x0B])) # Write a flag to check bosses if setting up the final boss door + rom.write_bytes(0x0123AA, bytearray([0x5C, 0xA3, 0xF9, 0x0B])) # If the boss check flag is set, read the number of bosses before opening door + + rom.write_bytes(0x015F7A, bytearray([0x5C, 0xCA, 0xF9, 0x0B])) # Write Boss Clears + + rom.write_bytes(0x0BE16E, bytearray([0x80, 0x12])) # Disable overworld bandit code + + rom.write_bytes(0x083015, bytearray([0x5C, 0x26, 0xFA, 0x0B])) # Flip Cards + rom.write_bytes(0x0839B6, bytearray([0x5C, 0x18, 0xFA, 0x0B])) # Scratch Cards + rom.write_bytes(0x085094, bytearray([0x5C, 0x31, 0xFA, 0x0B])) # Draw Lots + rom.write_bytes(0x0852C5, bytearray([0x5C, 0x3D, 0xFA, 0x0B])) # Match Cards + rom.write_bytes(0x0845EA, bytearray([0x5C, 0x48, 0xFA, 0x0B])) # Roulette + rom.write_bytes(0x083E0A, bytearray([0x5C, 0x53, 0xFA, 0x0B])) # Slots + + rom.write_bytes(0x01D845, bytearray([0x5C, 0x76, 0xF9, 0x0B])) # Check setting for disabled autoscrolls + + rom.write_bytes(0x0BDAC2, bytearray([0x80, 0x0E])) # Prevent extra and bonus stages from auto-unlocking at 100 points + rom.write_bytes(0x0BA720, bytearray([0xA9, 0x00, 0x00])) # Always read level scores as 0. This stops extras and bonus from trying to unlock + + rom.write_bytes(0x0BA720, bytearray([0xA9, 0x00, 0x00])) # Always read level scores as 0. This stops extras and bonus from trying to unlock + + rom.write_bytes(0x03FE85, bytearray([0x5C, 0x09, 0xFB, 0x0B])) # Decrement the key counter when unlocking the 6-4 cork + + rom.write_bytes(0x06F1B4, bytearray([0x5C, 0x22, 0xFB, 0x0B])) # Mark the goal and bowser clear after defeating bowser + + rom.write_bytes(0x005FE2, bytearray([0x5C, 0x9C, 0xFB, 0x0B])) # Flag red coins as checked if the last one came from a pole + + rom.write_bytes(0x01C2E1, bytearray([0x80])) # Makes hidden clouds not flash + rom.write_bytes(0x0120C0, bytearray([0x80])) # Prevents bandit game doors from sealing + + rom.write_bytes(0x0382A7, bytearray([0x5C, 0xC2, 0xFB, 0x0B])) # Make cactus eggplants check the eggplant item correctly + + rom.write_bytes(0x025E71, bytearray([0x5C, 0xFA, 0xFB, 0x0B])) # Write the stored reverse value + + rom.write_bytes(0x00B587, bytearray([0x5C, 0x24, 0xFC, 0x0B])) # Store the reverse value and zero it + + rom.write_bytes(0x0B9932, bytearray([0x5C, 0x96, 0xFA, 0x0B])) # Get 16 bit life count + + rom.write_bytes(0x00C288, bytearray([0x00])) + rom.write_bytes(0x00C28B, bytearray([0x80])) # Disable baby mario tutorial text + + rom.write_bytes(0x01141F, bytearray([0x80])) # Disable Middle Ring tutorial + + rom.write_bytes(0x073534, bytearray([0x80])) # Disable Flower tutorial + + rom.write_bytes(0x065B24, bytearray([0x5C, 0x45, 0xFC, 0x0B])) # Fix boss cutscenes + + rom.write_bytes(0x011507, bytearray([0x5C, 0x70, 0xFC, 0x0B])) # Fix Hookbill middle ring during boss shuffle + + rom.write_bytes(0x019E98, bytearray([0x5C, 0xB4, 0xFC, 0x0B])) # Flag red coins as checked if the last one was eaten + + rom.write_bytes(0x011AB6, bytearray([0x5C, 0xD7, 0xFC, 0x0B])) # Check egg refills for how many eggs to spawn + + rom.write_bytes(0x00DCA6, bytearray([0x5C, 0x00, 0xFD, 0x0B])) # Check egg refill pause use + + rom.write_bytes(0x0BE06B, bytearray([0x5C, 0x56, 0xFD, 0x0B])) # Get level from shuffled order + + rom.write_bytes(0x00C14B, bytearray([0xAE, 0x7C, 0x02, 0x8E, 0x1A, 0x02])) # Return to the original list when exiting a level + + rom.write_bytes(0x00BEA8, bytearray([0x5C, 0x3F, 0xFE, 0x0B])) # Save the original level when beating a shuffled one. + + rom.write_bytes(0x00E702, bytearray([0xAD, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0x80, 0x05])) # Save the original level when leaving through death + + rom.write_bytes(0x0BE72A, bytearray([0x7C])) # Load yoshi colors by slot number not level number + + rom.write_bytes(0x003346, bytearray([0x22, 0x54, 0xFE, 0x0B, 0xEA, 0xEA])) # Fix World 6 levels using weird tilesets + + rom.write_bytes(0x003A37, bytearray([0x22, 0x54, 0xFE, 0x0B, 0xEA, 0xEA])) # Fix World 6 levels using weird tilesets + + rom.write_bytes(0x0B87D5, bytearray([0x5C, 0x67, 0xFE, 0x0B])) + + rom.write_bytes(0x07081F, bytearray([0x80])) # Fix for weird falling chomps. Why does this even read the world number????? + + rom.write_bytes(0x0BC0B2, bytearray([0x5C, 0xD0, 0xED, 0x01])) # Load randomized yoshi colors on the world map + + rom.write_bytes(0x0BC6F7, bytearray([0x5C, 0x04, 0xEE, 0x01])) # Load selected yoshi color on the world map + + rom.write_bytes(0x0BC0AB, bytearray([0x80])) # Skip special color check for world 6; Levels handle this anyway + + +def write_lives(rom: LocalRom) -> None: + rom.write_bytes(0x05FA96, bytearray([0xC2, 0x20, 0xAF, 0x89, 0xFC, 0x0D, 0x8D, 0x79, 0x03, 0xE2, 0x20, 0x5C, 0x37, 0x99, 0x17])) + rom.write_bytes(0x05FABF, bytearray([0x48, 0xE2, 0x20, 0xAD, 0xCC, 0x00, 0xF0, 0x06, 0xCE, 0xCC, 0x00, 0xCE, 0xCC, 0x00, 0xC2, 0x20, 0x68, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x89, 0xFE, 0x07])) + + +def bonus_checks(rom: LocalRom) -> None: + rom.write_bytes(0x082156, bytearray([0x5C, 0x5F, 0xFA, 0x0B])) # Write bonus check + + +def bandit_checks(rom: LocalRom) -> None: + rom.write_bytes(0x08C9E4, bytearray([0x5C, 0xF3, 0xF9, 0x0B])) # Write Bandit Checks + + +def Handle_Locations(rom: LocalRom) -> None: + rom.write_bytes(0x05F3B8, bytearray([0xAD, 0x67, 0x14, 0xF0, 0x59, 0xDA, 0xC9, 0x1F])) + rom.write_bytes(0x05F3C0, bytearray([0xF0, 0x16, 0xC9, 0x20, 0xB0, 0x27, 0xAA, 0xBF, 0xAA, 0xFE, 0x06, 0xAA, 0xA9, 0x01, 0x9D, 0x40])) + rom.write_bytes(0x05F3D0, bytearray([0x14, 0xA9, 0x43, 0x8D, 0x53, 0x00, 0x80, 0x67, 0xAD, 0x5D, 0x14, 0xD0, 0x01, 0x1A, 0xC9, 0x06])) + rom.write_bytes(0x05F3E0, bytearray([0xF0, 0x04, 0x1A, 0x8D, 0x5D, 0x14, 0xA9, 0x03, 0x8D, 0x53, 0x00, 0x80, 0x52, 0xC9, 0x26, 0xB0])) + rom.write_bytes(0x05F3F0, bytearray([0x27, 0xA2, 0x00, 0xDF, 0xC9, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xCF, 0xFE, 0x06])) + rom.write_bytes(0x05F400, bytearray([0x8D, 0x4C, 0x00, 0xAD, 0x60, 0x14, 0x0C, 0x4C, 0x00, 0xAD, 0x4C, 0x00, 0x8D, 0x60, 0x14, 0xA9])) + rom.write_bytes(0x05F410, bytearray([0x97, 0x8D, 0x53, 0x00, 0x80, 0x29, 0x80, 0x70, 0xC9, 0x2D, 0xB0, 0x25, 0xA2, 0x00, 0xDF, 0xD5])) + rom.write_bytes(0x05F420, bytearray([0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xE3, 0xFE, 0x06, 0x8D, 0xCF, 0x00, 0xAD, 0x61])) + rom.write_bytes(0x05F430, bytearray([0x14, 0x0C, 0xCF, 0x00, 0xAD, 0xCF, 0x00, 0x8D, 0x61, 0x14, 0xA9, 0x95, 0x8D, 0x53, 0x00, 0x80])) + rom.write_bytes(0x05F440, bytearray([0x78, 0xC9, 0x34, 0xB0, 0x25, 0xA2, 0x00, 0xDF, 0xDC, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7])) + rom.write_bytes(0x05F450, bytearray([0xBF, 0xE3, 0xFE, 0x06, 0x8D, 0xCF, 0x00, 0xAD, 0x62, 0x14, 0x0C, 0xCF, 0x00, 0xAD, 0xCF, 0x00])) + rom.write_bytes(0x05F460, bytearray([0x8D, 0x62, 0x14, 0xA9, 0x95, 0x8D, 0x53, 0x00, 0x80, 0x4F, 0xC9, 0x3D, 0xB0, 0x1C, 0xA2, 0x00])) + rom.write_bytes(0x05F470, bytearray([0xDF, 0xEA, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0xF6, 0xFE, 0x06, 0x22, 0xA6, 0x9C])) + rom.write_bytes(0x05F480, bytearray([0x10, 0xA9, 0x36, 0x8D, 0x53, 0x00, 0x80, 0x31, 0x80, 0x64, 0xC9, 0x41, 0xB0, 0x2D, 0xA2, 0x00])) + rom.write_bytes(0x05F490, bytearray([0xDF, 0xFF, 0xFE, 0x06, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xA9, 0x00, 0xEB, 0xBF, 0x03, 0xFF, 0x06])) + rom.write_bytes(0x05F4A0, bytearray([0xAA, 0x18, 0xC2, 0x20, 0x6D, 0x79, 0x03, 0x8D, 0x79, 0x03, 0xE2, 0x20, 0xA9, 0x08, 0x22, 0xD2])) + rom.write_bytes(0x05F4B0, bytearray([0x85, 0x00, 0xCA, 0xE0, 0x00, 0xF0, 0x02, 0x80, 0xF5, 0x80, 0x51, 0xC9, 0x41, 0xF0, 0x1E, 0xC9])) + rom.write_bytes(0x05F4C0, bytearray([0x42, 0xF0, 0x2D, 0xC9, 0x43, 0xF0, 0x3A, 0xC2, 0x20, 0x5C, 0xFB, 0xB3, 0x21, 0x77, 0x14, 0xE2])) + rom.write_bytes(0x05F4D0, bytearray([0x20, 0xA9, 0x01, 0x8D, 0x7D, 0x02, 0xA9, 0x2E, 0x8D, 0x53, 0x00, 0x80, 0x2F, 0xA9, 0x01, 0x8D])) + rom.write_bytes(0x05F4E0, bytearray([0x68, 0x14, 0xC2, 0x20, 0xA9, 0x00, 0x04, 0x8D, 0x69, 0x14, 0xE2, 0x20, 0x80, 0x1E, 0x80, 0x22])) + rom.write_bytes(0x05F4F0, bytearray([0xC2, 0x20, 0xA9, 0x2C, 0x01, 0x8D, 0xCC, 0x0C, 0xE2, 0x20, 0xA9, 0xA0, 0x8D, 0x53, 0x00, 0x80])) + rom.write_bytes(0x05F500, bytearray([0x0B, 0xA9, 0x15, 0x8D, 0x53, 0x00, 0xA9, 0x05, 0x8F, 0xED, 0x61, 0x04, 0xFA, 0xA9, 0x00, 0x8D])) + rom.write_bytes(0x05F510, bytearray([0x67, 0x14, 0xA9, 0x10, 0x8D, 0x83, 0x0B, 0x5C, 0xDE, 0xC0, 0x01, 0xE2, 0x20, 0xAD, 0x7D, 0x02])) + rom.write_bytes(0x05F520, bytearray([0xF0, 0x25, 0xC2, 0x20, 0xAD, 0x7E, 0x02, 0xE2, 0x20, 0xF0, 0x12, 0xA9, 0x02, 0x8D, 0x00, 0x02])) + rom.write_bytes(0x05F530, bytearray([0xC2, 0x20, 0xAD, 0x7E, 0x02, 0x3A, 0x8D, 0x7E, 0x02, 0xE2, 0x20, 0x80, 0x0A, 0xA9, 0x0F, 0x8D])) + rom.write_bytes(0x05F540, bytearray([0x00, 0x02, 0xA9, 0x00, 0x8D, 0x7D, 0x02, 0xAD, 0x68, 0x14, 0xF0, 0x32, 0xC2, 0x20, 0xAD, 0x69])) + rom.write_bytes(0x05F550, bytearray([0x14, 0xF0, 0x1B, 0x3A, 0x8D, 0x69, 0x14, 0xE2, 0x20, 0x4C, 0x40, 0xFD, 0xE8, 0x1F, 0x70, 0xAD])) + rom.write_bytes(0x05F560, bytearray([0xD0, 0x00, 0xD0, 0x08, 0xEE, 0xD0, 0x00, 0xA9, 0x21, 0x8D, 0x53, 0x00, 0x80, 0x10, 0xE2, 0x20])) + rom.write_bytes(0x05F570, bytearray([0xA9, 0x22, 0x8D, 0x53, 0x00, 0xA9, 0x00, 0x8D, 0x68, 0x14, 0x8F, 0xE8, 0x1F, 0x70, 0x22, 0x59])) + rom.write_bytes(0x05F580, bytearray([0x82, 0x00, 0x5C, 0x8F, 0xC1, 0x01, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x30, 0x20, 0x48, 0xDA, 0xE2])) + rom.write_bytes(0x05F590, bytearray([0x20, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x01, 0x0C, 0xD1, 0x00, 0xAD])) + rom.write_bytes(0x05F5A0, bytearray([0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xC2, 0x20, 0xFA, 0x68, 0x5C, 0x6C, 0xB3, 0x03, 0x5C, 0x6D, 0xB3])) + rom.write_bytes(0x05F5B0, bytearray([0x03, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x08, 0x0C, 0xD1, 0x00, 0xAD])) + rom.write_bytes(0x05F5C0, bytearray([0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xAE, 0x57, 0x0B, 0xE0, 0x0D, 0x5C, 0x85, 0xB5, 0x01, 0xA0, 0x05])) + rom.write_bytes(0x05F5D0, bytearray([0x8C, 0xB8, 0x03, 0x08, 0xE2, 0x20, 0xDA, 0x48, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1])) + rom.write_bytes(0x05F5E0, bytearray([0x00, 0xA9, 0x02, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0x68, 0xFA, 0xC2, 0x20])) + rom.write_bytes(0x05F5F0, bytearray([0x28, 0x5C, 0xCB, 0xB4, 0x0E, 0xC2, 0x20, 0xAD, 0xB6, 0x03, 0xC9, 0x2C, 0x01, 0x90, 0x18, 0xE2])) + rom.write_bytes(0x05F600, bytearray([0x20, 0xDA, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9, 0x04, 0x0C, 0xD1, 0x00])) + rom.write_bytes(0x05F610, bytearray([0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0x9C, 0x84, 0x0B, 0xE2, 0x20, 0xAD, 0x0F, 0x0D, 0x5C])) + rom.write_bytes(0x05F620, bytearray([0xE4, 0xC0, 0x01, 0xC2, 0x20, 0x48, 0xE2, 0x20, 0xA9, 0x1F, 0x8D, 0x18, 0x01, 0xDA, 0x5A, 0x8B])) + rom.write_bytes(0x05F630, bytearray([0x4C, 0xB2, 0xFA, 0xC2, 0x20, 0xC2, 0x10, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xAA, 0xE2, 0x20, 0xA9])) + rom.write_bytes(0x05F640, bytearray([0x00, 0xEB, 0xA9, 0x7F, 0xA0, 0x40, 0x14, 0x54, 0x7E, 0x70, 0xE2, 0x10, 0xAB, 0x7A, 0xFA, 0xC2])) + rom.write_bytes(0x05F650, bytearray([0x20, 0x68, 0xE2, 0x20, 0x5C, 0x3C, 0x99, 0x17, 0xC2, 0x20, 0x48, 0xC2, 0x10, 0xDA, 0x5A, 0x8B])) + rom.write_bytes(0x05F660, bytearray([0xAD, 0x0E, 0x03, 0x29, 0x0F, 0x00, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2, 0x20, 0xA9, 0x00])) + rom.write_bytes(0x05F670, bytearray([0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0xE2, 0x10, 0xC2, 0x20])) + rom.write_bytes(0x05F680, bytearray([0x68, 0xE2, 0x20, 0xAD, 0x3D, 0x09, 0x29, 0x20, 0x5C, 0x4F, 0xE1, 0x17, 0xE2, 0x20, 0xAD, 0xD2])) + rom.write_bytes(0x05F690, bytearray([0x00, 0xC2, 0x20, 0xD0, 0x09, 0xA9, 0xC1, 0xB1, 0x85, 0x10, 0x5C, 0xA4, 0xD0, 0x01, 0xA9, 0x00])) + rom.write_bytes(0x05F6A0, bytearray([0x00, 0x85, 0x10, 0x85, 0x12, 0x85, 0x14, 0x85, 0x16, 0x5C, 0xB3, 0xD0, 0x01, 0xE2, 0x20, 0xAD])) + rom.write_bytes(0x05F6B0, bytearray([0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x09, 0xA9, 0x6F, 0x01, 0x05, 0x02, 0x5C, 0xBA, 0xBC, 0x01, 0x5C])) + rom.write_bytes(0x05F6C0, bytearray([0xBC, 0xBC, 0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x0B, 0xBF, 0xED, 0xB7, 0x01])) + rom.write_bytes(0x05F6D0, bytearray([0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xBF, 0x48, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0xC9, 0x80])) + rom.write_bytes(0x05F6E0, bytearray([0x00, 0xF0, 0x04, 0x5C, 0x79, 0xD0, 0x01, 0xBF, 0x66, 0xD3, 0x22, 0xDA, 0xAA, 0xAD, 0xD3, 0x00])) + rom.write_bytes(0x05F6F0, bytearray([0x29, 0xFF, 0x00, 0xC9, 0x01, 0x00, 0xF0, 0x21, 0xC9, 0x02, 0x00, 0xF0, 0x38, 0xBD, 0x40, 0x14])) + rom.write_bytes(0x05F700, bytearray([0x29, 0xFF, 0x00, 0xF0, 0x0C, 0xFA, 0xBF, 0x87, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0])) + rom.write_bytes(0x05F710, bytearray([0x01, 0xFA, 0xA9, 0x4E, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xBD, 0x4A, 0x14, 0x29, 0xFF, 0x00, 0xF0])) + rom.write_bytes(0x05F720, bytearray([0x0C, 0xFA, 0xBF, 0xA5, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xFA, 0xA9, 0x4E])) + rom.write_bytes(0x05F730, bytearray([0x00, 0x5C, 0x79, 0xD0, 0x01, 0xE0, 0x09, 0xD0, 0x05, 0xAD, 0x64, 0x14, 0x80, 0x03, 0xBD, 0x54])) + rom.write_bytes(0x05F740, bytearray([0x14, 0x29, 0xFF, 0x00, 0xF0, 0x0C, 0xFA, 0xBF, 0xC3, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0x5C, 0x79])) + rom.write_bytes(0x05F750, bytearray([0xD0, 0x01, 0xFA, 0xA9, 0x4E, 0x00, 0x5C, 0x79, 0xD0, 0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2])) + rom.write_bytes(0x05F760, bytearray([0x20, 0xD0, 0x08, 0xBF, 0x5F, 0xB8, 0x01, 0x5C, 0x7E, 0xD0, 0x01, 0xAD, 0xD3, 0x00, 0x29, 0xFF])) + rom.write_bytes(0x05F770, bytearray([0x00, 0xC9, 0x01, 0x00, 0xF0, 0x3C, 0xC9, 0x02, 0x00, 0xF0, 0x4B, 0xBF, 0x5F, 0xB8, 0x01, 0x05])) + rom.write_bytes(0x05F780, bytearray([0x18, 0x99, 0xA1, 0xB1, 0xBF, 0xDD, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xE1, 0xB1, 0xFA, 0xC8, 0xC8])) + rom.write_bytes(0x05F790, bytearray([0xE8, 0xE0, 0x1D, 0x90, 0x19, 0xEE, 0xD3, 0x00, 0xA0, 0x00, 0xA2, 0x00, 0xAD, 0xD3, 0x00, 0x29])) + rom.write_bytes(0x05F7A0, bytearray([0xFF, 0x00, 0xC9, 0x03, 0x00, 0xD0, 0x07, 0x9C, 0xD3, 0x00, 0x5C, 0x94, 0xD0, 0x01, 0x5C, 0x71])) + rom.write_bytes(0x05F7B0, bytearray([0xD0, 0x01, 0xBF, 0x5F, 0xB8, 0x01, 0x05, 0x18, 0x99, 0x21, 0xB2, 0xBF, 0xDD, 0xB8, 0x01, 0x05])) + rom.write_bytes(0x05F7C0, bytearray([0x18, 0x99, 0x61, 0xB2, 0x80, 0xC7, 0xBF, 0x5F, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xA1, 0xB2, 0xBF])) + rom.write_bytes(0x05F7D0, bytearray([0xDD, 0xB8, 0x01, 0x05, 0x18, 0x99, 0xE1, 0xB2, 0x80, 0xB3, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2])) + rom.write_bytes(0x05F7E0, bytearray([0x20, 0xD0, 0x0A, 0x64, 0x18, 0xAF, 0xB8, 0x03, 0x00, 0x5C, 0x80, 0xD1, 0x01, 0x5C, 0x02, 0xD2])) + rom.write_bytes(0x05F7F0, bytearray([0x01, 0xE2, 0x20, 0xAD, 0xD2, 0x00, 0xC2, 0x20, 0xD0, 0x08, 0x64, 0x18, 0xA0, 0x00, 0x5C, 0xE2])) + rom.write_bytes(0x05F800, bytearray([0xD0, 0x01, 0x5C, 0x02, 0xD2, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xD0, 0x08, 0xBF, 0x35])) + rom.write_bytes(0x05F810, bytearray([0xB8, 0x01, 0x5C, 0xB8, 0xCF, 0x01, 0xBF, 0xE1, 0xD3, 0x22, 0x29, 0xFF, 0x00, 0xC9, 0x80, 0x00])) + rom.write_bytes(0x05F820, bytearray([0xF0, 0x2E, 0xC9, 0x81, 0x00, 0xF0, 0x47, 0x5C, 0xB8, 0xCF, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF])) + rom.write_bytes(0x05F830, bytearray([0x00, 0xD0, 0x08, 0xBF, 0x4A, 0xB8, 0x01, 0x5C, 0xD4, 0xCF, 0x01, 0xBF, 0xF6, 0xD3, 0x22, 0x29])) + rom.write_bytes(0x05F840, bytearray([0xFF, 0x00, 0x4C, 0xB6, 0xFD, 0xF0, 0x18, 0xC9, 0x81, 0x00, 0xF0, 0x30, 0x5C, 0xD4, 0xCF, 0x01])) + rom.write_bytes(0x05F850, bytearray([0xDA, 0xE2, 0x20, 0xAD, 0xB3, 0x14, 0xAA, 0xC2, 0x20, 0x20, 0x8A, 0xF8, 0xFA, 0x80, 0xC8, 0xDA])) + rom.write_bytes(0x05F860, bytearray([0xE2, 0x20, 0xAD, 0xB3, 0x14, 0xAA, 0xC2, 0x20, 0x20, 0xBD, 0xF8, 0xFA, 0x80, 0xDE, 0xDA, 0xE2])) + rom.write_bytes(0x05F870, bytearray([0x20, 0xAF, 0x85, 0xFC, 0x0D, 0xAA, 0x20, 0x8A, 0xF8, 0xFA, 0x80, 0xAB, 0xDA, 0xE2, 0x20, 0xAF])) + rom.write_bytes(0x05F880, bytearray([0x86, 0xFC, 0x0D, 0xAA, 0x20, 0xBD, 0xF8, 0xFA, 0x80, 0xC2, 0xE2, 0x20, 0xC9, 0x0A, 0xB0, 0x1F])) + rom.write_bytes(0x05F890, bytearray([0xAD, 0xD5, 0x00, 0xD0, 0x0D, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xA9, 0x50, 0x00, 0xEE, 0xD5])) + rom.write_bytes(0x05F8A0, bytearray([0x00, 0x60, 0xBF, 0x0B, 0xD4, 0x22, 0x9C, 0xD4, 0x00, 0x9C, 0xD5, 0x00, 0xC2, 0x20, 0x60, 0xAD])) + rom.write_bytes(0x05F8B0, bytearray([0xD4, 0x00, 0xD0, 0xEE, 0xEE, 0xD4, 0x00, 0xC2, 0x20, 0xA9, 0x52, 0x00, 0x60, 0xE2, 0x20, 0xC9])) + rom.write_bytes(0x05F8C0, bytearray([0x0A, 0xB0, 0x1F, 0xAD, 0xD6, 0x00, 0xD0, 0x0D, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xA9, 0x50])) + rom.write_bytes(0x05F8D0, bytearray([0x00, 0xEE, 0xD6, 0x00, 0x60, 0xBF, 0x0B, 0xD4, 0x22, 0x9C, 0xD7, 0x00, 0x9C, 0xD6, 0x00, 0xC2])) + rom.write_bytes(0x05F8E0, bytearray([0x20, 0x60, 0xAD, 0xD7, 0x00, 0xD0, 0xEE, 0xEE, 0xD7, 0x00, 0xC2, 0x20, 0xA9, 0x52, 0x00, 0x60])) + rom.write_bytes(0x05F8F0, bytearray([0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xF0, 0x04, 0x5C, 0x74, 0xD2, 0x01, 0x64, 0x18, 0xA0, 0x00])) + rom.write_bytes(0x05F900, bytearray([0x5C, 0x07, 0xD2, 0x01, 0xAD, 0xD2, 0x00, 0x29, 0xFF, 0x00, 0xF0, 0x04, 0x5C, 0x74, 0xD2, 0x01])) + rom.write_bytes(0x05F910, bytearray([0xAF, 0x7C, 0x02, 0x00, 0x5C, 0x7B, 0xD2, 0x01, 0xA5, 0x38, 0x89, 0x20, 0xD0, 0x0A, 0x29, 0x10])) + rom.write_bytes(0x05F920, bytearray([0xF0, 0x02, 0xA9, 0x01, 0x5C, 0x08, 0xC1, 0x01, 0xEE, 0xD2, 0x00, 0x64, 0x38, 0x5C, 0x08, 0xC1])) + rom.write_bytes(0x05F930, bytearray([0x01, 0xAD, 0xD2, 0x00, 0xD0, 0x08, 0xA5, 0x38, 0x29, 0x20, 0x5C, 0x3B, 0xC1, 0x01, 0xA9, 0x00])) + rom.write_bytes(0x05F940, bytearray([0x80, 0xF8, 0xAD, 0x10, 0x0B, 0x49, 0x01, 0x9C, 0xD2, 0x00, 0x5C, 0x4D, 0xCE, 0x01, 0x9C, 0x01])) + rom.write_bytes(0x05F950, bytearray([0x02, 0xAD, 0x5E, 0x02, 0xF0, 0x16, 0xAD, 0xB0, 0x14, 0x89, 0x08, 0xD0, 0x15, 0xAD, 0xB3, 0x14])) + rom.write_bytes(0x05F960, bytearray([0xCF, 0x85, 0xFC, 0x0D, 0x90, 0x06, 0xA9, 0x80, 0x8F, 0x65, 0x02, 0x7E, 0xC2, 0x20, 0x5C, 0xBB])) + rom.write_bytes(0x05F970, bytearray([0xA5, 0x17, 0xA9, 0x01, 0x80, 0xF2, 0xE2, 0x20, 0xAF, 0x87, 0xFC, 0x0D, 0xC2, 0x20, 0xF0, 0x0D])) + rom.write_bytes(0x05F980, bytearray([0x4C, 0xBF, 0xFA, 0x8D, 0x1C, 0x0C, 0x8D, 0x1E, 0x0C, 0x5C, 0x4E, 0xD8, 0x03, 0xB9, 0x04, 0x0C])) + rom.write_bytes(0x05F990, bytearray([0x80, 0xF1, 0xE2, 0x20, 0xA9, 0x01, 0x8D, 0xD8, 0x00, 0xC2, 0x20, 0x22, 0xBE, 0xAE, 0x03, 0x5C])) + rom.write_bytes(0x05F9A0, bytearray([0xA2, 0xA0, 0x02, 0xE2, 0x20, 0xAD, 0xD8, 0x00, 0xD0, 0x0F, 0xC2, 0x20, 0xA9, 0x02, 0x00, 0x9D])) + rom.write_bytes(0x05F9B0, bytearray([0x96, 0x7A, 0xFE, 0x78, 0x79, 0x5C, 0xAF, 0xA3, 0x02, 0xAD, 0xB3, 0x14, 0xCF, 0x86, 0xFC, 0x0D])) + rom.write_bytes(0x05F9C0, bytearray([0xC2, 0x20, 0xB0, 0xE8, 0xC2, 0x20, 0x5C, 0x81, 0xA3, 0x02, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02])) + rom.write_bytes(0x05F9D0, bytearray([0xBD, 0x6D, 0x14, 0x89, 0x20, 0xF0, 0x0D, 0xFA, 0xC2, 0x20, 0xAD, 0x02, 0x74, 0xC9, 0x32, 0x00])) + rom.write_bytes(0x05F9E0, bytearray([0x5C, 0x80, 0xDF, 0x02, 0x18, 0x69, 0x20, 0x9D, 0x6D, 0x14, 0xAD, 0xB3, 0x14, 0x1A, 0x8D, 0xB3])) + rom.write_bytes(0x05F9F0, bytearray([0x14, 0x80, 0xE4, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D, 0xD1, 0x00, 0xA9])) + rom.write_bytes(0x05FA00, bytearray([0x10, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0xC2, 0x20, 0xA9, 0x36, 0x00])) + rom.write_bytes(0x05FA10, bytearray([0x22, 0xD2, 0x85, 0x00, 0x5C, 0xEB, 0xC9, 0x11, 0xB9, 0xE4, 0xB9, 0xC0, 0x00, 0xF0, 0x03, 0xEE])) + rom.write_bytes(0x05FA20, bytearray([0xD9, 0x00, 0x5C, 0xBB, 0xB9, 0x10, 0xA9, 0x06, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x19, 0xB0])) + rom.write_bytes(0x05FA30, bytearray([0x10, 0xA9, 0x05, 0x00, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x9A, 0xD0, 0x10, 0xA9, 0x06, 0x85])) + rom.write_bytes(0x05FA40, bytearray([0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0xC9, 0xD2, 0x10, 0xA9, 0x05, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C])) + rom.write_bytes(0x05FA50, bytearray([0xEE, 0xC5, 0x10, 0xA9, 0x05, 0x00, 0x85, 0x4D, 0xEE, 0xD9, 0x00, 0x5C, 0x0F, 0xBE, 0x10, 0xDA])) + rom.write_bytes(0x05FA60, bytearray([0xE2, 0x20, 0xAD, 0xD9, 0x00, 0xF0, 0x26, 0xA2, 0x00, 0xAD, 0x1A, 0x02, 0xDF, 0x18, 0xD4, 0x22])) + rom.write_bytes(0x05FA70, bytearray([0xF0, 0x07, 0xE8, 0xE0, 0x06, 0xF0, 0x16, 0x80, 0xF3, 0xAE, 0x1A, 0x02, 0xBD, 0x6D, 0x14, 0x8D])) + rom.write_bytes(0x05FA80, bytearray([0xD1, 0x00, 0xA9, 0x10, 0x0C, 0xD1, 0x00, 0xAD, 0xD1, 0x00, 0x9D, 0x6D, 0x14, 0xFA, 0x22, 0x67])) + rom.write_bytes(0x05FA90, bytearray([0xFA, 0x04, 0x5C, 0x5A, 0xA1, 0x10])) + + rom.write_bytes(0x05FAB2, bytearray([0xA9, 0x00, 0xEB, 0xAD, 0x0E, 0x03, 0xC2, 0x20, 0xC2, 0x10, 0x4C, 0x37, 0xF6])) + rom.write_bytes(0x05FABF, bytearray([0xE2])) + rom.write_bytes(0x05FAC0, bytearray([0x20, 0xAD, 0x1A, 0x02, 0xDA, 0xA2, 0x00, 0x00, 0xDF, 0x1E, 0xD4, 0x22, 0xF0, 0x11, 0xE8, 0xE0])) + rom.write_bytes(0x05FAD0, bytearray([0x01, 0x00, 0xF0, 0x02, 0x80, 0xF2, 0xFA, 0xC2, 0x20, 0xA9, 0x00, 0x00, 0x4C, 0x83, 0xF9, 0xFA])) + rom.write_bytes(0x05FAE0, bytearray([0xC2, 0x20, 0x4C, 0x8D, 0xF9])) + rom.write_bytes(0x05FAE5, bytearray([0x48, 0xE2, 0x20, 0xAD, 0x5D, 0x14, 0xC9, 0x01, 0xF0, 0x07])) + rom.write_bytes(0x05FAEF, bytearray([0xC2, 0x20, 0x68, 0x5C, 0xCE, 0xBE, 0x03, 0xAD, 0xCC, 0x00, 0xD0, 0xF4, 0xAF, 0xFA, 0x1D, 0x70])) + rom.write_bytes(0x05FAFF, bytearray([0xF0, 0xEE, 0xA9, 0x00, 0x8F, 0xFA, 0x1D, 0x70, 0x80, 0xE6])) + rom.write_bytes(0x05FB09, bytearray([0x48, 0xE2, 0x20, 0xAD, 0xCC, 0x00, 0xF0, 0x06, 0xCE, 0xCC, 0x00, 0xCE, 0xCC, 0x00, 0xC2, 0x20, 0x68, 0x22, 0x87, 0xBF, 0x03, 0x5C, 0x89, 0xFE, 0x07])) + rom.write_bytes(0x05FB22, bytearray([0xA0, 0x0A, 0x8C, 0x4D, 0x00, 0xE2, 0x20, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0x8D, 0xB6, 0x14, 0xC2, 0x20, 0x5C, 0xB9, 0xF1, 0x0D, 0x0D, 0xA8, 0xE2])) + rom.write_bytes(0x05FB3A, bytearray([0x20, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0xA9, 0x00, 0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0x1A, 0xEE, 0x14, 0xC2, 0x20, 0x68, 0x5C, 0xB9, 0xF1, 0x0D])) + rom.write_bytes(0x05FB58, bytearray([0x4C, 0xDD, 0xFB, 0x04, 0xAF, 0xAC, 0x00, 0x70])) + rom.write_bytes(0x05FB60, bytearray([0xD0, 0x2C, 0xAD, 0x35, 0x00, 0xC9, 0x50, 0xD0, 0x25, 0xAD, 0xDA, 0x00, 0xC9, 0x80, 0xF0, 0x11])) + rom.write_bytes(0x05FB70, bytearray([0xC9, 0x00, 0xF0, 0x21, 0xC9, 0x2A, 0xF0, 0x1D, 0xC9, 0x54, 0xF0, 0x19, 0xEE, 0xDA, 0x00, 0x80])) + rom.write_bytes(0x05FB80, bytearray([0x10, 0xA9, 0x2F, 0x8D, 0x53, 0x00, 0xA9, 0x11, 0x8D, 0x18, 0x01, 0xEE, 0xDB, 0x00, 0x9C, 0xDA])) + rom.write_bytes(0x05FB90, bytearray([0x00, 0x5C, 0x93, 0xC1, 0x01, 0xA9, 0x28, 0x8D, 0x53, 0x00, 0x80, 0xE0])) + rom.write_bytes(0x05FB9C, bytearray([0xA9, 0x93, 0x00, 0xEE])) + rom.write_bytes(0x05FBA0, bytearray([0xB4, 0x03, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x00, 0x90, 0x14, 0xE2, 0x20, 0xDA, 0xAE, 0x1A, 0x02])) + rom.write_bytes(0x05FBB0, bytearray([0xBD, 0x6D, 0x14, 0x09, 0x01, 0x9D, 0x6D, 0x14, 0xFA, 0xC2, 0x20, 0xA9, 0x94, 0x00, 0x5C, 0xF1, 0xDF, 0x00])) + rom.write_bytes(0x05FBC2, bytearray([0x48, 0xC9, 0x06, 0x00, 0xB0, 0x10, 0xE2, 0x20, 0xAD, 0x54, 0x14, 0xC9, 0x00, 0xC2, 0x20, 0xF0])) + rom.write_bytes(0x05FBD2, bytearray([0x05, 0x68, 0x5C, 0xAC, 0x82, 0x07, 0x68, 0x5C, 0xFB, 0x81, 0x07, 0xAD, 0x6A, 0x02, 0xF0, 0x11])) + rom.write_bytes(0x05FBE2, bytearray([0xC2, 0x20, 0xA9, 0x0E, 0x00, 0x22, 0xE2, 0xF6, 0x04, 0xA9, 0x00, 0x00, 0x8D, 0x6A, 0x02, 0xE2])) + rom.write_bytes(0x05FBF2, bytearray([0x20, 0x22, 0x28, 0xFD, 0x04, 0x4C, 0x5C, 0xFB, 0xAF, 0xB0, 0x23, 0x7E, 0xF0, 0x18, 0xAF, 0xAC])) + rom.write_bytes(0x05FC02, bytearray([0x00, 0x70, 0x29, 0xFF, 0x00, 0xD0, 0x0F, 0xAF, 0xB0, 0x23, 0x7E, 0x8F, 0xEC, 0x61, 0x04, 0xA9])) + rom.write_bytes(0x05FC12, bytearray([0x00, 0x00, 0x8F, 0xB0, 0x23, 0x7E, 0xBD, 0xD0, 0x61, 0xF0, 0x03, 0xDE, 0xD0, 0x61, 0x5C, 0x79])) + rom.write_bytes(0x05FC22, bytearray([0xDE, 0x04, 0x48, 0xC2, 0x20, 0xAF, 0xEC, 0x61, 0x04, 0xD0, 0x0B, 0xE2, 0x20, 0x68, 0x22, 0xCE])) + rom.write_bytes(0x05FC32, bytearray([0xC0, 0x01, 0x5C, 0x8B, 0xB5, 0x01, 0x8F, 0xB0, 0x23, 0x7E, 0xA9, 0x00, 0x00, 0x8F, 0xEC, 0x61])) + rom.write_bytes(0x05FC42, bytearray([0x04, 0x80, 0xE8, 0x48, 0xDA, 0xE2, 0x20, 0x4C, 0xA5, 0xFC, 0xA2, 0x00, 0xDF, 0x1F, 0xD4, 0x22])) + rom.write_bytes(0x05FC52, bytearray([0xF0, 0x03, 0xE8, 0x80, 0xF7, 0xBF, 0x8D, 0xFC, 0x0D, 0xAA, 0xBF, 0x2A, 0xD4, 0x22, 0x8D, 0xDC])) + rom.write_bytes(0x05FC62, bytearray([0x00, 0xC2, 0x20, 0xFA, 0x68, 0x0D, 0xDC, 0x00, 0x95, 0x76, 0x5C, 0x29, 0xDB, 0x0C, 0xE2, 0x20])) + rom.write_bytes(0x05FC72, bytearray([0xAD, 0x48, 0x0B, 0xF0, 0x23, 0xAF, 0xBE, 0x03, 0x02, 0xC9, 0x02, 0xD0, 0x1B, 0xAD, 0x1A, 0x02])) + rom.write_bytes(0x05FC82, bytearray([0xA2, 0x00, 0xDF, 0x1F, 0xD4, 0x22, 0xF0, 0x03, 0xE8, 0x80, 0xF7, 0x8A, 0x0A, 0xAA, 0xC2, 0x20])) + rom.write_bytes(0x05FC92, bytearray([0xBF, 0x35, 0xD4, 0x22, 0x8F, 0xBE, 0x03, 0x02, 0xC2, 0x20, 0xEE, 0xAC, 0x03, 0xC2, 0x10, 0x5C])) + rom.write_bytes(0x05FCA2, bytearray([0x0C, 0x95, 0x02, 0xAD, 0x1A, 0x02, 0xC9, 0x43, 0xF0, 0x03, 0x4C, 0x4C, 0xFC, 0xA9, 0x0A, 0x4C])) + rom.write_bytes(0x05FCB2, bytearray([0x60, 0xFC, 0xAC, 0xB4, 0x03, 0xC0, 0x14, 0x30, 0x14, 0x1A, 0xE2, 0x20, 0xDA, 0x48, 0xAE, 0x1A])) + rom.write_bytes(0x05FCC2, bytearray([0x02, 0xBD, 0x6D, 0x14, 0x09, 0x01, 0x9D, 0x6D, 0x14, 0x68, 0xFA, 0xC2, 0x20, 0x22, 0xD2, 0x85])) + rom.write_bytes(0x05FCD2, bytearray([0x00, 0x5C, 0xA4, 0x9E, 0x03, 0xE2, 0x20, 0xAD, 0xF6, 0x7D, 0xC9, 0x0C, 0xB0, 0x1A, 0xAD, 0xCC])) + rom.write_bytes(0x05FCE2, bytearray([0x00, 0xAD, 0x5E, 0x14, 0x38, 0xED, 0xCC, 0x00, 0x3A, 0x3A, 0x8D, 0xDE, 0x00, 0xAD, 0xF6, 0x7D])) + rom.write_bytes(0x05FCF2, bytearray([0x38, 0xED, 0xCC, 0x00, 0x18, 0xCD, 0xDE, 0x00, 0xC2, 0x20, 0x5C, 0xBC, 0x9A, 0x02, 0xE2, 0x20])) + rom.write_bytes(0x05FD02, bytearray([0xAD, 0x5D, 0x14, 0xF0, 0x33, 0xAA, 0xBF, 0xA3, 0xAF, 0x09, 0x18, 0x6D, 0xCC, 0x00, 0x8D, 0x5E])) + rom.write_bytes(0x05FD12, bytearray([0x14, 0xAD, 0xF6, 0x7D, 0xC9, 0x0C, 0xB0, 0x1A, 0xAD, 0xCC, 0x00, 0xAD, 0x5E, 0x14, 0x38, 0xED])) + rom.write_bytes(0x05FD22, bytearray([0xCC, 0x00, 0x3A, 0x3A, 0x8D, 0xDE, 0x00, 0xAD, 0xF6, 0x7D, 0x38, 0xED, 0xCC, 0x00, 0x18, 0xCD])) + rom.write_bytes(0x05FD32, bytearray([0xDE, 0x00, 0xC2, 0x20, 0x5C, 0xAC, 0xDC, 0x01, 0x1A, 0x8D, 0x5D, 0x14, 0x80, 0xC0, 0xA9, 0x00])) + rom.write_bytes(0x05FD42, bytearray([0x8F, 0xE8, 0x1F, 0x70, 0xAD, 0xAC, 0x60, 0xC9, 0x00, 0xD0, 0x06, 0xA9, 0x01, 0x8F, 0xE8, 0x1F])) + rom.write_bytes(0x05FD52, bytearray([0x70, 0x4C, 0x5F, 0xF5, 0xDA, 0xAD, 0x1A, 0x02, 0x8D, 0x7C, 0x02, 0xAD, 0x12, 0x11, 0xC9, 0x08])) + rom.write_bytes(0x05FD62, bytearray([0xB0, 0x1D, 0xAD, 0x18, 0x02, 0x4A, 0xAA, 0xA9, 0x00, 0xE0, 0x00, 0xF0, 0x06, 0x18, 0x69, 0x08])) + rom.write_bytes(0x05FD72, bytearray([0xCA, 0x80, 0xF6, 0x18, 0x6D, 0x12, 0x11, 0xAA, 0xBF, 0x4B, 0xD4, 0x22, 0x8D, 0x1A, 0x02, 0xFA])) + rom.write_bytes(0x05FD82, bytearray([0xA9, 0x02, 0x8D, 0x13, 0x11, 0x5C, 0x70, 0xE0, 0x17, 0xAC, 0x7C, 0x02, 0x8C, 0x1A, 0x02, 0xB9])) + rom.write_bytes(0x05FD92, bytearray([0x22, 0x02, 0x5C, 0xA7, 0x82, 0x10, 0xAD, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0xC2, 0x20, 0xE2, 0x10])) + rom.write_bytes(0x05FDA2, bytearray([0x5C, 0xBC, 0xB2, 0x01, 0xC9, 0x45, 0xB0, 0x03, 0x8D, 0x7C, 0x02, 0x8D, 0x1A, 0x02, 0x29, 0x07])) + rom.write_bytes(0x05FDB2, bytearray([0x5C, 0x3A, 0x81, 0x10, 0xC9, 0x82, 0x00, 0xF0, 0x2E, 0xC9, 0x83, 0x00, 0xF0, 0x40, 0xC9, 0x84])) + rom.write_bytes(0x05FDC2, bytearray([0x00, 0xF0, 0x0B, 0xC9, 0x85, 0x00, 0xF0, 0x0E, 0xC9, 0x80, 0x00, 0x4C, 0x45, 0xF8, 0xE2, 0x20])) + rom.write_bytes(0x05FDD2, bytearray([0xAF, 0x99, 0xFC, 0x0D, 0x80, 0x16, 0xDA, 0xE2, 0x20, 0xAD, 0xE3, 0x00, 0xD0, 0x4E, 0x9C, 0xE1])) + rom.write_bytes(0x05FDE2, bytearray([0x00, 0xAF, 0x99, 0xFC, 0x0D, 0x80, 0x25, 0xE2, 0x20, 0xAD, 0xB5, 0x14, 0xC9, 0x64, 0xC2, 0x20])) + rom.write_bytes(0x05FDF2, bytearray([0xB0, 0x06, 0xA9, 0x4E, 0x00, 0x4C, 0x4C, 0xF8, 0xA9, 0x52, 0x00, 0x4C, 0x4C, 0xF8, 0xDA, 0xE2])) + rom.write_bytes(0x05FE02, bytearray([0x20, 0xAD, 0xE3, 0x00, 0xD0, 0x26, 0x9C, 0xE1, 0x00, 0xAD, 0xB5, 0x14, 0xC9, 0x0A, 0x90, 0x08])) + rom.write_bytes(0x05FE12, bytearray([0x38, 0xE9, 0x0A, 0xEE, 0xE1, 0x00, 0x80, 0xF4, 0x8D, 0xE2, 0x00, 0xEE, 0xE3, 0x00, 0xAD, 0xE1])) + rom.write_bytes(0x05FE22, bytearray([0x00, 0xAA, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xFA, 0x4C, 0x46, 0xF8, 0x9C, 0xE3, 0x00, 0xAD])) + rom.write_bytes(0x05FE32, bytearray([0xE2, 0x00, 0xAA, 0xBF, 0x0B, 0xD4, 0x22, 0xC2, 0x20, 0xFA, 0x4C, 0x4C, 0xF8, 0x22, 0xB7, 0xB2])) + rom.write_bytes(0x05FE42, bytearray([0x01, 0xEA, 0xEA, 0xEA, 0xAE, 0x7C, 0x02, 0x8E, 0x1A, 0x02, 0xEA, 0xEA, 0xEA, 0xEA, 0x5C, 0xAC])) + rom.write_bytes(0x05FE52, bytearray([0xBE, 0x01, 0xE2, 0x20, 0xAD, 0x1A, 0x02, 0xc9, 0x3C, 0xC2, 0x20, 0xB0, 0x04, 0xA9, 0x02, 0x00])) + rom.write_bytes(0x05FE62, bytearray([0x6B, 0xA9, 0x00, 0x00, 0x6B, 0xAD, 0x18, 0x01, 0xC9, 0x19, 0xD0, 0x3A, 0xC2, 0x20, 0x48, 0xA9])) + rom.write_bytes(0x05FE72, bytearray([0x00, 0x00, 0xE2, 0x20, 0xAF, 0x9A, 0xFC, 0x0D, 0xD0, 0x05, 0xA9, 0x08, 0x0C, 0xB0, 0x14, 0xC2])) + rom.write_bytes(0x05FE82, bytearray([0x10, 0xDA, 0x5A, 0x8B, 0xAD, 0x0E, 0x03, 0xC2, 0x20, 0xAA, 0xBF, 0x07, 0xFF, 0x06, 0xA8, 0xE2])) + rom.write_bytes(0x05FE92, bytearray([0x20, 0xA9, 0x00, 0xEB, 0xA9, 0x7F, 0xA2, 0x40, 0x14, 0x54, 0x70, 0x7E, 0xAB, 0x7A, 0xFA, 0xC2])) + rom.write_bytes(0x05FEA2, bytearray([0x20, 0x68, 0xE2, 0x20, 0xE2, 0x10, 0x22, 0x4B, 0x82, 0x00, 0x5C, 0xD9, 0x87, 0x17])) + + rom.write_bytes(0x00EDD0, bytearray([0xda, 0xa2, 0x00, 0x00, 0xe2, 0x20, 0xc9, 0x00, 0xf0, 0x0b, 0x48, 0x8a, 0x18, 0x69, 0x0c, 0xaa])) + rom.write_bytes(0x00EDE0, bytearray([0x68, 0x3a, 0x3a, 0x80, 0xf1, 0x98, 0x4a, 0x8f, 0x80, 0x24, 0x7e, 0x18, 0x8a, 0x6f, 0x80, 0x24])) + rom.write_bytes(0x00EDF0, bytearray([0x7e, 0xaa, 0xbf, 0x00, 0x80, 0x02, 0x0a, 0xaa, 0xc2, 0x20, 0xbf, 0x8e, 0xd4, 0x22, 0xfa, 0x18])) + rom.write_bytes(0x00EE00, bytearray([0x5c, 0xb6, 0xc0, 0x17, 0xda, 0xe2, 0x20, 0xa2, 0x00, 0x00, 0xdf, 0x4b, 0xd4, 0x22, 0xf0, 0x0e])) + rom.write_bytes(0x00EE10, bytearray([0xe8, 0xe0, 0x30, 0x00, 0xb0, 0x02, 0x80, 0xf2, 0xa9, 0x00, 0xeb, 0xad, 0x1a, 0x02, 0xaa, 0xbf])) + rom.write_bytes(0x00EE20, bytearray([0x00, 0x80, 0x02, 0xaa, 0xbf, 0x9e, 0xd4, 0x22, 0xc2, 0x20, 0x29, 0xff, 0x00, 0xfa, 0x5c, 0xfd])) + rom.write_bytes(0x00EE30, bytearray([0xc6, 0x17])) + + +def ExtendedItemHandler(rom: LocalRom) -> None: + rom.write_bytes(0x10B3FB, bytearray([0xE2, 0x20, 0xC9, 0x45, 0xB0])) + rom.write_bytes(0x10B400, bytearray([0x0C, 0xC2, 0x20, 0xA9, 0x10, 0x03, 0x8D, 0x7E, 0x02, 0x5C, 0xCF, 0xF4, 0x0B, 0xAD, 0x0F, 0x0B])) + rom.write_bytes(0x10B410, bytearray([0xD0, 0x38, 0xEE, 0xB5, 0x14, 0xA9, 0x18, 0x8D, 0x53, 0x00, 0xAF, 0x9A, 0xFC, 0x0D, 0xF0, 0x09])) + rom.write_bytes(0x10B420, bytearray([0xAD, 0xB5, 0x14, 0xCF, 0x99, 0xFC, 0x0D, 0xB0, 0x04, 0x5C, 0x0A, 0xF5, 0x0B, 0xAD, 0xB6, 0x14])) + rom.write_bytes(0x10B430, bytearray([0xD0, 0xF7, 0xA9, 0x01, 0x8D, 0xB6, 0x14, 0xA9, 0x0A, 0x8D, 0x18, 0x02, 0xA9, 0x16, 0x8D, 0x18])) + rom.write_bytes(0x10B440, bytearray([0x01, 0xA9, 0x97, 0x8D, 0x53, 0x00, 0x5C, 0x0A, 0xF5, 0x0B, 0xFA, 0x5C, 0x10, 0xF5, 0x0B])) + + +def patch_rom(world: "YoshisIslandWorld", rom: LocalRom, player: int) -> None: + handle_items(rom) # Implement main item functionality + Item_Data(rom) # Pointers necessary for item functionality + write_lives(rom) # Writes the number of lives as set in AP + CodeHandler(rom) # Jumps to my code + Server_Data(rom) # Pointers mostly related to receiving items + Menu_Data(rom) # Data related to the AP menu + Handle_Locations(rom) + ExtendedItemHandler(rom) + rom.write_bytes(0x11544B, bytearray(world.global_level_list)) + rom.write_bytes(0x11547A, bytearray([0x43])) + + rom.write_bytes(0x06FC89, world.starting_lives) + rom.write_bytes(0x03464F, ([world.baby_mario_sfx])) + rom.write_bytes(0x06FC83, ([world.options.starting_world.value])) + rom.write_bytes(0x06FC84, ([world.options.hidden_object_visibility.value])) + rom.write_bytes(0x06FC88, ([world.options.shuffle_midrings.value])) + rom.write_bytes(0x06FC85, ([world.options.castle_open_condition.value])) + rom.write_bytes(0x06FC86, ([world.options.castle_clear_condition.value])) + rom.write_bytes(0x06FC87, ([world.options.disable_autoscroll.value])) + rom.write_bytes(0x06FC8B, ([world.options.minigame_checks.value])) + rom.write_byte(0x06FC8C, world.options.death_link.value) + rom.write_bytes(0x06FC8D, bytearray(world.boss_room_id)) + rom.write_bytes(0x06FC99, bytearray([world.options.luigi_pieces_required.value])) + rom.write_bytes(0x06FC9A, bytearray([world.options.goal.value])) + + if world.options.yoshi_colors != YoshiColors.option_normal: + rom.write_bytes(0x113A33, bytearray(world.bowser_text)) + + rom.write_bytes(0x0A060C, bytearray(world.boss_burt_data)) + rom.write_bytes(0x0A8666, bytearray(world.boss_slime_data)) + rom.write_bytes(0x0A9D90, bytearray(world.boss_boo_data)) + rom.write_bytes(0x0074EA, bytearray(world.boss_pot_data)) + rom.write_bytes(0x08DC0A, bytearray(world.boss_frog_data)) + rom.write_bytes(0x0A4440, bytearray(world.boss_plant_data)) + rom.write_bytes(0x0968A2, bytearray(world.boss_milde_data)) + rom.write_bytes(0x0B3E10, bytearray(world.boss_koop_data)) + rom.write_bytes(0x0B4BD0, bytearray(world.boss_slug_data)) + rom.write_bytes(0x0B6BBA, bytearray(world.boss_raph_data)) + rom.write_bytes(0x087BED, bytearray(world.boss_tap_data)) + + rom.write_bytes(0x07A00D, ([world.tap_tap_room])) + rom.write_bytes(0x079DF2, ([world.tap_tap_room])) + rom.write_bytes(0x079CCF, ([world.tap_tap_room])) + rom.write_bytes(0x079C4D, ([world.tap_tap_room])) + + rom.write_bytes(0x045A2E, bytearray(world.Stage11StageGFX)) + rom.write_bytes(0x045A31, bytearray(world.Stage12StageGFX)) + rom.write_bytes(0x045A34, bytearray(world.Stage13StageGFX)) + rom.write_bytes(0x045A37, bytearray(world.Stage14StageGFX)) + rom.write_bytes(0x045A3A, bytearray(world.Stage15StageGFX)) + rom.write_bytes(0x045A3D, bytearray(world.Stage16StageGFX)) + rom.write_bytes(0x045A40, bytearray(world.Stage17StageGFX)) + rom.write_bytes(0x045A43, bytearray(world.Stage18StageGFX)) + + rom.write_bytes(0x045A52, bytearray(world.Stage21StageGFX)) + rom.write_bytes(0x045A55, bytearray(world.Stage22StageGFX)) + rom.write_bytes(0x045A58, bytearray(world.Stage23StageGFX)) + rom.write_bytes(0x045A5B, bytearray(world.Stage24StageGFX)) + rom.write_bytes(0x045A5E, bytearray(world.Stage25StageGFX)) + rom.write_bytes(0x045A61, bytearray(world.Stage26StageGFX)) + rom.write_bytes(0x045A64, bytearray(world.Stage27StageGFX)) + rom.write_bytes(0x045A67, bytearray(world.Stage28StageGFX)) + + rom.write_bytes(0x045A76, bytearray(world.Stage31StageGFX)) + rom.write_bytes(0x045A79, bytearray(world.Stage32StageGFX)) + rom.write_bytes(0x045A7C, bytearray(world.Stage33StageGFX)) + rom.write_bytes(0x045A7F, bytearray(world.Stage34StageGFX)) + rom.write_bytes(0x045A82, bytearray(world.Stage35StageGFX)) + rom.write_bytes(0x045A85, bytearray(world.Stage36StageGFX)) + rom.write_bytes(0x045A88, bytearray(world.Stage37StageGFX)) + rom.write_bytes(0x045A8B, bytearray(world.Stage38StageGFX)) + + rom.write_bytes(0x045A9A, bytearray(world.Stage41StageGFX)) + rom.write_bytes(0x045A9D, bytearray(world.Stage42StageGFX)) + rom.write_bytes(0x045AA0, bytearray(world.Stage43StageGFX)) + rom.write_bytes(0x045AA3, bytearray(world.Stage44StageGFX)) + rom.write_bytes(0x045AA6, bytearray(world.Stage45StageGFX)) + rom.write_bytes(0x045AA9, bytearray(world.Stage46StageGFX)) + rom.write_bytes(0x045AAC, bytearray(world.Stage47StageGFX)) + rom.write_bytes(0x045AAF, bytearray(world.Stage48StageGFX)) + + rom.write_bytes(0x045ABE, bytearray(world.Stage51StageGFX)) + rom.write_bytes(0x045AC1, bytearray(world.Stage52StageGFX)) + rom.write_bytes(0x045AC4, bytearray(world.Stage53StageGFX)) + rom.write_bytes(0x045AC7, bytearray(world.Stage54StageGFX)) + rom.write_bytes(0x045ACA, bytearray(world.Stage55StageGFX)) + rom.write_bytes(0x045ACD, bytearray(world.Stage56StageGFX)) + rom.write_bytes(0x045AD0, bytearray(world.Stage57StageGFX)) + rom.write_bytes(0x045AD3, bytearray(world.Stage58StageGFX)) + + rom.write_bytes(0x045AE2, bytearray(world.Stage61StageGFX)) + rom.write_bytes(0x045AE5, bytearray(world.Stage62StageGFX)) + rom.write_bytes(0x045AE8, bytearray(world.Stage63StageGFX)) + rom.write_bytes(0x045AEB, bytearray(world.Stage64StageGFX)) + rom.write_bytes(0x045AEE, bytearray(world.Stage65StageGFX)) + rom.write_bytes(0x045AF1, bytearray(world.Stage66StageGFX)) + rom.write_bytes(0x045AF4, bytearray(world.Stage67StageGFX)) + + rom.write_bytes(0x0BDBAF, bytearray(world.level_gfx_table)) + rom.write_bytes(0x0BDC4F, bytearray(world.palette_panel_list)) + + if world.options.yoshi_colors == YoshiColors.option_random_order: + rom.write_bytes(0x010000, ([world.leader_color])) + rom.write_bytes(0x010008, ([world.leader_color])) + rom.write_bytes(0x010009, ([world.leader_color])) + rom.write_bytes(0x010001, bytearray(world.color_order)) + rom.write_bytes(0x01000C, ([world.leader_color])) + rom.write_bytes(0x010014, ([world.leader_color])) + rom.write_bytes(0x010015, ([world.leader_color])) + rom.write_bytes(0x01000D, bytearray(world.color_order)) + rom.write_bytes(0x010018, ([world.leader_color])) + rom.write_bytes(0x010020, ([world.leader_color])) + rom.write_bytes(0x010021, ([world.leader_color])) + rom.write_bytes(0x01001A, bytearray(world.color_order)) + rom.write_bytes(0x010024, ([world.leader_color])) + rom.write_bytes(0x01002C, ([world.leader_color])) + rom.write_bytes(0x01002D, ([world.leader_color])) + rom.write_bytes(0x010025, bytearray(world.color_order)) + rom.write_bytes(0x010030, ([world.leader_color])) + rom.write_bytes(0x010038, ([world.leader_color])) + rom.write_bytes(0x010039, ([world.leader_color])) + rom.write_bytes(0x010031, bytearray(world.color_order)) + rom.write_bytes(0x01003C, ([world.leader_color])) + rom.write_bytes(0x010044, ([world.leader_color])) + rom.write_bytes(0x010045, ([world.leader_color])) + rom.write_bytes(0x01003D, bytearray(world.color_order)) + rom.write_bytes(0x010043, ([world.leader_color])) + elif world.options.yoshi_colors in {YoshiColors.option_random_color, YoshiColors.option_singularity}: + rom.write_bytes(0x010000, bytearray(world.level_colors)) + + if world.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: + bonus_checks(rom) + + if world.options.minigame_checks in {MinigameChecks.option_bandit_games, MinigameChecks.option_both}: + bandit_checks(rom) + + rom.write_bytes(0x00BF2C, bytearray(world.world_bonus)) + + if world.options.softlock_prevention: + rom.write_bytes(0x00C18F, bytearray([0x5C, 0x58, 0xFB, 0x0B])) # R + X Code + + if world.options.bowser_door_mode != BowserDoor.option_manual: + rom.write_bytes(0x07891F, bytearray(world.castle_door)) # 1 Entry + rom.write_bytes(0x078923, bytearray(world.castle_door)) # 2 Entry + rom.write_bytes(0x078927, bytearray(world.castle_door)) # 3 Entry + rom.write_bytes(0x07892B, bytearray(world.castle_door)) # 4 Entry + + if world.options.bowser_door_mode == BowserDoor.option_gauntlet: + rom.write_bytes(0x0AF517, bytearray([0xC6, 0x07, 0x7A, 0x00])) # Door 2 + rom.write_bytes(0x0AF6B7, bytearray([0xCD, 0x05, 0x5B, 0x00])) # Door 3 + rom.write_bytes(0x0AF8F2, bytearray([0xD3, 0x00, 0x77, 0x06])) # Door 4 + + if world.options.goal == PlayerGoal.option_luigi_hunt: + rom.write_bytes(0x1153F6, bytearray([0x16, 0x28, 0x10, 0x0C, 0x10, 0x4E, 0x1E, 0x10, 0x08, 0x04, 0x08, 0x24, 0x36, 0x82, 0x83, 0x83, 0x34, 0x84, 0x85, 0x85])) # Luigi piece clear text + rom.write_bytes(0x06FC86, bytearray([0xFF])) # Boss clear goal = 255, renders bowser inaccessible + + from Main import __version__ + rom.name = bytearray(f'YOSHIAP{__version__.replace(".", "")[0:3]}_{player}_{world.multiworld.seed:11}\0', "utf8")[:21] + rom.name.extend([0] * (21 - len(rom.name))) + rom.write_bytes(0x007FC0, rom.name) + + +class YoshisIslandDeltaPatch(APDeltaPatch): + hash = USHASH + game: str = "Yoshi's Island" + patch_file_ending = ".apyi" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basemd5 = hashlib.md5() + basemd5.update(base_rom_bytes) + if USHASH != basemd5.hexdigest(): + raise Exception("Supplied Base Rom does not match known MD5 for US(1.0) release. " + "Get the correct game and version, then dump it") + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + if not file_name: + file_name = get_settings()["yoshisisland_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.user_path(file_name) + return file_name diff --git a/worlds/yoshisisland/Rules.py b/worlds/yoshisisland/Rules.py new file mode 100644 index 000000000000..09f6eaced07c --- /dev/null +++ b/worlds/yoshisisland/Rules.py @@ -0,0 +1,612 @@ +from .level_logic import YoshiLogic +from worlds.generic.Rules import set_rule +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import YoshisIslandWorld + + +def set_easy_rules(world: "YoshisIslandWorld") -> None: + logic = YoshiLogic(world) + player = world.player + + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Stars", player), lambda state: state.has_all({"Tulip", "Beanstalk", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Level Clear", player), lambda state: state.has("Beanstalk", player)) + + set_rule(world.multiworld.get_location("Watch Out Below!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Watch Out Below!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Watch Out Below!: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Watch Out Below!: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Cave Of Chomp Rock: Red Coins", player), lambda state: state.has("Chomp Rock", player)) + set_rule(world.multiworld.get_location("The Cave Of Chomp Rock: Flowers", player), lambda state: state.has("Chomp Rock", player)) + + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player)) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player) and (logic.has_midring(state) or state.has("Key", player))) + + set_rule(world.multiworld.get_location("Hop! Hop! Donut Lifts: Stars", player), lambda state: logic.has_midring(state) or logic.cansee_clouds(state)) + + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player) or logic.has_midring(state) and state.has("Beanstalk", player)) and state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Beanstalk"}, player)) + + set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Stars", player), lambda state: logic.has_midring(state) or (logic.cansee_clouds and state.has_all({"Spring Ball", "Chomp Rock", "Beanstalk"}, player))) + + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player)) + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player)) + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has("Platform Ghost", player) or state.has_all({"Arrow Wheel", "Key"}, player))) + + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has_all({"Poochy", "Large Spring Ball", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball", "Mole Tank Morph"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Tulip", player))) and state.has_all({"Beanstalk", "Super Star", "Large Spring Ball", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Egg Launcher", "Large Spring Ball"}, player)) + + set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player)) + set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Flowers", player), lambda state: state.has_any({"Large Spring Ball", "Super Star"}, player)) + set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Level Clear", player), lambda state: state.has_any({"Large Spring Ball", "Super Star"}, player)) + + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player) and logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Watch Out For Lakitu: Red Coins", player), lambda state: state.has("Chomp Rock", player)) + set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph", "Chomp Rock"}, player)) + set_rule(world.multiworld.get_location("Watch Out For Lakitu: Level Clear", player), lambda state: state.has("Chomp Rock", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Lakitu's Wall: Red Coins", player), lambda state: (state.has_any({"Dashed Platform", "Giant Eggs"}, player) or logic.combat_item(state)) and state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player) and (logic.combat_item(state) or state.has("Giant Eggs", player))) + set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Lakitu's Wall: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Car Morph"}, player)) + + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1))) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1))) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player) and logic.has_midring(state) and (state.has("Egg Capacity Upgrade", player, 1))) + + set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has_all({"Dashed Stairs", "Spring Ball"}, player)) + + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Stars", player), lambda state: logic.has_midring(state) or state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player)) + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state)) and (state.has("Dashed Platform", player))) + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Jammin' Through The Trees: Flowers", player), lambda state: state.has("Watermelon", player) or logic.melon_item(state)) + set_rule(world.multiworld.get_location("Jammin' Through The Trees: Stars", player), lambda state: ((logic.has_midring(state) or state.has("Tulip", player)) and logic.cansee_clouds(state)) or logic.has_midring(state) and state.has("Tulip", player)) + + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Beanstalk", "Mole Tank Morph", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Beanstalk", "Mole Tank Morph", "Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: state.has_all({"Tulip", "Large Spring Ball", "Dashed Stairs", "Chomp Rock", "Beanstalk", "Mole Tank Morph"}, player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Chomp Rock", "Large Spring Ball", "Key"}, player)) + + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph", "Large Spring Ball", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Flowers", player), lambda state: state.has_all({"Beanstalk", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Stars", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player)) + + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Stars", player), lambda state: state.has_all({"Large Spring Ball", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has_all({"! Switch", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player)) and state.has("! Switch", player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Level Clear", player), lambda state: state.has("! Switch", player)) + + set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player) and (logic.has_midring(state) or state.has("Vanishing Arrow Wheel", player) or logic.cansee_clouds(state))) + + set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Chomp Rock"}, player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Spring Ball", "Dashed Platform"}, player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Spring Ball", "Dashed Platform"}, player)) + + set_rule(world.multiworld.get_location("Lake Shore Paradise: Red Coins", player), lambda state: state.has_any({"Large Spring Ball", "Spring Ball"}, player) and (state.has("Egg Plant", player) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Flowers", player), lambda state: state.has_any({"Large Spring Ball", "Spring Ball"}, player) and (state.has("Egg Plant", player) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))) and (state.has("Egg Plant", player) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Level Clear", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state)) + + set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) and state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has_any({"Dashed Stairs", "Vanishing Arrow Wheel"}, player))) + + set_rule(world.multiworld.get_location("BLIZZARD!!!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("BLIZZARD!!!: Stars", player), lambda state: logic.cansee_clouds(state) or ((logic.has_midring(state) and state.has("Dashed Stairs", player)) or state.has("Tulip", player))) + + set_rule(world.multiworld.get_location("Ride The Ski Lifts: Stars", player), lambda state: logic.has_midring(state) or state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Bucket", "Spring Ball", "Super Star", "Skis", "Dashed Platform"}, player))) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis", "Dashed Platform"}, player)) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) and state.has("Spring Ball", player)) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis", "Dashed Platform"}, player)) + + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player)) + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, player)) + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Stars", player), lambda state: ((state.has_all({"Dashed Stairs", "Platform Ghost"}, player)) and logic.has_midring(state)) or (logic.cansee_clouds(state) and state.has("Dashed Stairs", player) and state.has("Dashed Platform", player))) + + set_rule(world.multiworld.get_location("Goonie Rides!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Goonie Rides!: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Goonie Rides!: Stars", player), lambda state: logic.has_midring(state)) + set_rule(world.multiworld.get_location("Goonie Rides!: Level Clear", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("Welcome To Cloud World: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player)) + + set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Red Coins", player), lambda state: state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) + set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Flowers", player), lambda state: state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) + set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Stars", player), lambda state: logic.has_midring(state) and state.has("Arrow Wheel", player)) + + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: state.has("Dashed Platform", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has_all({"Dashed Platform", "Large Spring Ball"}, player)) + + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Stars", player), lambda state: logic.cansee_clouds(state) or logic.has_midring(state)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: logic.has_midring(state) and state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, player)) + + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Red Coins", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Flowers", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state)) and logic.has_midring(state)) + + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Tulip", "Key"}, player) or (logic.has_midring(state) and state.has_all({"Key", "Chomp Rock", "Large Spring Ball"}, player))) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball", "Dashed Platform"}, player)) + + set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Flowers", player), lambda state: state.has("Egg Plant", player)) + set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68Route(state)) + + set_easy_extra_rules(world) + + +def set_easy_extra_rules(world: "YoshisIslandWorld") -> None: + player = world.player + logic = YoshiLogic(world) + if not world.options.extras_enabled: + return + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player)) + + set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "! Switch", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: logic.has_midring(state) and state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + + +def set_normal_rules(world: "YoshisIslandWorld") -> None: + logic = YoshiLogic(world) + player = world.player + + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Red Coins", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Flowers", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Make Eggs, Throw Eggs: Stars", player), lambda state: state.has_any({"Tulip", "Dashed Stairs"}, player)) + + set_rule(world.multiworld.get_location("Watch Out Below!: Red Coins", player), lambda state: state.has("Helicopter Morph", player)) + set_rule(world.multiworld.get_location("Watch Out Below!: Flowers", player), lambda state: state.has("Helicopter Morph", player)) + set_rule(world.multiworld.get_location("Watch Out Below!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player)) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player)) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Level Clear", player), lambda state: logic._14CanFightBoss(state)) + + + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Stars", player), lambda state: (logic.has_midring(state) and state.has_any(["Tulip", "Beanstalk"], player)) or (state.has_all(["Tulip", "Beanstalk", "Large Spring Ball"], player))) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Beanstalk"}, player)) + + set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Stars", player), lambda state: logic.has_midring(state) or (logic.cansee_clouds and state.has_all({"Spring Ball", "Chomp Rock", "Beanstalk"}, player))) + + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player)) + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player)) + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Stars", player), lambda state: logic.has_midring(state) and (state.has("Platform Ghost", player) or state.has_all({"Arrow Wheel", "Key"}, player))) + + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has_all({"Poochy", "Large Spring Ball", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Large Spring Ball", "Mole Tank Morph"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball", "Beanstalk", "Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player))) and state.has_all({"Beanstalk", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: state.has_all({"Beanstalk", "Super Star", "Large Spring Ball"}, player)) + + set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player)) + + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has_all({"! Switch", "Dashed Stairs"}, player)) + + set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph"}, player)) + + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Stars", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player) and (logic.combat_item(state) or state.has("Giant Eggs", player))) + set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) or logic.has_midring(state)) + + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Key", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has("Arrow Wheel", player) and (logic.has_midring(state) or state.has("Key", player))) + + set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has("Dashed Stairs", player)) + + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player)) + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state)) and (state.has("Dashed Platform", player) or logic.has_midring(state))) + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Jammin' Through The Trees: Flowers", player), lambda state: state.has("Watermelon", player) or logic.melon_item(state)) + + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"Mole Tank Morph", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"! Switch", "Mole Tank Morph", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: (state.has_any({"Dashed Stairs", "Beanstalk"}, player) and state.has_all({"Mole Tank Morph", "Large Spring Ball"}, player)) and (logic.has_midring(state) or state.has("Tulip", player))) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Key"}, player)) + + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph", "Large Spring Ball", "Beanstalk"}, player)) + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Flowers", player), lambda state: state.has("Beanstalk", player)) + + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Naval Piranha's Castle: Stars", player), lambda state: (logic.has_midring(state) and state.has("Tulip", player)) and state.has("Egg Capacity Upgrade", player, 1)) + + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Stars", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Stars", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "! Switch", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: (logic.has_midring(state) or state.has("Tulip", player)) and state.has("! Switch", player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Level Clear", player), lambda state: state.has("! Switch", player)) + + set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player)) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player)) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player)) + + set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Chomp Rock"}, player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Dashed Platform"}, player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch", "Dashed Platform"}, player)) + + set_rule(world.multiworld.get_location("Lake Shore Paradise: Red Coins", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state)) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Flowers", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state)) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state))) and (state.has("Egg Plant", player) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Lake Shore Paradise: Level Clear", player), lambda state: state.has("Egg Plant", player) or logic.combat_item(state)) + + set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) or state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Stars", player), lambda state: logic.has_midring(state) or (state.has_any({"Dashed Stairs", "Vanishing Arrow Wheel"}, player))) + + set_rule(world.multiworld.get_location("BLIZZARD!!!: Red Coins", player), lambda state: state.has_any({"Dashed Stairs", "Ice Melon"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state) or state.has("Helicopter Morph", player))) + + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Spring Ball", "Skis"}, player)) and (state.has("Super Star", player) or logic.melon_item(state))) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis"}, player)) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) or (logic.has_midring(state) and (state.has_all({"Tulip", "Dashed Platform"}, player)))) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis"}, player)) + + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player)) + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, player)) + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Stars", player), lambda state: ((state.has_all({"Dashed Stairs", "Platform Ghost"}, player))) or (logic.cansee_clouds(state) and state.has("Dashed Stairs", player))) + + set_rule(world.multiworld.get_location("Goonie Rides!: Red Coins", player), lambda state: state.has("Helicopter Morph", player)) + set_rule(world.multiworld.get_location("Goonie Rides!: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: logic.has_midring(state)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: logic.has_midring(state) and state.has_all({"Spring Ball", "Egg Plant", "Key"}, player)) + + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Red Coins", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Flowers", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: state.has("Chomp Rock", player) and (state.has("Egg Capacity Upgrade", player, 2) or logic.combat_item(state)) and logic.has_midring(state)) + + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Chomp Rock", "Key", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Tulip", "Key"}, player) or (logic.has_midring(state) and state.has_all({"Key", "Chomp Rock", "Large Spring Ball"}, player))) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Key", "Large Spring Ball", "Dashed Platform"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player))) + + set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has_all({"Helicopter Morph", "Egg Plant"}, player) and logic._68Route(state)) + + set_normal_extra_rules(world) + + +def set_normal_extra_rules(world: "YoshisIslandWorld") -> None: + player = world.player + logic = YoshiLogic(world) + if not world.options.extras_enabled: + return + + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player)) + + set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player) and logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) or logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: logic.has_midring(state) or state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state)) and state.has(("Large Spring Ball"), player)) + + +def set_hard_rules(world: "YoshisIslandWorld"): + logic = YoshiLogic(world) + player = world.player + + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Red Coins", player), lambda state: state.has("Spring Ball", player)) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Flowers", player), lambda state: state.has_all({"Spring Ball", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 3) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Burt The Bashful's Fort: Stars", player), lambda state: state.has("Spring Ball", player)) + + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "Flashing Eggs", "Mole Tank Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Shy-Guys On Stilts: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Touch Fuzzy Get Dizzy: Red Coins", player), lambda state: state.has_all({"Flashing Eggs", "Spring Ball", "Chomp Rock", "Beanstalk"}, player)) + + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Red Coins", player), lambda state: state.has("Platform Ghost", player)) + set_rule(world.multiworld.get_location("Salvo The Slime's Castle: Flowers", player), lambda state: state.has("Platform Ghost", player)) + + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Red Coins", player), lambda state: state.has("Large Spring Ball", player) and (state.has("Poochy", player) or logic.melon_item(state))) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Flowers", player), lambda state: state.has_all({"Super Star", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Stars", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Visit Koopa And Para-Koopa: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Baseball Boys: Red Coins", player), lambda state: state.has("Mole Tank Morph", player) and (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state))) + set_rule(world.multiworld.get_location("The Baseball Boys: Flowers", player), lambda state: (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state))) + set_rule(world.multiworld.get_location("The Baseball Boys: Level Clear", player), lambda state: (state.has_any({"Ice Melon", "Large Spring Ball"}, player) or logic.melon_item(state))) + + set_rule(world.multiworld.get_location("What's Gusty Taste Like?: Red Coins", player), lambda state: state.has("! Switch", player)) + + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Red Coins", player), lambda state: state.has_all({"! Switch", "Key"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Flowers", player), lambda state: state.has_all({"! Switch", "Key"}, player)) + set_rule(world.multiworld.get_location("Bigger Boo's Fort: Stars", player), lambda state: state.has("! Switch", player)) + + set_rule(world.multiworld.get_location("Watch Out For Lakitu: Flowers", player), lambda state: state.has_all({"Key", "Train Morph"}, player)) + + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("The Cave Of The Mystery Maze: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Lakitu's Wall: Flowers", player), lambda state: state.has("! Switch", player)) + set_rule(world.multiworld.get_location("Lakitu's Wall: Stars", player), lambda state: state.has("Giant Eggs", player) or logic.has_midring(state)) + + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Key"}, player)) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Key", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("The Potted Ghost's Castle: Stars", player), lambda state: state.has("Arrow Wheel", player)) + + set_rule(world.multiworld.get_location("Welcome To Monkey World!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Jungle Rhythm...: Red Coins", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Flowers", player), lambda state: state.has("Dashed Stairs", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Stars", player), lambda state: logic.has_midring(state) and state.has("Tulip", player)) + set_rule(world.multiworld.get_location("Jungle Rhythm...: Level Clear", player), lambda state: state.has("Dashed Stairs", player)) + + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Red Coins", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Flowers", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Nep-Enuts' Domain: Level Clear", player), lambda state: state.has_all({"Submarine Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Red Coins", player), lambda state: state.has("Submarine Morph", player)) + set_rule(world.multiworld.get_location("Prince Froggy's Fort: Flowers", player), lambda state: (state.has("Egg Capacity Upgrade", player, 5) or logic.combat_item(state))) + + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Red Coins", player), lambda state: state.has("Mole Tank Morph", player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Flowers", player), lambda state: state.has_all({"Mole Tank Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player)) + set_rule(world.multiworld.get_location("The Cave Of Harry Hedgehog: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "Key"}, player)) + + set_rule(world.multiworld.get_location("Monkeys' Favorite Lake: Red Coins", player), lambda state: state.has_all({"! Switch", "Submarine Morph"}, player)) + + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("GO! GO! MARIO!!: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Red Coins", player), lambda state: state.has_all({"! Switch", "Egg Launcher"}, player)) + set_rule(world.multiworld.get_location("The Cave Of The Lakitus: Flowers", player), lambda state: state.has("Egg Launcher", player)) + + set_rule(world.multiworld.get_location("Don't Look Back!: Red Coins", player), lambda state: state.has_all({"Helicopter Morph", "Large Spring Ball"}, player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Don't Look Back!: Stars", player), lambda state: logic.has_midring(state) or state.has("Tulip", player)) + + set_rule(world.multiworld.get_location("Marching Milde's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, player)) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket"}, player)) + set_rule(world.multiworld.get_location("Marching Milde's Fort: Stars", player), lambda state: state.has("Dashed Stairs", player)) + + set_rule(world.multiworld.get_location("Chomp Rock Zone: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Flowers", player), lambda state: state.has_all({"Chomp Rock", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Chomp Rock Zone: Stars", player), lambda state: state.has_all({"Chomp Rock", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("Lake Shore Paradise: Stars", player), lambda state: (logic.has_midring(state) or (state.has("Tulip", player) and logic.cansee_clouds(state)))) + + set_rule(world.multiworld.get_location("Ride Like The Wind: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Stars", player), lambda state: (logic.has_midring(state) or state.has("Helicopter Morph", player)) and state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Ride Like The Wind: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Red Coins", player), lambda state: state.has_all({"Key", "Dashed Stairs"}, player)) + set_rule(world.multiworld.get_location("Hookbill The Koopa's Castle: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Key"}, player)) + + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Red Coins", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and (state.has_all({"Spring Ball", "Skis"}, player)) and (state.has("Super Star", player) or logic.melon_item(state))) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Flowers", player), lambda state: (state.has("Fire Melon", player) or logic.melon_item(state)) and state.has_all({"Spring Ball", "Skis"}, player)) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Stars", player), lambda state: (logic.has_midring(state) and (state.has("Fire Melon", player) or logic.melon_item(state))) or (logic.has_midring(state) and (state.has_all({"Tulip", "Dashed Platform"}, player)))) + set_rule(world.multiworld.get_location("Danger - Icy Conditions Ahead: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Skis"}, player)) + + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Red Coins", player), lambda state: state.has_all({"Dashed Stairs", "Dashed Platform", "Platform Ghost"}, player)) + set_rule(world.multiworld.get_location("Sluggy The Unshaven's Fort: Flowers", player), lambda state: state.has_all({"Dashed Stairs", "Platform Ghost"}, player)) + + set_rule(world.multiworld.get_location("Shifting Platforms Ahead: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Red Coins", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + set_rule(world.multiworld.get_location("Raphael The Raven's Castle: Flowers", player), lambda state: state.has_all({"Arrow Wheel", "Train Morph"}, player)) + + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Red Coins", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Flowers", player), lambda state: state.has("Large Spring Ball", player)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Stars", player), lambda state: logic.has_midring(state)) + set_rule(world.multiworld.get_location("Scary Skeleton Goonies!: Level Clear", player), lambda state: state.has("Large Spring Ball", player)) + + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Red Coins", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Flowers", player), lambda state: state.has("Super Star", player)) + set_rule(world.multiworld.get_location("The Cave Of The Bandits: Level Clear", player), lambda state: state.has("Super Star", player)) + + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Red Coins", player), lambda state: state.has_all({"Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Flowers", player), lambda state: state.has_all({"Egg Plant", "Key"}, player) and (state.has("Egg Capacity Upgrade", player, 1) or logic.combat_item(state))) + set_rule(world.multiworld.get_location("Tap-Tap The Red Nose's Fort: Stars", player), lambda state: state.has("Egg Plant", player) and state.has("Key", player)) + + set_rule(world.multiworld.get_location("The Very Loooooong Cave: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Red Coins", player), lambda state: state.has_all({"Key", "Large Spring Ball"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player))) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Flowers", player), lambda state: state.has_all({"Key", "Large Spring Ball"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player))) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Stars", player), lambda state: state.has_all({"Chomp Rock", "Key"}, player)) + set_rule(world.multiworld.get_location("The Deep, Underground Maze: Level Clear", player), lambda state: state.has_all({"Key", "Large Spring Ball", "Dashed Platform"}, player) and (logic.combat_item(state) or state.has("Chomp Rock", player))) + + set_rule(world.multiworld.get_location("KEEP MOVING!!!!: Stars", player), lambda state: logic.has_midring(state)) + + set_rule(world.multiworld.get_location("King Bowser's Castle: Red Coins", player), lambda state: state.has("Helicopter Morph", player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Flowers", player), lambda state: state.has("Helicopter Morph", player) and logic._68CollectibleRoute(state)) + set_rule(world.multiworld.get_location("King Bowser's Castle: Stars", player), lambda state: state.has("Helicopter Morph", player) and logic._68Route(state)) + + set_hard_extra_rules(world) + + +def set_hard_extra_rules(world: "YoshisIslandWorld") -> None: + player = world.player + logic = YoshiLogic(world) + if not world.options.extras_enabled: + return + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Red Coins", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Flowers", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Stars", player), lambda state: state.has("Poochy", player)) + set_rule(world.multiworld.get_location("Poochy Ain't Stupid: Level Clear", player), lambda state: state.has("Poochy", player)) + + set_rule(world.multiworld.get_location("Hit That Switch!!: Red Coins", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Flowers", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Hit That Switch!!: Level Clear", player), lambda state: state.has_all({"Large Spring Ball", "! Switch"}, player)) + + set_rule(world.multiworld.get_location("The Impossible? Maze: Red Coins", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph", "Flashing Eggs"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Flowers", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Stars", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph"}, player)) + set_rule(world.multiworld.get_location("The Impossible? Maze: Level Clear", player), lambda state: state.has_all({"Spring Ball", "Large Spring Ball", "Mole Tank Morph", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Kamek's Revenge: Red Coins", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Flowers", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph", "! Switch"}, player)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Stars", player), lambda state: state.has("! Switch", player) or logic.has_midring(state)) + set_rule(world.multiworld.get_location("Kamek's Revenge: Level Clear", player), lambda state: state.has_all({"Key", "Skis", "Helicopter Morph"}, player)) + + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Red Coins", player), lambda state: state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Flowers", player), lambda state: state.has(("Large Spring Ball"), player)) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Stars", player), lambda state: True) + set_rule(world.multiworld.get_location("Castles - Masterpiece Set: Level Clear", player), lambda state: state.has(("Large Spring Ball"), player)) diff --git a/worlds/yoshisisland/__init__.py b/worlds/yoshisisland/__init__.py new file mode 100644 index 000000000000..b5d7e137b5f3 --- /dev/null +++ b/worlds/yoshisisland/__init__.py @@ -0,0 +1,388 @@ +import base64 +import os +import typing +import threading + +from typing import List, Set, TextIO, Dict +from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification +from worlds.AutoWorld import World, WebWorld +import settings +from .Items import get_item_names_per_category, item_table, filler_items, trap_items +from .Locations import get_locations +from .Regions import init_areas +from .Options import YoshisIslandOptions, PlayerGoal, ObjectVis, StageLogic, MinigameChecks +from .setup_game import setup_gamevars +from .Client import YoshisIslandSNIClient +from .Rules import set_easy_rules, set_normal_rules, set_hard_rules +from .Rom import LocalRom, patch_rom, get_base_rom_path, YoshisIslandDeltaPatch, USHASH + + +class YoshisIslandSettings(settings.Group): + class RomFile(settings.SNESRomPath): + """File name of the Yoshi's Island 1.0 US rom""" + description = "Yoshi's Island ROM File" + copy_to = "Super Mario World 2 - Yoshi's Island (U).sfc" + md5s = [USHASH] + + rom_file: RomFile = RomFile(RomFile.copy_to) + + +class YoshisIslandWeb(WebWorld): + theme = "ocean" + + setup_en = Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Yoshi's Island randomizer" + "and connecting to an Archipelago server.", + "English", + "setup_en.md", + "setup/en", + ["Pink Switch"] + ) + + tutorials = [setup_en] + + +class YoshisIslandWorld(World): + """ + Yoshi's Island is a 2D platforming game. + During a delivery, Bowser's evil ward, Kamek, attacked the stork, kidnapping Luigi and dropping Mario onto Yoshi's Island. + As Yoshi, you must run, jump, and throw eggs to escort the baby Mario across the island to defeat Bowser and reunite the two brothers with their parents. + """ + game = "Yoshi's Island" + option_definitions = YoshisIslandOptions + required_client_version = (0, 4, 4) + + item_name_to_id = {item: item_table[item].code for item in item_table} + location_name_to_id = {location.name: location.code for location in get_locations(None)} + item_name_groups = get_item_names_per_category() + + web = YoshisIslandWeb() + settings: typing.ClassVar[YoshisIslandSettings] + # topology_present = True + + options_dataclass = YoshisIslandOptions + options: YoshisIslandOptions + + locked_locations: List[str] + set_req_bosses: str + lives_high: int + lives_low: int + castle_bosses: int + bowser_bosses: int + baby_mario_sfx: int + leader_color: int + boss_order: list + boss_burt: int + luigi_count: int + + rom_name: bytearray + + def __init__(self, multiworld: MultiWorld, player: int): + self.rom_name_available_event = threading.Event() + super().__init__(multiworld, player) + self.locked_locations = [] + + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld) -> None: + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + + def fill_slot_data(self) -> Dict[str, List[int]]: + return { + "world_1": self.world_1_stages, + "world_2": self.world_2_stages, + "world_3": self.world_3_stages, + "world_4": self.world_4_stages, + "world_5": self.world_5_stages, + "world_6": self.world_6_stages + } + + def write_spoiler_header(self, spoiler_handle: TextIO) -> None: + spoiler_handle.write(f"Burt The Bashful's Boss Door: {self.boss_order[0]}\n") + spoiler_handle.write(f"Salvo The Slime's Boss Door: {self.boss_order[1]}\n") + spoiler_handle.write(f"Bigger Boo's Boss Door: {self.boss_order[2]}\n") + spoiler_handle.write(f"Roger The Ghost's Boss Door: {self.boss_order[3]}\n") + spoiler_handle.write(f"Prince Froggy's Boss Door: {self.boss_order[4]}\n") + spoiler_handle.write(f"Naval Piranha's Boss Door: {self.boss_order[5]}\n") + spoiler_handle.write(f"Marching Milde's Boss Door: {self.boss_order[6]}\n") + spoiler_handle.write(f"Hookbill The Koopa's Boss Door: {self.boss_order[7]}\n") + spoiler_handle.write(f"Sluggy The Unshaven's Boss Door: {self.boss_order[8]}\n") + spoiler_handle.write(f"Raphael The Raven's Boss Door: {self.boss_order[9]}\n") + spoiler_handle.write(f"Tap-Tap The Red Nose's Boss Door: {self.boss_order[10]}\n") + spoiler_handle.write(f"\nLevels:\n1-1: {self.level_name_list[0]}\n") + spoiler_handle.write(f"1-2: {self.level_name_list[1]}\n") + spoiler_handle.write(f"1-3: {self.level_name_list[2]}\n") + spoiler_handle.write(f"1-4: {self.level_name_list[3]}\n") + spoiler_handle.write(f"1-5: {self.level_name_list[4]}\n") + spoiler_handle.write(f"1-6: {self.level_name_list[5]}\n") + spoiler_handle.write(f"1-7: {self.level_name_list[6]}\n") + spoiler_handle.write(f"1-8: {self.level_name_list[7]}\n") + + spoiler_handle.write(f"\n2-1: {self.level_name_list[8]}\n") + spoiler_handle.write(f"2-2: {self.level_name_list[9]}\n") + spoiler_handle.write(f"2-3: {self.level_name_list[10]}\n") + spoiler_handle.write(f"2-4: {self.level_name_list[11]}\n") + spoiler_handle.write(f"2-5: {self.level_name_list[12]}\n") + spoiler_handle.write(f"2-6: {self.level_name_list[13]}\n") + spoiler_handle.write(f"2-7: {self.level_name_list[14]}\n") + spoiler_handle.write(f"2-8: {self.level_name_list[15]}\n") + + spoiler_handle.write(f"\n3-1: {self.level_name_list[16]}\n") + spoiler_handle.write(f"3-2: {self.level_name_list[17]}\n") + spoiler_handle.write(f"3-3: {self.level_name_list[18]}\n") + spoiler_handle.write(f"3-4: {self.level_name_list[19]}\n") + spoiler_handle.write(f"3-5: {self.level_name_list[20]}\n") + spoiler_handle.write(f"3-6: {self.level_name_list[21]}\n") + spoiler_handle.write(f"3-7: {self.level_name_list[22]}\n") + spoiler_handle.write(f"3-8: {self.level_name_list[23]}\n") + + spoiler_handle.write(f"\n4-1: {self.level_name_list[24]}\n") + spoiler_handle.write(f"4-2: {self.level_name_list[25]}\n") + spoiler_handle.write(f"4-3: {self.level_name_list[26]}\n") + spoiler_handle.write(f"4-4: {self.level_name_list[27]}\n") + spoiler_handle.write(f"4-5: {self.level_name_list[28]}\n") + spoiler_handle.write(f"4-6: {self.level_name_list[29]}\n") + spoiler_handle.write(f"4-7: {self.level_name_list[30]}\n") + spoiler_handle.write(f"4-8: {self.level_name_list[31]}\n") + + spoiler_handle.write(f"\n5-1: {self.level_name_list[32]}\n") + spoiler_handle.write(f"5-2: {self.level_name_list[33]}\n") + spoiler_handle.write(f"5-3: {self.level_name_list[34]}\n") + spoiler_handle.write(f"5-4: {self.level_name_list[35]}\n") + spoiler_handle.write(f"5-5: {self.level_name_list[36]}\n") + spoiler_handle.write(f"5-6: {self.level_name_list[37]}\n") + spoiler_handle.write(f"5-7: {self.level_name_list[38]}\n") + spoiler_handle.write(f"5-8: {self.level_name_list[39]}\n") + + spoiler_handle.write(f"\n6-1: {self.level_name_list[40]}\n") + spoiler_handle.write(f"6-2: {self.level_name_list[41]}\n") + spoiler_handle.write(f"6-3: {self.level_name_list[42]}\n") + spoiler_handle.write(f"6-4: {self.level_name_list[43]}\n") + spoiler_handle.write(f"6-5: {self.level_name_list[44]}\n") + spoiler_handle.write(f"6-6: {self.level_name_list[45]}\n") + spoiler_handle.write(f"6-7: {self.level_name_list[46]}\n") + spoiler_handle.write("6-8: King Bowser's Castle") + + def create_item(self, name: str) -> Item: + data = item_table[name] + return Item(name, data.classification, data.code, self.player) + + def create_regions(self) -> None: + init_areas(self, get_locations(self)) + + def get_filler_item_name(self) -> str: + trap_chance: int = self.options.trap_percent.value + + if self.random.random() < (trap_chance / 100) and self.options.traps_enabled: + return self.random.choice(trap_items) + else: + return self.random.choice(filler_items) + + def set_rules(self) -> None: + rules_per_difficulty = { + 0: set_easy_rules, + 1: set_normal_rules, + 2: set_hard_rules + } + + rules_per_difficulty[self.options.stage_logic.value](self) + self.multiworld.completion_condition[self.player] = lambda state: state.has("Saved Baby Luigi", self.player) + self.get_location("Burt The Bashful's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Salvo The Slime's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Bigger Boo's Boss Room", ).place_locked_item(self.create_item("Boss Clear")) + self.get_location("Roger The Ghost's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Prince Froggy's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Naval Piranha's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Marching Milde's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Hookbill The Koopa's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Sluggy The Unshaven's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Raphael The Raven's Boss Room").place_locked_item(self.create_item("Boss Clear")) + self.get_location("Tap-Tap The Red Nose's Boss Room").place_locked_item(self.create_item("Boss Clear")) + + if self.options.goal == PlayerGoal.option_luigi_hunt: + self.get_location("Reconstituted Luigi").place_locked_item(self.create_item("Saved Baby Luigi")) + else: + self.get_location("King Bowser's Castle: Level Clear").place_locked_item( + self.create_item("Saved Baby Luigi") + ) + + self.get_location("Touch Fuzzy Get Dizzy: Gather Coins").place_locked_item( + self.create_item("Bandit Consumables") + ) + self.get_location("The Cave Of the Mystery Maze: Seed Spitting Contest").place_locked_item( + self.create_item("Bandit Watermelons") + ) + self.get_location("Lakitu's Wall: Gather Coins").place_locked_item(self.create_item("Bandit Consumables")) + self.get_location("Ride Like The Wind: Gather Coins").place_locked_item(self.create_item("Bandit Consumables")) + + def generate_early(self) -> None: + setup_gamevars(self) + + def get_excluded_items(self) -> Set[str]: + excluded_items: Set[str] = set() + + starting_gate = ["World 1 Gate", "World 2 Gate", "World 3 Gate", + "World 4 Gate", "World 5 Gate", "World 6 Gate"] + + excluded_items.add(starting_gate[self.options.starting_world]) + + if not self.options.shuffle_midrings: + excluded_items.add("Middle Ring") + + if not self.options.add_secretlens: + excluded_items.add("Secret Lens") + + if not self.options.extras_enabled: + excluded_items.add("Extra Panels") + excluded_items.add("Extra 1") + excluded_items.add("Extra 2") + excluded_items.add("Extra 3") + excluded_items.add("Extra 4") + excluded_items.add("Extra 5") + excluded_items.add("Extra 6") + + if self.options.split_extras: + excluded_items.add("Extra Panels") + else: + excluded_items.add("Extra 1") + excluded_items.add("Extra 2") + excluded_items.add("Extra 3") + excluded_items.add("Extra 4") + excluded_items.add("Extra 5") + excluded_items.add("Extra 6") + + if self.options.split_bonus: + excluded_items.add("Bonus Panels") + else: + excluded_items.add("Bonus 1") + excluded_items.add("Bonus 2") + excluded_items.add("Bonus 3") + excluded_items.add("Bonus 4") + excluded_items.add("Bonus 5") + excluded_items.add("Bonus 6") + + return excluded_items + + def create_item_with_correct_settings(self, name: str) -> Item: + data = item_table[name] + item = Item(name, data.classification, data.code, self.player) + + if not item.advancement: + return item + + if name == "Car Morph" and self.options.stage_logic != StageLogic.option_strict: + item.classification = ItemClassification.useful + + secret_lens_visibility_check = ( + self.options.hidden_object_visibility >= ObjectVis.option_clouds_only + or self.options.stage_logic != StageLogic.option_strict + ) + if name == "Secret Lens" and secret_lens_visibility_check: + item.classification = ItemClassification.useful + + is_bonus_location = name in {"Bonus 1", "Bonus 2", "Bonus 3", "Bonus 4", "Bonus 5", "Bonus 6", "Bonus Panels"} + bonus_games_disabled = ( + self.options.minigame_checks not in {MinigameChecks.option_bonus_games, MinigameChecks.option_both} + ) + if is_bonus_location and bonus_games_disabled: + item.classification = ItemClassification.useful + + if name in {"Bonus 1", "Bonus 3", "Bonus 4", "Bonus Panels"} and self.options.item_logic: + item.classification = ItemClassification.progression + + if name == "Piece of Luigi" and self.options.goal == PlayerGoal.option_luigi_hunt: + if self.luigi_count >= self.options.luigi_pieces_required: + item.classification = ItemClassification.useful + else: + item.classification = ItemClassification.progression_skip_balancing + self.luigi_count += 1 + + return item + + def generate_filler(self, pool: List[Item]) -> None: + if self.options.goal == PlayerGoal.option_luigi_hunt: + for _ in range(self.options.luigi_pieces_in_pool.value): + item = self.create_item_with_correct_settings("Piece of Luigi") + pool.append(item) + + for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(pool) - 16): + item = self.create_item_with_correct_settings(self.get_filler_item_name()) + pool.append(item) + + def get_item_pool(self, excluded_items: Set[str]) -> List[Item]: + pool: List[Item] = [] + + for name, data in item_table.items(): + if name not in excluded_items: + for _ in range(data.amount): + item = self.create_item_with_correct_settings(name) + pool.append(item) + + return pool + + def create_items(self) -> None: + self.luigi_count = 0 + + if self.options.minigame_checks in {MinigameChecks.option_bonus_games, MinigameChecks.option_both}: + self.multiworld.get_location("Flip Cards", self.player).place_locked_item( + self.create_item("Bonus Consumables")) + self.multiworld.get_location("Drawing Lots", self.player).place_locked_item( + self.create_item("Bonus Consumables")) + self.multiworld.get_location("Match Cards", self.player).place_locked_item( + self.create_item("Bonus Consumables")) + + pool = self.get_item_pool(self.get_excluded_items()) + + self.generate_filler(pool) + + self.multiworld.itempool += pool + + def generate_output(self, output_directory: str) -> None: + rompath = "" # if variable is not declared finally clause may fail + try: + world = self.multiworld + player = self.player + rom = LocalRom(get_base_rom_path()) + patch_rom(self, rom, self.player) + + rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc") + rom.write_to_file(rompath) + self.rom_name = rom.name + + patch = YoshisIslandDeltaPatch(os.path.splitext(rompath)[0] + YoshisIslandDeltaPatch.patch_file_ending, + player=player, player_name=world.player_name[player], patched_path=rompath) + patch.write() + finally: + self.rom_name_available_event.set() + if os.path.exists(rompath): + os.unlink(rompath) + + def modify_multidata(self, multidata: dict) -> None: + # wait for self.rom_name to be available. + self.rom_name_available_event.wait() + rom_name = getattr(self, "rom_name", None) + if rom_name: + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + def extend_hint_information(self, hint_data: typing.Dict[int, typing.Dict[int, str]]) -> None: + world_names = [f"World {i}" for i in range(1, 7)] + world_stages = [ + self.world_1_stages, self.world_2_stages, self.world_3_stages, + self.world_4_stages, self.world_5_stages, self.world_6_stages + ] + + stage_pos_data = {} + for loc in self.multiworld.get_locations(self.player): + if loc.address is None: + continue + + level_id = getattr(loc, "level_id") + for level, stages in zip(world_names, world_stages): + if level_id in stages: + stage_pos_data[loc.address] = level + break + + hint_data[self.player] = stage_pos_data diff --git a/worlds/yoshisisland/docs/en_Yoshi's Island.md b/worlds/yoshisisland/docs/en_Yoshi's Island.md new file mode 100644 index 000000000000..8cd825cc7f34 --- /dev/null +++ b/worlds/yoshisisland/docs/en_Yoshi's Island.md @@ -0,0 +1,71 @@ +# Yoshi's Island + +## Where is the settings page? + +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. + +## What does randomization do to this game? + +Certain interactable objects within levels will be unable to be used until the corresponding item is found. If the item is not in the player's posession, the object will flash and will not function. Objects include: +- Spring Ball +- Large Spring Ball +- ! Switch +- Dashed Platform +- Dashed Stairs +- Beanstalk +- Arrow Wheel +- Vanishing Arrow Wheel +- Ice, fire, and normal watermelons +- Super Star +- Flashing Eggs +- Giant Eggs +- Egg Launcher +- Egg Refill Plant +- Chomp Rock +- Poochy +- Transformation Morphs +- Skis +- Platform Ghost +- Middle Rings +- Buckets +- Tulips + +Yoshi will start out being able to carry only one egg, and 5 capacity upgrades can be found to bring the total up to 6. +The player will start with all levels unlocked in their starting world, and can collect 'World Gates' to unlock levels from other worlds. +Extra and Bonus stages will also start out locked, and require respective items to access them. 6-8 is locked, and will be unlocked +upon reaching the number of boss clears defined by the player. +Other checks will grant the player extra lives, consumables for use in the inventory, or traps. + +Additionally, the player is able to randomize the bosses found at the end of boss stages, the order of stages, +the world they start in, the starting amount of lives, route through 6-8, and the color of Yoshi for each stage. + +## What is the goal of Yoshi's Island when randomized? + +The player can choose one of two goals: +- Bowser: Defeat a pre-defined number of bosses, and defeat Bowser at the end of 6-8. +- Luigi Hunt: Collect a pre-defined number of 'Pieces of Luigi' within levels. + +## What items and locations get shuffled? + +Locations consist of 'level objectives', that being: +- Beating the stage +- Collecting 20 red coins. +- Collecting 5 flowers. +- Collecting 30 stars. + +Checks will be sent immediately upon achieving that objective, regardless of if the stage is cleared or not. +Additional checks can be placed on Bandit mini-battles, or overworld minigames. + + +## Which items can be in another player's world? + +Any shuffled item can be in other players' worlds. + +## What does another world's item look like in Yoshi's Island + +Items do not have an appearance in Yoshi's Island + +## When the player receives an item, what happens? + +When the player recieves an item, a fanfare or sound will be heard to reflect the item received. Most items, aside from Egg Capacity and level unlocks, can be checked on the menu by pressing SELECT. +If an item is in the queue and has not been received, checks will not be processed. diff --git a/worlds/yoshisisland/docs/setup_en.md b/worlds/yoshisisland/docs/setup_en.md new file mode 100644 index 000000000000..30aadbfa604d --- /dev/null +++ b/worlds/yoshisisland/docs/setup_en.md @@ -0,0 +1,123 @@ +# Yoshi's Island Archipelago Randomizer Setup Guide + +## Required Software + +- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). + + +- Hardware or software capable of loading and playing SNES ROM files + - An emulator capable of connecting to SNI such as: + - snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases), + - BizHawk from: [TASVideos](https://tasvideos.org/BizHawk) + - snes9x-nwa from: [snes9x nwa](https://github.com/Skarsnik/snes9x-emunwa/releases) + + NOTE: RetroArch and FXPakPro are not currently supported. +- Your legally obtained Yoshi's Island English 1.0 ROM file, probably named `Super Mario World 2 - Yoshi's Island (U).sfc` + + +## Installation Procedures + +### Windows Setup + +1. Download and install Archipelago from the link above, making sure to install the most recent version. +2. During generation/patching, you will be asked to locate your base ROM file. This is your Yoshi's Island ROM file. +3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM + files. + 1. Extract your emulator's folder to your Desktop, or somewhere you will remember. + 2. Right-click on a ROM file and select **Open with...** + 3. Check the box next to **Always use this app to open .sfc files** + 4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC** + 5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you + extracted in step one. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Options page on the website allows you to configure your personal settings and export a config file from +them. + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whomever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.apyi` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + +### Connect to the client + +#### With an emulator + +When the client launched automatically, SNI should have also automatically launched in the background. If this is its +first time launching, you may be prompted to allow it to communicate through the Windows Firewall. + +##### snes9x-rr + +1. Load your ROM file if it hasn't already been loaded. +2. Click on the File menu and hover on **Lua Scripting** +3. Click on **New Lua Script Window...** +4. In the new window, click **Browse...** +5. Select the connector lua file included with your client + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. +6. If you see an error while loading the script that states `socket.dll missing` or similar, navigate to the folder of +the lua you are using in your file explorer and copy the `socket.dll` to the base folder of your snes9x install. + +##### BizHawk + +1. Ensure you have the BSNES core loaded. This is done with the main menubar, under: + - (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES` + - (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+` +2. Load your ROM file if it hasn't already been loaded. + If you changed your core preference after loading the ROM, don't forget to reload it (default hotkey: Ctrl+R). +3. Drag+drop the `Connector.lua` file included with your client onto the main EmuHawk window. + - Look in the Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the + emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only. + - You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to `Connector.lua` + with the file picker. + + + +### Connect to the Archipelago Server + +The patch file which launched your client should have automatically connected you to the AP Server. There are a few +reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the +client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it +into the "Server" input field then press enter. + +The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected". + +### Play the game + +When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on +successfully joining a multiworld game! + +## Hosting a MultiWorld game + +The recommended way to host a game is to use our hosting service. The process is relatively simple: + +1. Collect config files from your players. +2. Create a zip file containing your players' config files. +3. Upload that zip file to the Generate page above. + - Generate page: [WebHost Seed Generation Page](/generate) +4. Wait a moment while the seed is generated. +5. When the seed is generated, you will be redirected to a "Seed Info" page. +6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so + they may download their patch files from there. +7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all + players in the game. Any observers may also be given the link to this page. +8. Once all players have joined, you may begin playing. diff --git a/worlds/yoshisisland/level_logic.py b/worlds/yoshisisland/level_logic.py new file mode 100644 index 000000000000..094e5efed12d --- /dev/null +++ b/worlds/yoshisisland/level_logic.py @@ -0,0 +1,482 @@ +from BaseClasses import CollectionState +from typing import TYPE_CHECKING + +from .Options import StageLogic, BowserDoor, ObjectVis + +if TYPE_CHECKING: + from . import YoshisIslandWorld + + +class YoshiLogic: + player: int + game_logic: str + midring_start: bool + clouds_always_visible: bool + consumable_logic: bool + luigi_pieces: int + + def __init__(self, world: "YoshisIslandWorld") -> None: + self.player = world.player + self.boss_order = world.boss_order + self.luigi_pieces = world.options.luigi_pieces_required.value + + if world.options.stage_logic == StageLogic.option_strict: + self.game_logic = "Easy" + elif world.options.stage_logic == StageLogic.option_loose: + self.game_logic = "Normal" + else: + self.game_logic = "Hard" + + self.midring_start = not world.options.shuffle_midrings + self.consumable_logic = not world.options.item_logic + + self.clouds_always_visible = world.options.hidden_object_visibility >= ObjectVis.option_clouds_only + + self.bowser_door = world.options.bowser_door_mode.value + if self.bowser_door == BowserDoor.option_door_4: + self.bowser_door = BowserDoor.option_door_3 + + def has_midring(self, state: CollectionState) -> bool: + return self.midring_start or state.has("Middle Ring", self.player) + + def reconstitute_luigi(self, state: CollectionState) -> bool: + return state.has("Piece of Luigi", self.player, self.luigi_pieces) + + def bandit_bonus(self, state: CollectionState) -> bool: + return state.has("Bandit Consumables", self.player) or state.has("Bandit Watermelons", self.player) + + def item_bonus(self, state: CollectionState) -> bool: + return state.has("Bonus Consumables", self.player) + + def combat_item(self, state: CollectionState) -> bool: + if not self.consumable_logic: + return False + else: + if self.game_logic == "Easy": + return self.item_bonus(state) + else: + return self.bandit_bonus(state) or self.item_bonus(state) + + def melon_item(self, state: CollectionState) -> bool: + if not self.consumable_logic: + return False + else: + if self.game_logic == "Easy": + return self.item_bonus(state) + else: + return state.has("Bandit Watermelons", self.player) or self.item_bonus(state) + + def default_vis(self, state: CollectionState) -> bool: + if self.clouds_always_visible: + return True + else: + return False + + def cansee_clouds(self, state: CollectionState) -> bool: + if self.game_logic != "Easy": + return True + else: + return self.default_vis(state) or state.has("Secret Lens", self.player) or self.combat_item(state) + + def bowserdoor_1(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Egg Plant", "! Switch"}, self.player) and state.has("Egg Capacity Upgrade", self.player, 2) + elif self.game_logic == "Normal": + return state.has("Egg Plant", self.player) and state.has("Egg Capacity Upgrade", self.player, 1) + else: + return state.has("Egg Plant", self.player) + + def bowserdoor_2(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return ((state.has("Egg Capacity Upgrade", self.player, 3) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player) + elif self.game_logic == "Normal": + return ((state.has("Egg Capacity Upgrade", self.player, 2) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player) + else: + return ((state.has("Egg Capacity Upgrade", self.player, 1) and state.has("Egg Plant", self.player)) or self.combat_item(state)) and state.has("Key", self.player) + + def bowserdoor_3(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def bowserdoor_4(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _68Route(self, state: CollectionState) -> bool: + if self.bowser_door == 0: + return True + elif self.bowser_door == 1: + return self.bowserdoor_1(state) + elif self.bowser_door == 2: + return self.bowserdoor_2(state) + elif self.bowser_door == 3: + return True + elif self.bowser_door == 4: + return True + elif self.bowser_door == 5: + return self.bowserdoor_1(state) and self.bowserdoor_2(state) and self.bowserdoor_3(state) + + def _68CollectibleRoute(self, state: CollectionState) -> bool: + if self.bowser_door == 0: + return True + elif self.bowser_door == 1: + return self.bowserdoor_1(state) + elif self.bowser_door == 2: + return self.bowserdoor_2(state) + elif self.bowser_door == 3: + return True + elif self.bowser_door == 4: + return True + elif self.bowser_door == 5: + return self.bowserdoor_1(state) + + +############################################################################## + def _13Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Key", self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _14Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Spring Ball", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Spring Ball", "Key"}, self.player) + else: + return state.has_all({"Spring Ball", "Key"}, self.player) + + def _14Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Egg Plant", self.player) + elif self.game_logic == "Normal": + return state.has("Egg Plant", self.player) + else: + return (state.has("Egg Capacity Upgrade", self.player, 5) or state.has("Egg Plant", self.player)) + + def _14CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[0], "Location", self.player): + return True +############################################################################## + def _17Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Key", self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _18Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Key", "Arrow Wheel"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Key", "Arrow Wheel"}, self.player) + else: + return state.has_all({"Key", "Arrow Wheel"}, self.player) + + def _18Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _18CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[1], "Location", self.player): + return True +############################################################################## + def _21Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player) + else: + return state.has_all({"Poochy", "Large Spring Ball", "Key"}, self.player) +############################################################################## + def _23Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Mole Tank Morph", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Mole Tank Morph", "Key"}, self.player) + else: + return state.has_all({"Mole Tank Morph", "Key"}, self.player) +############################################################################## + def _24Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"! Switch", "Key", "Dashed Stairs"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"! Switch", "Dashed Stairs"}, self.player) + else: + return state.has("! Switch", self.player) + + def _24Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _24CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[2], "Location", self.player): + return True +############################################################################## + def _26Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Large Spring Ball", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Large Spring Ball", "Key"}, self.player) + else: + return state.has_all({"Large Spring Ball", "Key"}, self.player) +############################################################################## + def _27Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Key", self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _28Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Arrow Wheel", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1)) + elif self.game_logic == "Normal": + return state.has_all({"Arrow Wheel", "Key"}, self.player) + else: + return state.has_all({"Arrow Wheel", "Key"}, self.player) + + def _28Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _28CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[3], "Location", self.player): + return True +############################################################################## + def _32Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Dashed Stairs", "Spring Ball", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Dashed Stairs", "Key"}, self.player) + else: + return state.has_all({"Dashed Stairs", "Key"}, self.player) +############################################################################## + def _34Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Dashed Platform", self.player) + elif self.game_logic == "Normal": + return (state.has("Dashed Platform", self.player) or self.has_midring(state)) + else: + return True + + def _34Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Giant Eggs", self.player) + elif self.game_logic == "Normal": + return True + else: + return True + + def _34CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[4], "Location", self.player): + return True +############################################################################## + def _37Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Key", "Large Spring Ball"}, self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _38Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return (state.has("Egg Capacity Upgrade", self.player, 3) or self.combat_item(state)) + elif self.game_logic == "Normal": + return (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state)) + else: + return True + + def _38Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _38CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[5], "Location", self.player): + return True +############################################################################## + def _42Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Large Spring Ball", "Key"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Large Spring Ball", "Key"}, self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _44Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state)) + elif self.game_logic == "Normal": + return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player) + else: + return state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Arrow Wheel", "Bucket", "Key"}, self.player) + + def _44Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _44CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[6], "Location", self.player): + return True +######################################################################################################## + + def _46Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Key", "Large Spring Ball"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Key", "Large Spring Ball"}, self.player) + else: + return state.has_all({"Key", "Large Spring Ball"}, self.player) + + def _47Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Key", "Large Spring Ball"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Key", "Large Spring Ball"}, self.player) + else: + return state.has_all({"Key", "Large Spring Ball"}, self.player) +############################################################################## + def _48Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return (state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key", "Large Spring Ball"}, self.player)) + elif self.game_logic == "Normal": + return (state.has_all({"Dashed Stairs", "Vanishing Arrow Wheel", "Key", "Large Spring Ball"}, self.player)) + else: + return (state.has_all({"Key", "Large Spring Ball"}, self.player)) + + def _48Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return (state.has("Egg Capacity Upgrade", self.player, 3)) + elif self.game_logic == "Normal": + return (state.has("Egg Capacity Upgrade", self.player, 2)) + else: + return (state.has("Egg Capacity Upgrade", self.player, 1)) + + def _48CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[7], "Location", self.player): + return True +###################################################################################################################### + def _51Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Key", self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _54Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return (state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, self.player)) + elif self.game_logic == "Normal": + return (state.has_all({"Dashed Stairs", "Platform Ghost", "Dashed Platform"}, self.player)) + else: + return (state.has_all({"Dashed Stairs", "Platform Ghost"}, self.player)) + + def _54Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return (state.has("Egg Capacity Upgrade", self.player, 2) and state.has("Egg Plant", self.player)) + elif self.game_logic == "Normal": + return ((state.has("Egg Capacity Upgrade", self.player, 1) and state.has("Egg Plant", self.player)) or (state.has("Egg Capacity Upgrade", self.player, 5) and self.has_midring(state))) + else: + return ((state.has("Egg Plant", self.player)) or (state.has("Egg Capacity Upgrade", self.player, 3) and self.has_midring(state))) + + def _54CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[8], "Location", self.player): + return True +################################################################################################### + def _58Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player) + else: + return state.has_all({"Arrow Wheel", "Large Spring Ball"}, self.player) + + def _58Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return True + elif self.game_logic == "Normal": + return True + else: + return True + + def _58CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[9], "Location", self.player): + return True +############################################################################## + def _61Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Dashed Platform", "Key", "Beanstalk"}, self.player) + elif self.game_logic == "Normal": + return state.has_all({"Dashed Platform", "Key", "Beanstalk"}, self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _64Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Spring Ball", "Large Spring Ball", "Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 3) or self.combat_item(state)) + elif self.game_logic == "Normal": + return state.has_all({"Large Spring Ball", "Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 2) or self.combat_item(state)) + else: + return state.has_all({"Egg Plant", "Key"}, self.player) and (state.has("Egg Capacity Upgrade", self.player, 1) or self.combat_item(state)) + + def _64Boss(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Egg Plant", self.player) + elif self.game_logic == "Normal": + return state.has("Egg Plant", self.player) + else: + return True + + def _64CanFightBoss(self, state: CollectionState) -> bool: + if state.can_reach(self.boss_order[10], "Location", self.player): + return True +############################################################################## + def _67Game(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has("Key", self.player) + elif self.game_logic == "Normal": + return state.has("Key", self.player) + else: + return state.has("Key", self.player) +############################################################################## + def _68Clear(self, state: CollectionState) -> bool: + if self.game_logic == "Easy": + return state.has_all({"Helicopter Morph", "Egg Plant", "Giant Eggs"}, self.player) and self._68Route(state) + elif self.game_logic == "Normal": + return state.has_all({"Helicopter Morph", "Egg Plant", "Giant Eggs"}, self.player) and self._68Route(state) + else: + return state.has_all({"Helicopter Morph", "Giant Eggs"}, self.player) and self._68Route(state) diff --git a/worlds/yoshisisland/setup_bosses.py b/worlds/yoshisisland/setup_bosses.py new file mode 100644 index 000000000000..bbefdd31a05c --- /dev/null +++ b/worlds/yoshisisland/setup_bosses.py @@ -0,0 +1,19 @@ +from BaseClasses import CollectionState +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from . import YoshisIslandWorld + + +class BossReqs: + player: int + + def __init__(self, world: "YoshisIslandWorld") -> None: + self.player = world.player + self.castle_unlock = world.options.castle_open_condition.value + self.boss_unlock = world.options.castle_clear_condition.value + + def castle_access(self, state: CollectionState) -> bool: + return state.has("Boss Clear", self.player, self.castle_unlock) + + def castle_clear(self, state: CollectionState) -> bool: + return state.has("Boss Clear", self.player, self.boss_unlock) diff --git a/worlds/yoshisisland/setup_game.py b/worlds/yoshisisland/setup_game.py new file mode 100644 index 000000000000..000420a95b07 --- /dev/null +++ b/worlds/yoshisisland/setup_game.py @@ -0,0 +1,460 @@ +import struct +from typing import TYPE_CHECKING + +from .Options import YoshiColors, BabySound, LevelShuffle + +if TYPE_CHECKING: + from . import YoshisIslandWorld + + +def setup_gamevars(world: "YoshisIslandWorld") -> None: + if world.options.luigi_pieces_in_pool < world.options.luigi_pieces_required: + world.options.luigi_pieces_in_pool.value = world.random.randint(world.options.luigi_pieces_required.value, 100) + world.starting_lives = struct.pack("H", world.options.starting_lives) + + world.level_colors = [] + world.color_order = [] + for i in range(72): + world.level_colors.append(world.random.randint(0, 7)) + if world.options.yoshi_colors == YoshiColors.option_singularity: + singularity_color = world.options.yoshi_singularity_color.value + for i in range(len(world.level_colors)): + world.level_colors[i] = singularity_color + elif world.options.yoshi_colors == YoshiColors.option_random_order: + world.leader_color = world.random.randint(0, 7) + for i in range(7): + world.color_order.append(world.random.randint(0, 7)) + + bonus_valid = [0x00, 0x02, 0x04, 0x06, 0x08, 0x0A] + + world.world_bonus = [] + for i in range(12): + world.world_bonus.append(world.random.choice(bonus_valid)) + + safe_baby_sounds = [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, + 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, + 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, + 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, + 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x52, 0x53, 0x54, 0x55, 0x56, + 0x57, 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, 0x62, + 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, + 0x73, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8A, 0x8B, + 0x8C, 0x8D, 0x8E, 0x8F, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F, 0xA0, 0xA1, 0xA2] + + if world.options.baby_mario_sound == BabySound.option_random_sound_effect: + world.baby_mario_sfx = world.random.choice(safe_baby_sounds) + elif world.options.baby_mario_sound == BabySound.option_disabled: + world.baby_mario_sfx = 0x42 + else: + world.baby_mario_sfx = 0x44 + + boss_list = ["Burt The Bashful's Boss Room", "Salvo The Slime's Boss Room", + "Bigger Boo's Boss Room", "Roger The Ghost's Boss Room", + "Prince Froggy's Boss Room", "Naval Piranha's Boss Room", + "Marching Milde's Boss Room", "Hookbill The Koopa's Boss Room", + "Sluggy The Unshaven's Boss Room", "Raphael The Raven's Boss Room", + "Tap-Tap The Red Nose's Boss Room"] + + world.boss_order = [] + + if world.options.boss_shuffle: + world.random.shuffle(boss_list) + world.boss_order = boss_list + + burt_pointers = [0x3D, 0x05, 0x63, 0x00] + slime_pointers = [0x70, 0x04, 0x78, 0x00] + boo_pointers = [0x74, 0xBB, 0x7A, 0x00] + pot_pointers = [0xCF, 0x04, 0x4D, 0x00] + frog_pointers = [0xBF, 0x12, 0x62, 0x04] + plant_pointers = [0x7F, 0x0D, 0x42, 0x00] + milde_pointers = [0x82, 0x06, 0x64, 0x00] + koop_pointers = [0x86, 0x0D, 0x78, 0x00] + slug_pointers = [0x8A, 0x09, 0x7A, 0x00] + raph_pointers = [0xC4, 0x03, 0x4B, 0x05] + tap_pointers = [0xCC, 0x49, 0x64, 0x02] + + boss_data_list = [ + burt_pointers, + slime_pointers, + boo_pointers, + pot_pointers, + frog_pointers, + plant_pointers, + milde_pointers, + koop_pointers, + slug_pointers, + raph_pointers, + tap_pointers + ] + + boss_levels = [0x03, 0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F] + + boss_room_idlist = { + "Burt The Bashful's Boss Room": 0, + "Salvo The Slime's Boss Room": 1, + "Bigger Boo's Boss Room": 2, + "Roger The Ghost's Boss Room": 3, + "Prince Froggy's Boss Room": 4, + "Naval Piranha's Boss Room": 5, + "Marching Milde's Boss Room": 6, + "Hookbill The Koopa's Boss Room": 7, + "Sluggy The Unshaven's Boss Room": 8, + "Raphael The Raven's Boss Room": 9, + "Tap-Tap The Red Nose's Boss Room": 10, + } + + boss_check_list = { + "Burt The Bashful's Boss Room": "Burt The Bashful Defeated", + "Salvo The Slime's Boss Room": "Salvo The Slime Defeated", + "Bigger Boo's Boss Room": "Bigger Boo Defeated", + "Roger The Ghost's Boss Room": "Roger The Ghost Defeated", + "Prince Froggy's Boss Room": "Prince Froggy Defeated", + "Naval Piranha's Boss Room": "Naval Piranha Defeated", + "Marching Milde's Boss Room": "Marching Milde Defeated", + "Hookbill The Koopa's Boss Room": "Hookbill The Koopa Defeated", + "Sluggy The Unshaven's Boss Room": "Sluggy The Unshaven Defeated", + "Raphael The Raven's Boss Room": "Raphael The Raven Defeated", + "Tap-Tap The Red Nose's Boss Room": "Tap-Tap The Red Nose Defeated", + } + + world.boss_room_id = [boss_room_idlist[roomnum] for roomnum in world.boss_order] + world.tap_tap_room = boss_levels[world.boss_room_id.index(10)] + world.boss_ap_loc = [boss_check_list[roomnum] for roomnum in world.boss_order] + + world.boss_burt_data = boss_data_list[world.boss_room_id[0]] + + world.boss_slime_data = boss_data_list[world.boss_room_id[1]] + + world.boss_boo_data = boss_data_list[world.boss_room_id[2]] + + world.boss_pot_data = boss_data_list[world.boss_room_id[3]] + + world.boss_frog_data = boss_data_list[world.boss_room_id[4]] + + world.boss_plant_data = boss_data_list[world.boss_room_id[5]] + + world.boss_milde_data = boss_data_list[world.boss_room_id[6]] + + world.boss_koop_data = boss_data_list[world.boss_room_id[7]] + + world.boss_slug_data = boss_data_list[world.boss_room_id[8]] + + world.boss_raph_data = boss_data_list[world.boss_room_id[9]] + + world.boss_tap_data = boss_data_list[world.boss_room_id[10]] + + world.global_level_list = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42] + level_id_list = { + 0x00: "1-1", + 0x01: "1-2", + 0x02: "1-3", + 0x03: "1-4", + 0x04: "1-5", + 0x05: "1-6", + 0x06: "1-7", + 0x07: "1-8", + 0x0C: "2-1", + 0x0D: "2-2", + 0x0E: "2-3", + 0x0F: "2-4", + 0x10: "2-5", + 0x11: "2-6", + 0x12: "2-7", + 0x13: "2-8", + 0x18: "3-1", + 0x19: "3-2", + 0x1A: "3-3", + 0x1B: "3-4", + 0x1C: "3-5", + 0x1D: "3-6", + 0x1E: "3-7", + 0x1F: "3-8", + 0x24: "4-1", + 0x25: "4-2", + 0x26: "4-3", + 0x27: "4-4", + 0x28: "4-5", + 0x29: "4-6", + 0x2A: "4-7", + 0x2B: "4-8", + 0x30: "5-1", + 0x31: "5-2", + 0x32: "5-3", + 0x33: "5-4", + 0x34: "5-5", + 0x35: "5-6", + 0x36: "5-7", + 0x37: "5-8", + 0x3C: "6-1", + 0x3D: "6-2", + 0x3E: "6-3", + 0x3F: "6-4", + 0x40: "6-5", + 0x41: "6-6", + 0x42: "6-7" + } + + level_names = { + 0x00: "Make Eggs, Throw Eggs", + 0x01: "Watch Out Below!", + 0x02: "The Cave Of Chomp Rock", + 0x03: "Burt The Bashful's Fort", + 0x04: "Hop! Hop! Donut Lifts", + 0x05: "Shy-Guys On Stilts", + 0x06: "Touch Fuzzy Get Dizzy", + 0x07: "Salvo The Slime's Castle", + 0x0C: "Visit Koopa And Para-Koopa", + 0x0D: "The Baseball Boys", + 0x0E: "What's Gusty Taste Like?", + 0x0F: "Bigger Boo's Fort", + 0x10: "Watch Out For Lakitu", + 0x11: "The Cave Of The Mystery Maze", + 0x12: "Lakitu's Wall", + 0x13: "The Potted Ghost's Castle", + 0x18: "Welcome To Monkey World!", + 0x19: "Jungle Rhythm...", + 0x1A: "Nep-Enuts' Domain", + 0x1B: "Prince Froggy's Fort", + 0x1C: "Jammin' Through The Trees", + 0x1D: "The Cave Of Harry Hedgehog", + 0x1E: "Monkeys' Favorite Lake", + 0x1F: "Naval Piranha's Castle", + 0x24: "GO! GO! MARIO!!", + 0x25: "The Cave Of The Lakitus", + 0x26: "Don't Look Back!", + 0x27: "Marching Milde's Fort", + 0x28: "Chomp Rock Zone", + 0x29: "Lake Shore Paradise", + 0x2A: "Ride Like The Wind", + 0x2B: "Hookbill The Koopa's Castle", + 0x30: "BLIZZARD!!!", + 0x31: "Ride The Ski Lifts", + 0x32: "Danger - Icy Conditions Ahead", + 0x33: "Sluggy The Unshaven's Fort", + 0x34: "Goonie Rides!", + 0x35: "Welcome To Cloud World", + 0x36: "Shifting Platforms Ahead", + 0x37: "Raphael The Raven's Castle", + 0x3C: "Scary Skeleton Goonies!", + 0x3D: "The Cave Of The Bandits", + 0x3E: "Beware The Spinning Logs", + 0x3F: "Tap-Tap The Red Nose's Fort", + 0x40: "The Very Loooooong Cave", + 0x41: "The Deep, Underground Maze", + 0x42: "KEEP MOVING!!!!" + } + + world_1_offsets = [0x01, 0x00, 0x00, 0x00, 0x00, 0x00] + world_2_offsets = [0x01, 0x01, 0x00, 0x00, 0x00, 0x00] + world_3_offsets = [0x01, 0x01, 0x01, 0x00, 0x00, 0x00] + world_4_offsets = [0x01, 0x01, 0x01, 0x01, 0x00, 0x00] + world_5_offsets = [0x01, 0x01, 0x01, 0x01, 0x01, 0x00] + easy_start_lv = [0x02, 0x04, 0x06, 0x0E, 0x10, 0x18, 0x1C, 0x28, + 0x30, 0x31, 0x35, 0x36, 0x3E, 0x40, 0x42] + norm_start_lv = [0x00, 0x01, 0x02, 0x04, 0x06, 0x0E, 0x10, 0x12, 0x18, 0x1A, + 0x1C, 0x1E, 0x28, 0x30, 0x31, 0x34, 0x35, 0x36, 0x3D, 0x3E, 0x40, 0x42] + hard_start_lv = [0x00, 0x01, 0x02, 0x04, 0x06, 0x0D, 0x0E, 0x10, 0x11, 0x12, 0x18, 0x1A, 0x1C, + 0x1E, 0x24, 0x25, 0x26, 0x28, 0x29, 0x30, 0x31, 0x34, 0x35, 0x36, 0x3D, 0x3E, + 0x40, 0x42] + diff_index = [easy_start_lv, norm_start_lv, hard_start_lv] + diff_level = diff_index[world.options.stage_logic.value] + boss_lv = [0x03, 0x07, 0x0F, 0x13, 0x1B, 0x1F, 0x27, 0x2B, 0x33, 0x37, 0x3F] + world.world_start_lv = [0, 8, 16, 24, 32, 40] + if not world.options.shuffle_midrings: + easy_start_lv.extend([0x1A, 0x24, 0x34]) + norm_start_lv.extend([0x24, 0x3C]) + hard_start_lv.extend([0x1D, 0x3C]) + + if world.options.level_shuffle != LevelShuffle.option_bosses_guranteed: + hard_start_lv.extend([0x07, 0x1B, 0x1F, 0x2B, 0x33, 0x37]) + if not world.options.shuffle_midrings: + easy_start_lv.extend([0x1B]) + norm_start_lv.extend([0x1B, 0x2B, 0x37]) + + starting_level = world.random.choice(diff_level) + + starting_level_entrance = world.world_start_lv[world.options.starting_world.value] + if world.options.level_shuffle: + world.global_level_list.remove(starting_level) + world.random.shuffle(world.global_level_list) + if world.options.level_shuffle == LevelShuffle.option_bosses_guranteed: + for i in range(11): + world.global_level_list = [item for item in world.global_level_list + if item not in boss_lv] + world.random.shuffle(boss_lv) + + world.global_level_list.insert(3 - world_1_offsets[world.options.starting_world.value], boss_lv[0]) # 1 if starting world is 1, 0 otherwise + world.global_level_list.insert(7 - world_1_offsets[world.options.starting_world.value], boss_lv[1]) + world.global_level_list.insert(11 - world_2_offsets[world.options.starting_world.value], boss_lv[2]) + world.global_level_list.insert(15 - world_2_offsets[world.options.starting_world.value], boss_lv[3]) + world.global_level_list.insert(19 - world_3_offsets[world.options.starting_world.value], boss_lv[4]) + world.global_level_list.insert(23 - world_3_offsets[world.options.starting_world.value], boss_lv[5]) + world.global_level_list.insert(27 - world_4_offsets[world.options.starting_world.value], boss_lv[6]) + world.global_level_list.insert(31 - world_4_offsets[world.options.starting_world.value], boss_lv[7]) + world.global_level_list.insert(35 - world_5_offsets[world.options.starting_world.value], boss_lv[8]) + world.global_level_list.insert(39 - world_5_offsets[world.options.starting_world.value], boss_lv[9]) + world.global_level_list.insert(43 - 1, boss_lv[10]) + world.global_level_list.insert(starting_level_entrance, starting_level) + world.level_location_list = [level_id_list[LevelID] for LevelID in world.global_level_list] + world.level_name_list = [level_names[LevelID] for LevelID in world.global_level_list] + + level_panel_dict = { + 0x00: [0x04, 0x04, 0x53], + 0x01: [0x20, 0x04, 0x53], + 0x02: [0x3C, 0x04, 0x53], + 0x03: [0x58, 0x04, 0x53], + 0x04: [0x74, 0x04, 0x53], + 0x05: [0x90, 0x04, 0x53], + 0x06: [0xAC, 0x04, 0x53], + 0x07: [0xC8, 0x04, 0x53], + 0x0C: [0x04, 0x24, 0x53], + 0x0D: [0x20, 0x24, 0x53], + 0x0E: [0x3C, 0x24, 0x53], + 0x0F: [0x58, 0x24, 0x53], + 0x10: [0x74, 0x24, 0x53], + 0x11: [0x90, 0x24, 0x53], + 0x12: [0xAC, 0x24, 0x53], + 0x13: [0xC8, 0x24, 0x53], + 0x18: [0x04, 0x44, 0x53], + 0x19: [0x20, 0x44, 0x53], + 0x1A: [0x3C, 0x44, 0x53], + 0x1B: [0x58, 0x44, 0x53], + 0x1C: [0x74, 0x44, 0x53], + 0x1D: [0x90, 0x44, 0x53], + 0x1E: [0xAC, 0x44, 0x53], + 0x1F: [0xC8, 0x44, 0x53], + 0x24: [0x04, 0x64, 0x53], + 0x25: [0x20, 0x64, 0x53], + 0x26: [0x3C, 0x64, 0x53], + 0x27: [0x58, 0x64, 0x53], + 0x28: [0x74, 0x64, 0x53], + 0x29: [0x90, 0x64, 0x53], + 0x2A: [0xAC, 0x64, 0x53], + 0x2B: [0xC8, 0x64, 0x53], + 0x30: [0x04, 0x04, 0x53], + 0x31: [0x20, 0x04, 0x53], + 0x32: [0x3C, 0x04, 0x53], + 0x33: [0x58, 0x04, 0x53], + 0x34: [0x74, 0x04, 0x53], + 0x35: [0x90, 0x04, 0x53], + 0x36: [0xAC, 0x04, 0x53], + 0x37: [0xC8, 0x04, 0x53], + 0x3C: [0x04, 0x24, 0x53], + 0x3D: [0x20, 0x24, 0x53], + 0x3E: [0x3C, 0x24, 0x53], + 0x3F: [0x58, 0x24, 0x53], + 0x40: [0x74, 0x24, 0x53], + 0x41: [0x90, 0x24, 0x53], + 0x42: [0xAC, 0x24, 0x53], + } + panel_palette_1 = [0x00, 0x03, 0x04, 0x05, 0x0C, 0x10, 0x12, 0x13, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, + 0x24, 0x26, 0x27, 0x29, 0x2A, 0x2B, 0x30, 0x32, 0x34, + 0x35, 0x37, 0x3C, 0x3D, 0x40, 0x41] # 000C + panel_palette_2 = [0x01, 0x02, 0x06, 0x07, 0x0D, 0x0E, 0x0F, 0x11, 0x18, 0x1E, 0x1F, 0x25, 0x28, + 0x31, 0x33, 0x36, 0x3E, 0x3F, 0x42] # 0010 + + stage_number = 0 + world_number = 1 + for i in range(47): + stage_number += 1 + if stage_number >= 9: + world_number += 1 + stage_number = 1 + for _ in range(3): + setattr(world, f"Stage{world_number}{stage_number}StageGFX", + level_panel_dict[world.global_level_list[i]]) + + world.level_gfx_table = [] + world.palette_panel_list = [] + + for i in range(47): + if world.global_level_list[i] >= 0x30: + world.level_gfx_table.append(0x15) + else: + world.level_gfx_table.append(0x11) + + if world.global_level_list[i] in panel_palette_1: + world.palette_panel_list.extend([0x00, 0x0C]) + elif world.global_level_list[i] in panel_palette_2: + world.palette_panel_list.extend([0x00, 0x10]) + + world.palette_panel_list[16:16] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18] + world.palette_panel_list[40:40] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18] + world.palette_panel_list[64:64] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18] + world.palette_panel_list[88:88] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18] + world.palette_panel_list[112:112] = [0x00, 0x0c, 0x00, 0x0c, 0x00, 0x18, 0x00, 0x18] + + world.level_gfx_table.insert(8, 0x15) + world.level_gfx_table.insert(8, 0x15) + world.level_gfx_table.insert(8, 0x15) + world.level_gfx_table.insert(8, 0x11) + + world.level_gfx_table.insert(20, 0x15) + world.level_gfx_table.insert(20, 0x15) + world.level_gfx_table.insert(20, 0x15) + world.level_gfx_table.insert(20, 0x11) + + world.level_gfx_table.insert(32, 0x15) + world.level_gfx_table.insert(32, 0x15) + world.level_gfx_table.insert(32, 0x15) + world.level_gfx_table.insert(32, 0x11) + + world.level_gfx_table.insert(44, 0x15) + world.level_gfx_table.insert(44, 0x15) + world.level_gfx_table.insert(44, 0x15) + world.level_gfx_table.insert(44, 0x11) + + world.level_gfx_table.insert(56, 0x15) + world.level_gfx_table.insert(56, 0x15) + world.level_gfx_table.insert(56, 0x15) + world.level_gfx_table.insert(56, 0x15) + + castle_door_dict = { + 0: [0xB8, 0x05, 0x77, 0x00], + 1: [0xB8, 0x05, 0x77, 0x00], + 2: [0xC6, 0x07, 0x7A, 0x00], + 3: [0xCD, 0x05, 0x5B, 0x00], + 4: [0xD3, 0x00, 0x77, 0x06], + 5: [0xB8, 0x05, 0x77, 0x00], + } + + world.castle_door = castle_door_dict[world.options.bowser_door_mode.value] + + world.world_1_stages = world.global_level_list[0:8] + world.world_2_stages = world.global_level_list[8:16] + world.world_3_stages = world.global_level_list[16:24] + world.world_4_stages = world.global_level_list[24:32] + world.world_5_stages = world.global_level_list[32:40] + world.world_6_stages = world.global_level_list[40:47] + + world.world_1_stages.extend([0x08, 0x09]) + world.world_2_stages.extend([0x14, 0x15]) + world.world_3_stages.extend([0x20, 0x21]) + world.world_4_stages.extend([0x2C, 0x2D]) + world.world_5_stages.extend([0x38, 0x39]) + world.world_6_stages.extend([0x43, 0x44, 0x45]) + + bowser_text_table = { + 0: [0xDE, 0xEE, 0xDC, 0xDC, 0xE5], # Gween + 1: [0xE7, 0xE0, 0xE5, 0xE2, 0xD0], # Pink + 3: [0xEB, 0xDF, 0xF0, 0xD8, 0xE5], # Thyan + 2: [0xF0, 0xDC, 0xEE, 0xEE, 0xE6], # Yewow + 4: [0xE7, 0xEC, 0xDF, 0xE7, 0xE3], # puhpl + 5: [0xD9, 0xEE, 0xE6, 0xEE, 0xE5], # Bwown + 6: [0xEE, 0xDC, 0xDB, 0xD0, 0xD0], # Wed + 7: [0xD9, 0xEE, 0xEC, 0xDC, 0xD0], # Bwue + } + + if world.options.yoshi_colors == YoshiColors.option_random_order: + world.bowser_text = bowser_text_table[world.leader_color] + else: + world.bowser_text = bowser_text_table[world.level_colors[67]] From bdd498db2321417374d572bff8beede083fef2b2 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:36:27 -0500 Subject: [PATCH 25/74] ALTTP: Fix #2290's crashes (#2973) --- worlds/alttp/ItemPool.py | 38 +++++++++++++++++++------------------- worlds/alttp/__init__.py | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 3929342aa56f..438c6226bc38 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -508,7 +508,7 @@ def cut_item(items, item_to_cut, minimum_items): multiworld.itempool += items if multiworld.retro_caves[player]: - set_up_take_anys(multiworld, player) # depends on world.itempool to be set + set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set take_any_locations = { @@ -528,30 +528,30 @@ def cut_item(items, item_to_cut, minimum_items): take_any_locations.sort() -def set_up_take_anys(world, player): +def set_up_take_anys(multiworld, world, player): # these are references, do not modify these lists in-place - if world.mode[player] == 'inverted': + if multiworld.mode[player] == 'inverted': take_any_locs = take_any_locations_inverted else: take_any_locs = take_any_locations - regions = world.random.sample(take_any_locs, 5) + regions = multiworld.random.sample(take_any_locs, 5) - old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, world) - world.regions.append(old_man_take_any) + old_man_take_any = LTTPRegion("Old Man Sword Cave", LTTPRegionType.Cave, 'the sword cave', player, multiworld) + multiworld.regions.append(old_man_take_any) reg = regions.pop() - entrance = world.get_region(reg, player).entrances[0] - connect_entrance(world, entrance.name, old_man_take_any.name, player) + entrance = multiworld.get_region(reg, player).entrances[0] + connect_entrance(multiworld, entrance.name, old_man_take_any.name, player) entrance.target = 0x58 old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots) - world.shops.append(old_man_take_any.shop) + multiworld.shops.append(old_man_take_any.shop) - swords = [item for item in world.itempool if item.player == player and item.type == 'Sword'] + swords = [item for item in multiworld.itempool if item.player == player and item.type == 'Sword'] if swords: - sword = world.random.choice(swords) - world.itempool.remove(sword) - world.itempool.append(item_factory('Rupees (20)', world)) + sword = multiworld.random.choice(swords) + multiworld.itempool.remove(sword) + multiworld.itempool.append(item_factory('Rupees (20)', world)) old_man_take_any.shop.add_inventory(0, sword.name, 0, 0) loc_name = "Old Man Sword Cave" location = ALttPLocation(player, loc_name, shop_table_by_location[loc_name], parent=old_man_take_any) @@ -562,16 +562,16 @@ def set_up_take_anys(world, player): old_man_take_any.shop.add_inventory(0, 'Rupees (300)', 0, 0) for num in range(4): - take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, world) - world.regions.append(take_any) + take_any = LTTPRegion("Take-Any #{}".format(num+1), LTTPRegionType.Cave, 'a cave of choice', player, multiworld) + multiworld.regions.append(take_any) - target, room_id = world.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)]) + target, room_id = multiworld.random.choice([(0x58, 0x0112), (0x60, 0x010F), (0x46, 0x011F)]) reg = regions.pop() - entrance = world.get_region(reg, player).entrances[0] - connect_entrance(world, entrance.name, take_any.name, player) + entrance = multiworld.get_region(reg, player).entrances[0] + connect_entrance(multiworld, entrance.name, take_any.name, player) entrance.target = target take_any.shop = TakeAny(take_any, room_id, 0xE3, True, True, total_shop_slots + num + 1) - world.shops.append(take_any.shop) + multiworld.shops.append(take_any.shop) take_any.shop.add_inventory(0, 'Blue Potion', 0, 0) take_any.shop.add_inventory(1, 'Boss Heart Container', 0, 0) location = ALttPLocation(player, take_any.name, shop_table_by_location[take_any.name], parent=take_any) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 63c53007d861..a3b1dfa65880 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -649,7 +649,7 @@ def stage_fill_hook(cls, multiworld, progitempool, usefulitempool, filleritempoo if not multiworld.ganonstower_vanilla[player] or \ world.options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', "no_logic"}: pass - elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or world.players == 1): + elif 'triforce_hunt' in world.options.goal.current_key and ('local' in world.options.goal.current_key or multiworld.players == 1): trash_counts[player] = multiworld.random.randint(world.options.crystals_needed_for_gt * 2, world.options.crystals_needed_for_gt * 4) else: From 7e904a1c78c91fb502706fe030a1f1765f734de4 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Sun, 24 Mar 2024 21:51:46 +0100 Subject: [PATCH 26/74] SC2: Fix Kerrigan presence resolving when deciding which races should be used (#2978) --- worlds/sc2/PoolFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sc2/PoolFilter.py b/worlds/sc2/PoolFilter.py index 5f8151ed399f..068c62314923 100644 --- a/worlds/sc2/PoolFilter.py +++ b/worlds/sc2/PoolFilter.py @@ -560,7 +560,7 @@ def filter_items(world: World, mission_req_table: Dict[SC2Campaign, Dict[str, Mi def get_used_races(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], world: World) -> Set[SC2Race]: grant_story_tech = get_option_value(world, "grant_story_tech") take_over_ai_allies = get_option_value(world, "take_over_ai_allies") - kerrigan_presence = get_option_value(world, "kerrigan_presence") \ + kerrigan_presence = get_option_value(world, "kerrigan_presence") in kerrigan_unit_available \ and SC2Campaign.HOTS in get_enabled_campaigns(world) missions = missions_in_mission_table(mission_req_table) @@ -572,7 +572,7 @@ def get_used_races(mission_req_table: Dict[SC2Campaign, Dict[str, MissionInfo]], if SC2Mission.ENEMY_WITHIN in missions: # Zerg units need to be unlocked races.add(SC2Race.ZERG) - if kerrigan_presence in kerrigan_unit_available \ + if kerrigan_presence \ and not missions.isdisjoint({SC2Mission.BACK_IN_THE_SADDLE, SC2Mission.SUPREME, SC2Mission.CONVICTION, SC2Mission.THE_INFINITE_CYCLE}): # You need some Kerrigan abilities (they're granted if Kerriganless or story tech granted) races.add(SC2Race.ZERG) From 2b24539ea5b387a3b62063c8177c373e2e3f8389 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Sun, 24 Mar 2024 21:52:16 +0100 Subject: [PATCH 27/74] SC2 Tracker: Use level tinting to let the player know which level he has of Replenishable Magazine (#2986) --- WebHostLib/templates/tracker__Starcraft2.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/tracker__Starcraft2.html b/WebHostLib/templates/tracker__Starcraft2.html index b4252df2504d..1e9f6e276c24 100644 --- a/WebHostLib/templates/tracker__Starcraft2.html +++ b/WebHostLib/templates/tracker__Starcraft2.html @@ -180,7 +180,7 @@

{{ player_name }}'s Starcraft 2 Tracker

{{ sc2_icon('Nano Projector (Medic)') }} {{ sc2_icon('Vulture') }} - {{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }} + {{ sc2_progressive_icon_with_custom_name('Progressive Replenishable Magazine (Vulture)', replenishable_magazine_vulture_url, replenishable_magazine_vulture_name) }} {{ sc2_icon('Ion Thrusters (Vulture)') }} {{ sc2_icon('Auto Launchers (Vulture)') }} {{ sc2_icon('Auto-Repair (Vulture)') }} From 36c83073ad8c2ae1912d390ee3976ba0e2eb3f4a Mon Sep 17 00:00:00 2001 From: Salzkorn Date: Sun, 24 Mar 2024 21:52:41 +0100 Subject: [PATCH 28/74] SC2 Tracker: Fix grouped items pointing at wrong item IDs (#2992) --- WebHostLib/tracker.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index c2fdab0ed074..82e6ffc37e8d 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -2333,12 +2333,12 @@ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) "Progressive Zerg Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET, "Progressive Zerg Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET, "Progressive Zerg Flyer Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Zerg Weapon/Armor Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, - "Progressive Protoss Weapon Upgrade": 105 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Armor Upgrade": 106 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Ground Upgrade": 107 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Air Upgrade": 108 + SC2HOTS_ITEM_ID_OFFSET, - "Progressive Protoss Weapon/Armor Upgrade": 109 + SC2WOL_ITEM_ID_OFFSET, + "Progressive Zerg Weapon/Armor Upgrade": 109 + SC2HOTS_ITEM_ID_OFFSET, + "Progressive Protoss Weapon Upgrade": 105 + SC2LOTV_ITEM_ID_OFFSET, + "Progressive Protoss Armor Upgrade": 106 + SC2LOTV_ITEM_ID_OFFSET, + "Progressive Protoss Ground Upgrade": 107 + SC2LOTV_ITEM_ID_OFFSET, + "Progressive Protoss Air Upgrade": 108 + SC2LOTV_ITEM_ID_OFFSET, + "Progressive Protoss Weapon/Armor Upgrade": 109 + SC2LOTV_ITEM_ID_OFFSET, } grouped_item_replacements = { "Progressive Terran Weapon Upgrade": ["Progressive Terran Infantry Weapon", From c0368ae0d48b4b2807c5238aeb7b14937282fc3e Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:53:20 -0700 Subject: [PATCH 29/74] SC2: Fixed missing upgrade from custom tracker (#3013) --- WebHostLib/templates/tracker__Starcraft2.html | 9 +++++---- WebHostLib/tracker.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/WebHostLib/templates/tracker__Starcraft2.html b/WebHostLib/templates/tracker__Starcraft2.html index 1e9f6e276c24..089e7d6c2239 100644 --- a/WebHostLib/templates/tracker__Starcraft2.html +++ b/WebHostLib/templates/tracker__Starcraft2.html @@ -106,7 +106,7 @@

{{ player_name }}'s Starcraft 2 Tracker

{{ sc2_icon('Neosteel Bunker (Bunker)') }} {{ sc2_icon('Shrike Turret (Bunker)') }} {{ sc2_icon('Fortified Bunker (Bunker)') }} - + {{ sc2_icon('Missile Turret') }} {{ sc2_icon('Titanium Housing (Missile Turret)') }} {{ sc2_icon('Hellstorm Batteries (Missile Turret)') }} @@ -121,12 +121,13 @@

{{ player_name }}'s Starcraft 2 Tracker

{{ sc2_icon('Planetary Fortress') }} {{ sc2_progressive_icon_with_custom_name('Progressive Augmented Thrusters (Planetary Fortress)', augmented_thrusters_planetary_fortress_url, augmented_thrusters_planetary_fortress_name) }} {{ sc2_icon('Advanced Targeting (Planetary Fortress)') }} + + {{ sc2_icon('Micro-Filtering') }} + {{ sc2_icon('Automated Refinery') }} {{ sc2_icon('Advanced Construction (SCV)') }} {{ sc2_icon('Dual-Fusion Welders (SCV)') }} - - {{ sc2_icon('Micro-Filtering') }} - {{ sc2_icon('Automated Refinery') }} + {{ sc2_icon('Hostile Environment Adaptation (SCV)') }} {{ sc2_icon('Sensor Tower') }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 82e6ffc37e8d..95bca57493c2 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1606,6 +1606,7 @@ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) "Hellstorm Batteries (Missile Turret)": github_icon_base_url + "blizzard/btn-ability-stetmann-corruptormissilebarrage.png", "Advanced Construction (SCV)": github_icon_base_url + "blizzard/btn-ability-mengsk-trooper-advancedconstruction.png", "Dual-Fusion Welders (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-scvdoublerepair.png", + "Hostile Environment Adaptation (SCV)": github_icon_base_url + "blizzard/btn-upgrade-swann-hellarmor.png", "Fire-Suppression System Level 1": organics_icon_base_url + "Fire-SuppressionSystem.png", "Fire-Suppression System Level 2": github_icon_base_url + "blizzard/btn-upgrade-swann-firesuppressionsystem.png", From bf3856866c5ea385d0ac58014c71addfdc92637e Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 24 Mar 2024 23:53:49 +0300 Subject: [PATCH 30/74] Stardew Valley: presets with some of the new available values for existing settings to make them more accurate (#3014) --- worlds/stardew_valley/presets.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/stardew_valley/presets.py b/worlds/stardew_valley/presets.py index 020b3f49277f..e75eb5c5fcde 100644 --- a/worlds/stardew_valley/presets.py +++ b/worlds/stardew_valley/presets.py @@ -64,7 +64,7 @@ SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, ElevatorProgression.internal_name: ElevatorProgression.option_progressive, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, @@ -102,13 +102,13 @@ FarmType.internal_name: "random", StartingMoney.internal_name: "rich", ProfitMargin.internal_name: 150, - BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundleRandomization.internal_name: BundleRandomization.option_remixed, BundlePrice.internal_name: BundlePrice.option_normal, EntranceRandomization.internal_name: EntranceRandomization.option_non_progression, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, Cropsanity.internal_name: Cropsanity.option_enabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive_cheap, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_cheap, @@ -146,7 +146,7 @@ FarmType.internal_name: "random", StartingMoney.internal_name: "extra", ProfitMargin.internal_name: "normal", - BundleRandomization.internal_name: BundleRandomization.option_thematic, + BundleRandomization.internal_name: BundleRandomization.option_remixed, BundlePrice.internal_name: BundlePrice.option_expensive, EntranceRandomization.internal_name: EntranceRandomization.option_buildings, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, @@ -191,7 +191,7 @@ StartingMoney.internal_name: "vanilla", ProfitMargin.internal_name: "half", BundleRandomization.internal_name: BundleRandomization.option_shuffled, - BundlePrice.internal_name: BundlePrice.option_expensive, + BundlePrice.internal_name: BundlePrice.option_very_expensive, EntranceRandomization.internal_name: EntranceRandomization.option_buildings, SeasonRandomization.internal_name: SeasonRandomization.option_randomized, Cropsanity.internal_name: Cropsanity.option_enabled, @@ -234,13 +234,13 @@ FarmType.internal_name: "random", StartingMoney.internal_name: "filthy rich", ProfitMargin.internal_name: "quadruple", - BundleRandomization.internal_name: BundleRandomization.option_thematic, - BundlePrice.internal_name: BundlePrice.option_very_cheap, + BundleRandomization.internal_name: BundleRandomization.option_remixed, + BundlePrice.internal_name: BundlePrice.option_minimum, EntranceRandomization.internal_name: EntranceRandomization.option_disabled, SeasonRandomization.internal_name: SeasonRandomization.option_randomized_not_winter, Cropsanity.internal_name: Cropsanity.option_disabled, BackpackProgression.internal_name: BackpackProgression.option_early_progressive, - ToolProgression.internal_name: ToolProgression.option_progressive, + ToolProgression.internal_name: ToolProgression.option_progressive_very_cheap, ElevatorProgression.internal_name: ElevatorProgression.option_progressive_from_previous_floor, SkillProgression.internal_name: SkillProgression.option_progressive, BuildingProgression.internal_name: BuildingProgression.option_progressive_very_cheap, From ea47b90367b4a220c346d8057f3aeb4207d226a1 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Tue, 26 Mar 2024 09:25:41 -0400 Subject: [PATCH 31/74] TUNIC: You can grapple down here without the ladder, neat (#3019) --- worlds/tunic/er_rules.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index fdfd064561fe..96a3c39ad283 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -602,8 +602,8 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re regions["Library Exterior Ladder Region"].connect( connecting_region=regions["Library Exterior Tree Region"], rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks) - and state.has_any({grapple, laurels}, player) - and has_ladder("Ladders in Library", state, player, options)) + and (state.has(grapple, player) or (state.has(laurels, player) + and has_ladder("Ladders in Library", state, player, options)))) regions["Library Hall Bookshelf"].connect( connecting_region=regions["Library Hall"], From 98ce8f8844fd0c62214a5774609382cf6a6bc829 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Tue, 26 Mar 2024 14:29:25 +0100 Subject: [PATCH 32/74] sm64ex: New Options API and WebHost fix (#2979) --- worlds/sm64ex/Options.py | 69 ++++++++++++++++++++------------------- worlds/sm64ex/Regions.py | 33 ++++++++++--------- worlds/sm64ex/Rules.py | 29 ++++++++-------- worlds/sm64ex/__init__.py | 50 ++++++++++++++-------------- 4 files changed, 92 insertions(+), 89 deletions(-) diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index d9a877df2b37..60ec4bbe13c2 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,5 +1,6 @@ import typing -from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice +from dataclasses import dataclass +from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet from .Items import action_item_table class EnableCoinStars(DefaultOnToggle): @@ -114,35 +115,37 @@ class StrictMoveRequirements(DefaultOnToggle): if Move Randomization is enabled""" display_name = "Strict Move Requirements" -def getMoveRandomizerOption(action: str): - class MoveRandomizerOption(Toggle): - """Mario is unable to perform this action until a corresponding item is picked up. - This option is incompatible with builds using a 'nomoverando' branch.""" - display_name = f"Randomize {action}" - return MoveRandomizerOption - - -sm64_options: typing.Dict[str, type(Option)] = { - "AreaRandomizer": AreaRandomizer, - "BuddyChecks": BuddyChecks, - "ExclamationBoxes": ExclamationBoxes, - "ProgressiveKeys": ProgressiveKeys, - "EnableCoinStars": EnableCoinStars, - "StrictCapRequirements": StrictCapRequirements, - "StrictCannonRequirements": StrictCannonRequirements, - "StrictMoveRequirements": StrictMoveRequirements, - "AmountOfStars": AmountOfStars, - "FirstBowserStarDoorCost": FirstBowserStarDoorCost, - "BasementStarDoorCost": BasementStarDoorCost, - "SecondFloorStarDoorCost": SecondFloorStarDoorCost, - "MIPS1Cost": MIPS1Cost, - "MIPS2Cost": MIPS2Cost, - "StarsToFinish": StarsToFinish, - "death_link": DeathLink, - "CompletionType": CompletionType, -} - -for action in action_item_table: - # HACK: Disable randomization of double jump - if action == 'Double Jump': continue - sm64_options[f"MoveRandomizer{action.replace(' ','')}"] = getMoveRandomizerOption(action) +class EnableMoveRandomizer(Toggle): + """Mario is unable to perform some actions until a corresponding item is picked up. + This option is incompatible with builds using a 'nomoverando' branch. + Specific actions to randomize can be specified in the YAML.""" + display_name = "Enable Move Randomizer" + +class MoveRandomizerActions(OptionSet): + """Which actions to randomize when Move Randomizer is enabled""" + display_name = "Randomized Moves" + # HACK: Disable randomization for double jump + valid_keys = [action for action in action_item_table if action != 'Double Jump'] + default = valid_keys + +@dataclass +class SM64Options(PerGameCommonOptions): + area_rando: AreaRandomizer + buddy_checks: BuddyChecks + exclamation_boxes: ExclamationBoxes + progressive_keys: ProgressiveKeys + enable_coin_stars: EnableCoinStars + enable_move_rando: EnableMoveRandomizer + move_rando_actions: MoveRandomizerActions + strict_cap_requirements: StrictCapRequirements + strict_cannon_requirements: StrictCannonRequirements + strict_move_requirements: StrictMoveRequirements + amount_of_stars: AmountOfStars + first_bowser_star_door_cost: FirstBowserStarDoorCost + basement_star_door_cost: BasementStarDoorCost + second_floor_star_door_cost: SecondFloorStarDoorCost + mips1_cost: MIPS1Cost + mips2_cost: MIPS2Cost + stars_to_finish: StarsToFinish + death_link: DeathLink + completion_type: CompletionType diff --git a/worlds/sm64ex/Regions.py b/worlds/sm64ex/Regions.py index a493281ec3f6..333e2df3a97f 100644 --- a/worlds/sm64ex/Regions.py +++ b/worlds/sm64ex/Regions.py @@ -2,6 +2,7 @@ from enum import Enum from BaseClasses import MultiWorld, Region, Entrance, Location +from .Options import SM64Options from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \ locBBH_table, \ locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \ @@ -78,7 +79,7 @@ class SM64Region(Region): sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level } sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets } -def create_regions(world: MultiWorld, player: int): +def create_regions(world: MultiWorld, options: SM64Options, player: int): regSS = Region("Menu", player, world, "Castle Area") create_default_locs(regSS, locSS_table) world.regions.append(regSS) @@ -88,7 +89,7 @@ def create_regions(world: MultiWorld, player: int): "BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy") bob_island = create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins") regBoB.subregions = [bob_island] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regBoB, "BoB: 100 Coins") regWhomp = create_region("Whomp's Fortress", player, world) @@ -96,7 +97,7 @@ def create_regions(world: MultiWorld, player: int): "WF: Fall onto the Caged Island", "WF: Blast Away the Wall") wf_tower = create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy") regWhomp.subregions = [wf_tower] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regWhomp, "WF: 100 Coins") regJRB = create_region("Jolly Roger Bay", player, world) @@ -104,12 +105,12 @@ def create_regions(world: MultiWorld, player: int): "JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy") jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat") regJRB.subregions = [jrb_upper] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(jrb_upper, "JRB: 100 Coins") regCCM = create_region("Cool, Cool Mountain", player, world) create_default_locs(regCCM, locCCM_table) - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regCCM, "CCM: 100 Coins") regBBH = create_region("Big Boo's Haunt", player, world) @@ -118,7 +119,7 @@ def create_regions(world: MultiWorld, player: int): bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room") bbh_roof = create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion") regBBH.subregions = [bbh_third_floor, bbh_roof] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regBBH, "BBH: 100 Coins") regPSS = create_region("The Princess's Secret Slide", player, world) @@ -141,7 +142,7 @@ def create_regions(world: MultiWorld, player: int): hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins") hmc_pit_islands = create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit") regHMC.subregions = [hmc_red_coin_area, hmc_pit_islands] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(hmc_red_coin_area, "HMC: 100 Coins") regLLL = create_region("Lethal Lava Land", player, world) @@ -149,7 +150,7 @@ def create_regions(world: MultiWorld, player: int): "LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling") lll_upper_volcano = create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano") regLLL.subregions = [lll_upper_volcano] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regLLL, "LLL: 100 Coins") regSSL = create_region("Shifting Sand Land", player, world) @@ -159,7 +160,7 @@ def create_regions(world: MultiWorld, player: int): ssl_upper_pyramid = create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Inside the Ancient Pyramid", "SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle") regSSL.subregions = [ssl_upper_pyramid] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regSSL, "SSL: 100 Coins") regDDD = create_region("Dire, Dire Docks", player, world) @@ -167,7 +168,7 @@ def create_regions(world: MultiWorld, player: int): "DDD: The Manta Ray's Reward", "DDD: Collect the Caps...") ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins") regDDD.subregions = [ddd_moving_poles] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(ddd_moving_poles, "DDD: 100 Coins") regCotMC = create_region("Cavern of the Metal Cap", player, world) @@ -184,7 +185,7 @@ def create_regions(world: MultiWorld, player: int): regSL = create_region("Snowman's Land", player, world) create_default_locs(regSL, locSL_table) - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(regSL, "SL: 100 Coins") regWDW = create_region("Wet-Dry World", player, world) @@ -193,7 +194,7 @@ def create_regions(world: MultiWorld, player: int): "WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy") wdw_downtown = create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown") regWDW.subregions = [wdw_top, wdw_downtown] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(wdw_top, "WDW: 100 Coins") regTTM = create_region("Tall, Tall Mountain", player, world) @@ -202,7 +203,7 @@ def create_regions(world: MultiWorld, player: int): ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage", "TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge") regTTM.subregions = [ttm_middle, ttm_top] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(ttm_top, "TTM: 100 Coins") create_region("Tiny-Huge Island (Huge)", player, world) @@ -214,7 +215,7 @@ def create_regions(world: MultiWorld, player: int): "THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area") thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm") regTHI.subregions = [thi_pipes, thi_large_top] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(thi_large_top, "THI: 100 Coins") regFloor3 = create_region("Third Floor", player, world) @@ -225,7 +226,7 @@ def create_regions(world: MultiWorld, player: int): ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums") ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top") regTTC.subregions = [ttc_lower, ttc_upper, ttc_top] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(ttc_top, "TTC: 100 Coins") regRR = create_region("Rainbow Ride", player, world) @@ -235,7 +236,7 @@ def create_regions(world: MultiWorld, player: int): rr_cruiser = create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow") rr_house = create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky") regRR.subregions = [rr_maze, rr_cruiser, rr_house] - if (world.EnableCoinStars[player].value): + if options.enable_coin_stars: create_locs(rr_maze, "RR: 100 Coins") regWMotR = create_region("Wing Mario over the Rainbow", player, world) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index cc2b52f0f12f..72016b4f4014 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -3,6 +3,7 @@ from BaseClasses import MultiWorld from ..generic.Rules import add_rule, set_rule from .Locations import location_table +from .Options import SM64Options from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level,\ sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances from .Items import action_item_table @@ -24,7 +25,7 @@ def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_r swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest swapdict.pop(entrance) -def set_rules(world, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int): +def set_rules(world, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() valid_move_randomizer_start_courses = [ @@ -32,19 +33,19 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move "Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land" ] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR - if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses + if options.area_rando >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) # If not shuffling later, ensure a valid start course on move randomizer - if world.AreaRandomizer[player].value < 3 and move_rando_bitvec > 0: + if options.area_rando < 3 and move_rando_bitvec > 0: swapdict = randomized_level_to_paintings.copy() invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses} fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world) fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world) - if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well + if options.area_rando == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) randomized_entrances = {**randomized_level_to_paintings, **randomized_level_to_secrets} - if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool + if options.area_rando == 3: # Randomize Courses and Secrets in one pool randomized_entrances = shuffle_dict_keys(world, randomized_entrances) # Guarantee first entrance is a course swapdict = randomized_entrances.copy() @@ -67,7 +68,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()}) randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()} - rf = RuleFactory(world, player, move_rando_bitvec) + rf = RuleFactory(world, options, player, move_rando_bitvec) connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"]) connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1)) @@ -199,7 +200,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move # Bowser in the Sky rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG") # 100 Coin Stars - if world.EnableCoinStars[player]: + if options.enable_coin_stars: rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ") rf.assign_rule("WF: 100 Coins", "GP | MOVELESS") rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}") @@ -225,9 +226,9 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) - if world.CompletionType[player] == "last_bowser_stage": + if options.completion_type == "last_bowser_stage": world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player) - elif world.CompletionType[player] == "all_bowser_stages": + elif options.completion_type == "all_bowser_stages": world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \ state.can_reach("BitFS: Upper", 'Region', player) and \ state.can_reach("BitS: Top", 'Region', player) @@ -262,14 +263,14 @@ class RuleFactory: class SM64LogicException(Exception): pass - def __init__(self, world, player, move_rando_bitvec): + def __init__(self, world, options: SM64Options, player: int, move_rando_bitvec: int): self.world = world self.player = player self.move_rando_bitvec = move_rando_bitvec - self.area_randomizer = world.AreaRandomizer[player].value > 0 - self.capless = not world.StrictCapRequirements[player] - self.cannonless = not world.StrictCannonRequirements[player] - self.moveless = not world.StrictMoveRequirements[player] or not move_rando_bitvec > 0 + self.area_randomizer = options.area_rando > 0 + self.capless = not options.strict_cap_requirements + self.cannonless = not options.strict_cannon_requirements + self.moveless = not options.strict_move_requirements or not move_rando_bitvec > 0 def assign_rule(self, target_name: str, rule_expr: str): target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index e6a6e42c76a0..0e944aa4ab4b 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -3,7 +3,7 @@ import json from .Items import item_table, action_item_table, cannon_item_table, SM64Item from .Locations import location_table, SM64Location -from .Options import sm64_options +from .Options import SM64Options from .Rules import set_rules from .Regions import create_regions, sm64_level_to_entrances, SM64Levels from BaseClasses import Item, Tutorial, ItemClassification, Region @@ -40,7 +40,7 @@ class SM64World(World): area_connections: typing.Dict[int, int] - option_definitions = sm64_options + options_dataclass = SM64Options number_of_stars: int move_rando_bitvec: int @@ -49,38 +49,36 @@ class SM64World(World): def generate_early(self): max_stars = 120 - if (not self.multiworld.EnableCoinStars[self.player].value): + if (not self.options.enable_coin_stars): max_stars -= 15 self.move_rando_bitvec = 0 - for action, itemid in action_item_table.items(): - # HACK: Disable randomization of double jump - if action == 'Double Jump': continue - if getattr(self.multiworld, f"MoveRandomizer{action.replace(' ','')}")[self.player].value: + if self.options.enable_move_rando: + for action in self.options.move_rando_actions.value: max_stars -= 1 - self.move_rando_bitvec |= (1 << (itemid - action_item_table['Double Jump'])) - if (self.multiworld.ExclamationBoxes[self.player].value > 0): + self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump'])) + if (self.options.exclamation_boxes > 0): max_stars += 29 - self.number_of_stars = min(self.multiworld.AmountOfStars[self.player].value, max_stars) + self.number_of_stars = min(self.options.amount_of_stars, max_stars) self.filler_count = max_stars - self.number_of_stars self.star_costs = { - 'FirstBowserDoorCost': round(self.multiworld.FirstBowserStarDoorCost[self.player].value * self.number_of_stars / 100), - 'BasementDoorCost': round(self.multiworld.BasementStarDoorCost[self.player].value * self.number_of_stars / 100), - 'SecondFloorDoorCost': round(self.multiworld.SecondFloorStarDoorCost[self.player].value * self.number_of_stars / 100), - 'MIPS1Cost': round(self.multiworld.MIPS1Cost[self.player].value * self.number_of_stars / 100), - 'MIPS2Cost': round(self.multiworld.MIPS2Cost[self.player].value * self.number_of_stars / 100), - 'StarsToFinish': round(self.multiworld.StarsToFinish[self.player].value * self.number_of_stars / 100) + 'FirstBowserDoorCost': round(self.options.first_bowser_star_door_cost * self.number_of_stars / 100), + 'BasementDoorCost': round(self.options.basement_star_door_cost * self.number_of_stars / 100), + 'SecondFloorDoorCost': round(self.options.second_floor_star_door_cost * self.number_of_stars / 100), + 'MIPS1Cost': round(self.options.mips1_cost * self.number_of_stars / 100), + 'MIPS2Cost': round(self.options.mips2_cost * self.number_of_stars / 100), + 'StarsToFinish': round(self.options.stars_to_finish * self.number_of_stars / 100) } # Nudge MIPS 1 to match vanilla on default percentage - if self.number_of_stars == 120 and self.multiworld.MIPS1Cost[self.player].value == 12: + if self.number_of_stars == 120 and self.options.mips1_cost == 12: self.star_costs['MIPS1Cost'] = 15 - self.topology_present = self.multiworld.AreaRandomizer[self.player].value + self.topology_present = self.options.area_rando def create_regions(self): - create_regions(self.multiworld, self.player) + create_regions(self.multiworld, self.options, self.player) def set_rules(self): self.area_connections = {} - set_rules(self.multiworld, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec) + set_rules(self.multiworld, self.options, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec) if self.topology_present: # Write area_connections to spoiler log for entrance, destination in self.area_connections.items(): @@ -107,7 +105,7 @@ def create_items(self): # Power Stars self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)] # Keys - if (not self.multiworld.ProgressiveKeys[self.player].value): + if (not self.options.progressive_keys): key1 = self.create_item("Basement Key") key2 = self.create_item("Second Floor Key") self.multiworld.itempool += [key1, key2] @@ -116,7 +114,7 @@ def create_items(self): # Caps self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]] # Cannons - if (self.multiworld.BuddyChecks[self.player].value): + if (self.options.buddy_checks): self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()] # Moves self.multiworld.itempool += [self.create_item(action) @@ -124,7 +122,7 @@ def create_items(self): if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])] def generate_basic(self): - if not (self.multiworld.BuddyChecks[self.player].value): + if not (self.options.buddy_checks): self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB")) self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF")) self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB")) @@ -136,7 +134,7 @@ def generate_basic(self): self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) - if (self.multiworld.ExclamationBoxes[self.player].value == 0): + if (self.options.exclamation_boxes == 0): self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) @@ -174,8 +172,8 @@ def fill_slot_data(self): return { "AreaRando": self.area_connections, "MoveRandoVec": self.move_rando_bitvec, - "DeathLink": self.multiworld.death_link[self.player].value, - "CompletionType": self.multiworld.CompletionType[self.player].value, + "DeathLink": self.options.death_link.value, + "CompletionType": self.options.completion_type.value, **self.star_costs } From 702f006c848c05b847e85f7dbedeef68b70cdcc6 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:31:36 -0600 Subject: [PATCH 33/74] CV64: Change all mentions of "settings" to "options" and fix a broken link (#3015) --- worlds/cv64/__init__.py | 6 +++--- worlds/cv64/docs/en_Castlevania 64.md | 6 +++--- worlds/cv64/docs/setup_en.md | 4 ++-- worlds/cv64/options.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index ca4697bce8d5..1f528feac22f 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -48,9 +48,9 @@ class CV64Web(WebWorld): class CV64World(World): """ - Castlevania for the Nintendo 64 is the first 3D game in the franchise. As either whip-wielding Belmont descendant - Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you make your - way to Dracula's chamber and stop his rule of terror! + Castlevania for the Nintendo 64 is the first 3D game in the Castlevania franchise. As either whip-wielding Belmont + descendant Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you + make your way to Dracula's chamber and stop his rule of terror! """ game = "Castlevania 64" item_name_groups = { diff --git a/worlds/cv64/docs/en_Castlevania 64.md b/worlds/cv64/docs/en_Castlevania 64.md index 5fe85555c40a..692bbfe86a71 100644 --- a/worlds/cv64/docs/en_Castlevania 64.md +++ b/worlds/cv64/docs/en_Castlevania 64.md @@ -1,8 +1,8 @@ # Castlevania 64 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -116,7 +116,7 @@ Enabling Carrie Logic will also expect the following: - Orb-sniping dogs through the front gates in Villa -Library Skip is **NOT** logically expected on any setting. The basement hallway crack will always logically expect two Nitros +Library Skip is **NOT** logically expected by any options. The basement arena crack will always logically expect two Nitros and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room. diff --git a/worlds/cv64/docs/setup_en.md b/worlds/cv64/docs/setup_en.md index 6065b142c82c..707618a1eba5 100644 --- a/worlds/cv64/docs/setup_en.md +++ b/worlds/cv64/docs/setup_en.md @@ -28,8 +28,8 @@ the White Jewels. ## Generating and Patching a Game -1. Create your settings file (YAML). You can make one on the -[Castlevania 64 settings page](../../../games/Castlevania 64/player-settings). +1. Create your options file (YAML). You can make one on the +[Castlevania 64 options page](../../../games/Castlevania%2064/player-options). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). This will generate an output file for you. Your patch file will have the `.apcv64` file extension. 3. Open `ArchipelagoLauncher.exe` diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index 4545cd0b5c28..e1be03897dcf 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -337,13 +337,13 @@ class BigToss(Toggle): """Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge. Press A while tossed to cancel the launch momentum and avoid being thrown off ledges. Hold Z to have all incoming damage be treated as it normally would. - Any tricks that might be possible with it are NOT considered in logic on any setting.""" + Any tricks that might be possible with it are NOT considered in logic by any options.""" display_name = "Big Toss" class PantherDash(Choice): """Hold C-right at any time to sprint way faster. Any tricks that might be - possible with it are NOT considered in logic on any setting and any boss + possible with it are NOT considered in logic by any options and any boss fights with boss health meters, if started, are expected to be finished before leaving their arenas if Dracula's Condition is bosses. Jumpless will prevent jumping while moving at the increased speed to ensure logic cannot be broken with it.""" From f7b415dab00338443b68eba51f42614fc40b9152 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Tue, 26 Mar 2024 19:40:58 +0300 Subject: [PATCH 34/74] Stardew valley: Game version documentation (#2990) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- .../stardew_valley/docs/en_Stardew Valley.md | 67 +++++++++---------- worlds/stardew_valley/docs/setup_en.md | 11 +-- worlds/stardew_valley/options.py | 2 +- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index 789c12020c18..c29ae859e095 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -81,22 +81,17 @@ For the locations which do not include a normal reward, Resource Packs and traps A player can enable some options that will add some items to the pool that are relevant to progression - Seasons Randomizer: - * All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. - * At the end of each month, the player can choose the next season, instead of following the vanilla season order. On -Seasons Randomizer, they can only choose from the seasons they have received. + - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory. + - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. - Cropsanity: - * Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received - as multiworld items. Growing each seed and harvesting the resulting crop sends a location check - * The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells - unlimited seeds but in huge discount packs, not individually. + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check + - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. - Museumsanity: - * The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the - magic rock candy, are duplicated for convenience. - * The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. - She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. + - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. - TV Channels - Babies - * Only if Friendsanity is enabled + - Only if Friendsanity is enabled There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include - [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings) @@ -135,32 +130,32 @@ for these mods, the specifics will vary from mod to mod List of supported mods: - General - * [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753) - * [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571) - * [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963) - * [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845) - * [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401) - * [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109) + - [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753) + - [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571) + - [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963) + - [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845) + - [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401) + - [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109) - Skills - * [Magic](https://www.nexusmods.com/stardewvalley/mods/2007) - * [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521) - * [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142) - * [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793) - * [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522) - * [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073) + - [Magic](https://www.nexusmods.com/stardewvalley/mods/2007) + - [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521) + - [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142) + - [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793) + - [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522) + - [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073) - NPCs - * [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427) - * [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295) - * [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606) - * [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599) - * [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697) - * [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871) - * [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222) - * ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462) - * [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732) - * [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510) - * [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) - * [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) + - [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427) + - [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295) + - [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606) + - [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599) + - [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697) + - [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871) + - [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222) + - ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462) + - [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732) + - [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510) + - [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811) + - [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671) Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found [here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases) diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 3b51622d8d0a..74caf9b7daba 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -3,7 +3,11 @@ ## Required Software - Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) -- SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) + - You need version 1.5.6. It is available in a public beta branch on Steam ![image](https://i.imgur.com/uKAUmF0.png). + - If your Stardew is not on Steam, you are responsible for finding a way to downgrade it. + - This measure is temporary. We are working hard to bring the mod to Stardew 1.6 as soon as possible. +- SMAPI 3.x.x ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files)) + - Same as Stardew Valley itself, SMAPI needs a slightly older version to be compatible with Stardew Valley 1.5.6 ![image](https://i.imgur.com/kzgObHy.png) - [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. @@ -34,11 +38,10 @@ You can customize your options by visiting the [Stardew Valley Player Options Pa ### Installing the mod -- Install [SMAPI](https://smapi.io/) by following the instructions on their website +- Install [SMAPI version 3.x.x](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page - Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into your Stardew Valley "Mods" folder -- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: - - "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command% +- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%` - Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly - Stardew Valley should launch itself alongside a console which allows you to read mod information and interact with some of them. diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 634de45285f7..055407d97d4a 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -197,7 +197,7 @@ class Cropsanity(Choice): """Formerly named "Seed Shuffle" Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs. Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops - Shuffled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop + Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop """ internal_name = "cropsanity" display_name = "Cropsanity" From 1d4512590e0b78355e5c10174a9c6749e1098a72 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Wed, 27 Mar 2024 21:09:09 +0100 Subject: [PATCH 35/74] requirements.txt: _ instead of - to make PyCharm happy (#3043) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9531e3058e8a..d1a7b763f37f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,4 @@ certifi>=2023.11.17 cython>=3.0.8 cymem>=2.0.8 orjson>=3.9.10 -typing-extensions>=4.7.0 +typing_extensions>=4.7.0 From ca1812181106a3645e7f7af417590024b377b25e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Thu, 28 Mar 2024 04:27:49 -0400 Subject: [PATCH 36/74] Stardew Valley: Fix generation fail with SVE and entrance rando when Wizard Tower is in place of Sprite Spring (#2970) --- worlds/stardew_valley/data/villagers_data.py | 11 ++++++----- worlds/stardew_valley/logic/logic.py | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py index ae6a346d56d7..718bce743b1c 100644 --- a/worlds/stardew_valley/data/villagers_data.py +++ b/worlds/stardew_valley/data/villagers_data.py @@ -13,9 +13,9 @@ class Villager: name: str bachelor: bool - locations: Tuple[str] + locations: Tuple[str, ...] birthday: str - gifts: Tuple[str] + gifts: Tuple[str, ...] available: bool mod_name: str @@ -366,10 +366,11 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st return npc -def make_bachelor(mod_name: str, npc: Villager): +def adapt_wizard_to_sve(mod_name: str, npc: Villager): if npc.mod_name: mod_name = npc.mod_name - return Villager(npc.name, True, npc.locations, npc.birthday, npc.gifts, npc.available, mod_name) + # The wizard leaves his tower on sunday, for like 1 hour... Good enough to meet him! + return Villager(npc.name, True, npc.locations + forest, npc.birthday, npc.gifts, npc.available, mod_name) def register_villager_modification(mod_name: str, npc: Villager, modification_function): @@ -452,7 +453,7 @@ def register_villager_modification(mod_name: str, npc: Villager, modification_fu # Modified villagers; not included in all villagers -register_villager_modification(ModNames.sve, wizard, make_bachelor) +register_villager_modification(ModNames.sve, wizard, adapt_wizard_to_sve) all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers} all_villagers_by_mod: Dict[str, List[Villager]] = {} diff --git a/worlds/stardew_valley/logic/logic.py b/worlds/stardew_valley/logic/logic.py index 1c79e9345930..a7fcec922838 100644 --- a/worlds/stardew_valley/logic/logic.py +++ b/worlds/stardew_valley/logic/logic.py @@ -546,6 +546,7 @@ def can_succeed_luau_soup(self) -> StardewRule: def can_succeed_grange_display(self) -> StardewRule: if self.options.festival_locations != FestivalLocations.option_hard: return True_() + animal_rule = self.animal.has_animal(Generic.any) artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any) cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough From cf133dde7275e171d388fb466b9ed719ab7ed7c8 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Mar 2024 02:32:27 -0600 Subject: [PATCH 37/74] Pokemon Emerald: Fix typo (#3020) --- worlds/pokemon_emerald/data.py | 2 +- worlds/pokemon_emerald/data/locations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index c4f7d7711c81..786740a9e48f 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -646,7 +646,7 @@ def _init() -> None: ("SPECIES_CHIKORITA", "Chikorita", 152), ("SPECIES_BAYLEEF", "Bayleef", 153), ("SPECIES_MEGANIUM", "Meganium", 154), - ("SPECIES_CYNDAQUIL", "Cindaquil", 155), + ("SPECIES_CYNDAQUIL", "Cyndaquil", 155), ("SPECIES_QUILAVA", "Quilava", 156), ("SPECIES_TYPHLOSION", "Typhlosion", 157), ("SPECIES_TOTODILE", "Totodile", 158), diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json index fdaec9b83ca0..d654119ad635 100644 --- a/worlds/pokemon_emerald/data/locations.json +++ b/worlds/pokemon_emerald/data/locations.json @@ -2497,7 +2497,7 @@ "tags": ["Pokedex"] }, "POKEDEX_REWARD_155": { - "label": "Pokedex - Cindaquil", + "label": "Pokedex - Cyndaquil", "tags": ["Pokedex"] }, "POKEDEX_REWARD_156": { From 14f5f0127eb753eaf0431a54bebc82f5e74a1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Bolduc?= <16137441+Jouramie@users.noreply.github.com> Date: Thu, 28 Mar 2024 04:42:35 -0400 Subject: [PATCH 38/74] Stardew Valley: Fix potential soft lock with vanilla tools and entrance randomizer + Performance improvement for vanilla tool/skills (#3002) * fix vanilla tool fishing rod requiring metal bars fix vanilla skill requiring previous level (it's always the same rule or more restrictive) * add test to ensure fishing rod need fish shop * fishing rod should be indexed from 0 like a mentally sane person would do. * fishing rod 0 isn't real, but it definitely can hurt you. * reeeeeeeee --- worlds/stardew_valley/logic/fishing_logic.py | 3 +- worlds/stardew_valley/logic/skill_logic.py | 12 ++++- worlds/stardew_valley/logic/tool_logic.py | 20 +++++--- worlds/stardew_valley/test/TestRules.py | 49 ++++++++++++++++++++ 4 files changed, 75 insertions(+), 9 deletions(-) diff --git a/worlds/stardew_valley/logic/fishing_logic.py b/worlds/stardew_valley/logic/fishing_logic.py index 65b3cdc2ac88..a7399a65d99c 100644 --- a/worlds/stardew_valley/logic/fishing_logic.py +++ b/worlds/stardew_valley/logic/fishing_logic.py @@ -73,7 +73,8 @@ def can_catch_quality_fish(self, fish_quality: str) -> StardewRule: return rod_rule & self.logic.skill.has_level(Skill.fishing, 4) if fish_quality == FishQuality.iridium: return rod_rule & self.logic.skill.has_level(Skill.fishing, 10) - return False_() + + raise ValueError(f"Quality {fish_quality} is unknown.") def can_catch_every_fish(self) -> StardewRule: rules = [self.has_max_fishing()] diff --git a/worlds/stardew_valley/logic/skill_logic.py b/worlds/stardew_valley/logic/skill_logic.py index 9134dfae40bf..35946a0a4d36 100644 --- a/worlds/stardew_valley/logic/skill_logic.py +++ b/worlds/stardew_valley/logic/skill_logic.py @@ -44,10 +44,14 @@ def can_earn_level(self, skill: str, level: int) -> StardewRule: tool_material = ToolMaterial.tiers[tool_level] months = max(1, level - 1) months_rule = self.logic.time.has_lived_months(months) - previous_level_rule = self.logic.skill.has_level(skill, level - 1) + + if self.options.skill_progression != options.SkillProgression.option_vanilla: + previous_level_rule = self.logic.skill.has_level(skill, level - 1) + else: + previous_level_rule = True_() if skill == Skill.fishing: - xp_rule = self.logic.tool.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)]) + xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1)) elif skill == Skill.farming: xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level) elif skill == Skill.foraging: @@ -137,13 +141,17 @@ def can_get_fishing_xp(self) -> StardewRule: def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule: if isinstance(regions, str): regions = regions, + if regions is None or len(regions) == 0: regions = fishing_regions + skill_required = min(10, max(0, int((difficulty / 10) - 1))) if difficulty <= 40: skill_required = 0 + skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required) region_rule = self.logic.region.can_reach_any(regions) + # Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic. number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4) return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule diff --git a/worlds/stardew_valley/logic/tool_logic.py b/worlds/stardew_valley/logic/tool_logic.py index def02b35dab6..1b1dc2a52120 100644 --- a/worlds/stardew_valley/logic/tool_logic.py +++ b/worlds/stardew_valley/logic/tool_logic.py @@ -12,10 +12,14 @@ from ..stardew_rule import StardewRule, True_, False_ from ..strings.ap_names.skill_level_names import ModSkillLevel from ..strings.region_names import Region -from ..strings.skill_names import ModSkill from ..strings.spells import MagicSpell from ..strings.tool_names import ToolMaterial, Tool +fishing_rod_prices = { + 3: 1800, + 4: 7500, +} + tool_materials = { ToolMaterial.copper: 1, ToolMaterial.iron: 2, @@ -40,27 +44,31 @@ def __init__(self, *args, **kwargs): class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]): # Should be cached def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule: + assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`." + if material == ToolMaterial.basic or tool == Tool.scythe: return True_() if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {tool}", tool_materials[material]) - return self.logic.has(f"{material} Bar") & self.logic.money.can_spend(tool_upgrade_prices[material]) + return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material]) def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule: return self.has_tool(tool, material) & self.logic.region.can_reach(region) @cache_self1 def has_fishing_rod(self, level: int) -> StardewRule: + assert 1 <= level <= 4, "Fishing rod 0 isn't real, it can't hurt you. Training is 1, Bamboo is 2, Fiberglass is 3 and Iridium is 4." + if self.options.tool_progression & ToolProgression.option_progressive: return self.logic.received(f"Progressive {Tool.fishing_rod}", level) - if level <= 1: + if level <= 2: + # We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back. return self.logic.region.can_reach(Region.beach) - prices = {2: 500, 3: 1800, 4: 7500} - level = min(level, 4) - return self.logic.money.can_spend_at(Region.fish_shop, prices[level]) + + return self.logic.money.can_spend_at(Region.fish_shop, fishing_rod_prices[level]) # Should be cached def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule: diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py index 0d2fc38a19a3..787e0ce39c3e 100644 --- a/worlds/stardew_valley/test/TestRules.py +++ b/worlds/stardew_valley/test/TestRules.py @@ -8,6 +8,7 @@ FriendsanityHeartSize, BundleRandomization, SkillProgression from ..strings.entrance_names import Entrance from ..strings.region_names import Region +from ..strings.tool_names import Tool, ToolMaterial class TestProgressiveToolsLogic(SVTestBase): @@ -596,6 +597,54 @@ def swap_museum_and_bathhouse(multiworld, player): bathhouse_entrance.connect(museum_region) +class TestToolVanillaRequiresBlacksmith(SVTestBase): + options = { + options.EntranceRandomization: options.EntranceRandomization.option_buildings, + options.ToolProgression: options.ToolProgression.option_vanilla, + } + seed = 4111845104987680262 + + # Seed is hardcoded to make sure the ER is a valid roll that actually lock the blacksmith behind the Railroad Boulder Removed. + + def test_cannot_get_any_tool_without_blacksmith_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.blacksmith, Entrance.enter_bathhouse_entrance) + collect_all_except(self.multiworld, railroad_item) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_false(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) + + for tool in [Tool.pickaxe, Tool.axe, Tool.hoe, Tool.trash_can, Tool.watering_can]: + for material in [ToolMaterial.copper, ToolMaterial.iron, ToolMaterial.gold, ToolMaterial.iridium]: + self.assert_rule_true(self.world.logic.tool.has_tool(tool, material), self.multiworld.state) + + def test_cannot_get_fishing_rod_without_willy_access(self): + railroad_item = "Railroad Boulder Removed" + place_region_at_entrance(self.multiworld, self.player, Region.fish_shop, Entrance.enter_bathhouse_entrance) + collect_all_except(self.multiworld, railroad_item) + + for fishing_rod_level in [3, 4]: + self.assert_rule_false(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item(railroad_item), event=False) + + for fishing_rod_level in [3, 4]: + self.assert_rule_true(self.world.logic.tool.has_fishing_rod(fishing_rod_level), self.multiworld.state) + + +def place_region_at_entrance(multiworld, player, region, entrance): + region_to_place = multiworld.get_region(region, player) + entrance_to_place_region = multiworld.get_entrance(entrance, player) + + entrance_to_switch = region_to_place.entrances[0] + region_to_switch = entrance_to_place_region.connected_region + entrance_to_switch.connect(region_to_switch) + entrance_to_place_region.connect(region_to_place) + + def collect_all_except(multiworld, item_to_not_collect: str): for item in multiworld.get_items(): if item.name != item_to_not_collect: From d0ac2b744eac438570e6a2333e76fa212be66534 Mon Sep 17 00:00:00 2001 From: panicbit Date: Thu, 28 Mar 2024 10:11:26 +0100 Subject: [PATCH 39/74] LADX: fix local and non-local instrument placement (#2987) * LADX: fix local and non-local instrument placement * change confusing variable name --- worlds/ladx/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 9de3462ad059..d662b526bb61 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -184,19 +184,22 @@ def create_items(self) -> None: self.pre_fill_items = [] # For any and different world, set item rule instead - for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]: - option = "shuffle_" + option + for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]: + option = "shuffle_" + dungeon_item_type option = self.player_options[option] dungeon_item_types[option.ladxr_item] = option.value + # The color dungeon does not contain an instrument + num_items = 8 if dungeon_item_type == "instruments" else 9 + if option.value == DungeonItemShuffle.option_own_world: self.multiworld.local_items[self.player].value |= { - ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } elif option.value == DungeonItemShuffle.option_different_world: self.multiworld.non_local_items[self.player].value |= { - ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10) + ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1) } # option_original_dungeon = 0 # option_own_dungeons = 1 From 01cdb0d761a82349afaeb7222b4b59cb1766f4a0 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:44:23 -0400 Subject: [PATCH 40/74] SMW: Update World Doc for v2.0 Features (#3034) Co-authored-by: Scipio Wright --- worlds/smw/docs/en_Super Mario World.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/smw/docs/en_Super Mario World.md b/worlds/smw/docs/en_Super Mario World.md index 26623bac83d3..f7a12839df4d 100644 --- a/worlds/smw/docs/en_Super Mario World.md +++ b/worlds/smw/docs/en_Super Mario World.md @@ -25,10 +25,16 @@ There are two goals which can be chosen: ## What items and locations get shuffled? -Each unique level exit awards a location check. Optionally, collecting five Dragon Coins in each level can also award a location check. +Each unique level exit awards a location check. Additionally, the following in-level actions can be set to award a location check: +- Collecting Five Dragon Coins +- Collecting 3-Up Moons +- Activating Bonus Blocks +- Receiving Hidden 1-Ups +- Hitting Blocks containing coins or items + Mario's various abilities and powerups as described above are placed into the item pool. If the player is playing Yoshi Egg Hunt, a certain number of Yoshi Eggs will be placed into the item pool. -Any additional items that are needed to fill out the item pool with be 1-Up Mushrooms. +Any additional items that are needed to fill out the item pool will be 1-Up Mushrooms, bundles of coins, or, if enabled, various trap items. ## Which items can be in another player's world? From db15dd4bde442aad99048224bdb0d7dc28c26717 Mon Sep 17 00:00:00 2001 From: chandler05 <66492208+chandler05@users.noreply.github.com> Date: Thu, 28 Mar 2024 08:45:19 -0500 Subject: [PATCH 41/74] A Short Hike: Fix incorrect info in docs (#3016) --- worlds/shorthike/docs/en_A Short Hike.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/shorthike/docs/en_A Short Hike.md b/worlds/shorthike/docs/en_A Short Hike.md index 516bf28e47fc..11b5b4b8ca85 100644 --- a/worlds/shorthike/docs/en_A Short Hike.md +++ b/worlds/shorthike/docs/en_A Short Hike.md @@ -26,5 +26,4 @@ To achieve the Help Everyone goal, the following characters will need to be help ## Can I have more than one save at a time? -No, unfortunately only one save slot is available for use in A Short Hike. -Starting a new save will erase the old one _permanently_. \ No newline at end of file +You can have up to 3 saves at a time. To switch between them, use the Save Data button in the options menu. From fa93488f3fceac6c2f51851766543cab3ba121e6 Mon Sep 17 00:00:00 2001 From: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:46:00 -0400 Subject: [PATCH 42/74] Docs: Consistent naming for "connection plando" (#2994) --- worlds/generic/docs/plando_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/generic/docs/plando_en.md b/worlds/generic/docs/plando_en.md index d6a09cf4e610..161b1e465b33 100644 --- a/worlds/generic/docs/plando_en.md +++ b/worlds/generic/docs/plando_en.md @@ -201,7 +201,7 @@ Kirby's Dream Land 3: As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en) -## Connections Plando +## Connection Plando This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in From bcf223081facd030aa706dc7430a72bcf2fdadc9 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:54:56 -0400 Subject: [PATCH 43/74] TLOZ: Fix markdown issue with game info page (#2985) --- worlds/tloz/docs/en_The Legend of Zelda.md | 1 - 1 file changed, 1 deletion(-) diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md index 938496a161ae..96b613673f00 100644 --- a/worlds/tloz/docs/en_The Legend of Zelda.md +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -41,7 +41,6 @@ filler and useful items will cost less, and uncategorized items will be in the m - Pressing Select will cycle through your inventory. - Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. - What slots from a Take Any Cave have been chosen are similarly tracked. -- ## Local Unique Commands From 9182fe563fc18ed4ccaa8370cfed88407140398e Mon Sep 17 00:00:00 2001 From: Entropynines <163603868+Entropynines@users.noreply.github.com> Date: Thu, 28 Mar 2024 06:56:35 -0700 Subject: [PATCH 44/74] README: Remove outdated information about launchers (#2966) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 905c731b643a..cbfdf75f05b7 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,9 @@ We recognize that there is a strong community of incredibly smart people that ha Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project. ## Running Archipelago -For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only. +For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems. -If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md). +If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md). ## Related Repositories This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present. From 17748a4bf1cfd5cc11c6596a09ffc1f01434340f Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Thu, 28 Mar 2024 10:00:10 -0400 Subject: [PATCH 45/74] Launcher, Docs: Update UI and Set-Up Guide to Reference Options (#2950) --- Launcher.py | 2 +- worlds/generic/docs/setup_en.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Launcher.py b/Launcher.py index 890957958391..9fd5d91df042 100644 --- a/Launcher.py +++ b/Launcher.py @@ -100,7 +100,7 @@ def update_settings(): # Functions Component("Open host.yaml", func=open_host_yaml), Component("Open Patch", func=open_patch), - Component("Generate Template Settings", func=generate_yamls), + Component("Generate Template Options", func=generate_yamls), Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component("Browse Files", func=browse_files), diff --git a/worlds/generic/docs/setup_en.md b/worlds/generic/docs/setup_en.md index b99cdbc0fe54..ef2413378960 100644 --- a/worlds/generic/docs/setup_en.md +++ b/worlds/generic/docs/setup_en.md @@ -39,7 +39,7 @@ to your Archipelago installation. ### What is a YAML? YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which -game you will be playing as well as the settings you would like for that game. +game you will be playing as well as the options you would like for that game. YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website: @@ -48,10 +48,10 @@ validity of your YAML file you may check the file by uploading it to the check p ### Creating a YAML YAML files may be generated on the Archipelago website by visiting the [games page](/games) and clicking the -"Settings Page" link under the relevant game. Clicking "Export Settings" in a game's settings page will download the +"Options Page" link under the relevant game. Clicking "Export Options" in a game's options page will download the YAML to your system. -Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Settings` to create a set of template +Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Options` to create a set of template YAMLs for each game in your Archipelago install (including for APWorlds). These will be placed in your `Players/Templates` folder. In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's @@ -66,17 +66,17 @@ each player is planning on playing their own game then they will each need a YAM #### On the website The easiest way to get started playing an Archipelago generated game, after following the base setup from the game's -setup guide, is to find the game on the [Archipelago Games List](/games), click on `Settings Page`, set the settings for +setup guide, is to find the game on the [Archipelago Games List](/games), click on `Options Page`, set the options for how you want to play, and click `Generate Game` at the bottom of the page. This will create a page for the seed, from which you can create a room, and then [connect](#connecting-to-an-archipelago-server). -If you have downloaded the settings, or have created a settings file manually, this file can be uploaded on the +If you have downloaded the options, or have created an options file manually, this file can be uploaded on the [Generation Page](/generate) where you can also set any specific hosting settings. #### On your local installation To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago -installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded +installation (usually C:\ProgramData\Archipelago), and place the options file you have either created or downloaded from the website in the `Players` folder. Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation @@ -97,7 +97,7 @@ resources, and host the resulting multiworld on the website. #### Gather All Player YAMLs -All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they +All players that wish to play in the generated multiworld must have a YAML file which contains the options that they wish to play with. One person should gather all files from all participants in the generated multiworld. It is possible for a single player to have multiple games, or even multiple slots of a single game, but each YAML must have a unique player name. @@ -129,7 +129,7 @@ need the corresponding ROM files. Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode, auto-release, plando support, or setting a password. -All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago +All of these settings, plus more, can be changed by modifying the `host.yaml` file in the Archipelago installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten @@ -207,4 +207,4 @@ when creating your [YAML file](#creating-a-yaml). If the game is hosted on the w room page. The name is case-sensitive. * `Password` is the password set by the host in order to join the multiworld. By default, this will be empty and is almost never required, but one can be set when generating the game. Generally, leave this field blank when it exists, -unless you know that a password was set, and what that password is. \ No newline at end of file +unless you know that a password was set, and what that password is. From 4d954afd9b2311248083fc389ac737995985be86 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 28 Mar 2024 10:11:20 -0400 Subject: [PATCH 46/74] TUNIC: Add link to AP plando guide to connection plando section of game page (#2993) --- worlds/tunic/docs/en_TUNIC.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/tunic/docs/en_TUNIC.md b/worlds/tunic/docs/en_TUNIC.md index ad328999ac0c..f1e0056041bb 100644 --- a/worlds/tunic/docs/en_TUNIC.md +++ b/worlds/tunic/docs/en_TUNIC.md @@ -86,3 +86,5 @@ Notes: - There is no limit to the number of Shops hard-coded into place. - If you have more than one shop in a scene, you may be wrong warped when exiting a shop. - If you have a shop in every scene, and you have an odd number of shops, it will error out. + +See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando. From cfc1541be9e92f1f59b21f4a81f96fc88f4d9f7e Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Mar 2024 15:19:32 +0100 Subject: [PATCH 47/74] Docs: Mention the "last received item index" paradigm in the network protocol docs (#2989) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- docs/network protocol.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/network protocol.md b/docs/network protocol.md index 9f2c07883b9d..604ff6708fca 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -31,6 +31,9 @@ There are also a number of community-supported libraries available that implemen | GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | | ## Synchronizing Items +After a client connects, it will receive all previously collected items for its associated slot in a [ReceivedItems](#ReceivedItems) packet. This will include items the client may have already processed in a previous play session. +To ensure the client is able to reject those items if it needs to, each item in the packet has an associated `index` argument. You will need to find a way to save the "last processed item index" to the player's local savegame, a local file, or something to that effect. Before connecting, you should load that "last processed item index" value and compare against it in your received items handling. + When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption. From 77311719fa0fa5b67fe92f437c3cfed16bd5136f Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Thu, 28 Mar 2024 15:38:34 +0100 Subject: [PATCH 48/74] SC2: Fix HERC upgrades (#3044) --- WebHostLib/templates/tracker__Starcraft2.html | 3 ++- WebHostLib/tracker.py | 1 + worlds/sc2/Items.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/WebHostLib/templates/tracker__Starcraft2.html b/WebHostLib/templates/tracker__Starcraft2.html index 089e7d6c2239..d365d126338d 100644 --- a/WebHostLib/templates/tracker__Starcraft2.html +++ b/WebHostLib/templates/tracker__Starcraft2.html @@ -294,7 +294,8 @@

{{ player_name }}'s Starcraft 2 Tracker

{{ sc2_icon('HERC') }} {{ sc2_icon('Juggernaut Plating (HERC)') }} {{ sc2_icon('Kinetic Foam (HERC)') }} - + {{ sc2_icon('Resource Efficiency (HERC)') }} + {{ sc2_icon('Widow Mine') }} {{ sc2_icon('Drilling Claws (Widow Mine)') }} {{ sc2_icon('Concealment (Widow Mine)') }} diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 95bca57493c2..5b246e5aeef0 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -1674,6 +1674,7 @@ def render_Starcraft2_tracker(tracker_data: TrackerData, team: int, player: int) "Resource Efficiency (Spectre)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", "Juggernaut Plating (HERC)": organics_icon_base_url + "JuggernautPlating.png", "Kinetic Foam (HERC)": organics_icon_base_url + "KineticFoam.png", + "Resource Efficiency (HERC)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png", "Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg", "Vulture": github_icon_base_url + "blizzard/btn-unit-terran-vulture.png", diff --git a/worlds/sc2/Items.py b/worlds/sc2/Items.py index 85fb34e875af..8277d0e7e13d 100644 --- a/worlds/sc2/Items.py +++ b/worlds/sc2/Items.py @@ -661,11 +661,11 @@ def get_full_item_list(): description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("HERC")), ItemNames.HERC_JUGGERNAUT_PLATING: ItemData(285 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 16, SC2Race.TERRAN, - parent_item=ItemNames.WARHOUND, origin={"ext"}, + parent_item=ItemNames.HERC, origin={"ext"}, description="Increases HERC armor by 2."), ItemNames.HERC_KINETIC_FOAM: ItemData(286 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 17, SC2Race.TERRAN, - parent_item=ItemNames.WARHOUND, origin={"ext"}, + parent_item=ItemNames.HERC, origin={"ext"}, description="Increases HERC life by 50."), ItemNames.HELLION_TWIN_LINKED_FLAMETHROWER: From 80d7ac416493a540548aad67981202a1483b5e53 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Thu, 28 Mar 2024 09:41:32 -0500 Subject: [PATCH 49/74] KDL3: RC1 Fixes and Enhancement (#3022) * fix cloudy park 4 rule, zero deathlink message * remove redundant door_shuffle bool when generic ER gets in, this whole function gets rewritten. So just clean it a little now. * properly fix deathlink messages, fix fill error * update docs --- worlds/kdl3/Client.py | 17 +++-- worlds/kdl3/Regions.py | 85 +++++++++++---------- worlds/kdl3/Rules.py | 2 +- worlds/kdl3/docs/en_Kirby's Dream Land 3.md | 5 +- worlds/kdl3/docs/setup_en.md | 6 +- worlds/kdl3/test/test_locations.py | 3 +- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/worlds/kdl3/Client.py b/worlds/kdl3/Client.py index a1e68f8b67e3..e33a680bc025 100644 --- a/worlds/kdl3/Client.py +++ b/worlds/kdl3/Client.py @@ -36,8 +36,10 @@ KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020 KDL3_IS_DEMO = SRAM_1_START + 0x5AD5 -KDL3_GAME_STATE = SRAM_1_START + 0x36D0 KDL3_GAME_SAVE = SRAM_1_START + 0x3617 +KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F +KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641 +KDL3_GAME_STATE = SRAM_1_START + 0x36D0 KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF KDL3_KIRBY_HP = SRAM_1_START + 0x39D1 KDL3_BOSS_HP = SRAM_1_START + 0x39D5 @@ -46,8 +48,6 @@ KDL3_HEART_STARS = SRAM_1_START + 0x53A7 KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD -KDL3_CURRENT_WORLD = SRAM_1_START + 0x53CF -KDL3_CURRENT_LEVEL = SRAM_1_START + 0x53D3 KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5 KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1 KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4 @@ -74,7 +74,9 @@ 0x0202: " was out-numbered by Pon & Con.", 0x0203: " was defeated by Ado's powerful paintings.", 0x0204: " was clobbered by King Dedede.", - 0x0205: " lost their battle against Dark Matter." + 0x0205: " lost their battle against Dark Matter.", + 0x0300: " couldn't overcome the Boss Butch.", + 0x0400: " is bad at jumping.", }) @@ -281,6 +283,11 @@ async def game_watcher(self, ctx) -> None: for i in range(5): level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14) self.levels[i] = unpack("HHHHHHH", level_data) + self.levels[5] = [0x0205, # Hyper Zone + 0, # MG-5, can't send from here + 0x0300, # Boss Butch + 0x0400, # Jumping + 0, 0, 0] if self.consumables is None: consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1) @@ -314,7 +321,7 @@ async def game_watcher(self, ctx) -> None: current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0] current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0] currently_dead = current_hp[0] == 0x00 - message = deathlink_messages[self.levels[current_world][current_level - 1]] + message = deathlink_messages[self.levels[current_world][current_level]] await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}") recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2) diff --git a/worlds/kdl3/Regions.py b/worlds/kdl3/Regions.py index ed0d86586615..794a565e0a56 100644 --- a/worlds/kdl3/Regions.py +++ b/worlds/kdl3/Regions.py @@ -28,16 +28,30 @@ 0x77001C, # 5-4 needs Burning } +first_world_limit = { + # We need to limit the number of very restrictive stages in level 1 on solo gens + *first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks + 0x770007, + 0x770008, + 0x770013, + 0x77001E, -def generate_valid_level(level, stage, possible_stages, slot_random): - new_stage = slot_random.choice(possible_stages) - if level == 1 and stage == 0 and new_stage in first_stage_blacklist: - return generate_valid_level(level, stage, possible_stages, slot_random) - else: - return new_stage +} + + +def generate_valid_level(world: "KDL3World", level, stage, possible_stages, placed_stages): + new_stage = world.random.choice(possible_stages) + if level == 1: + if stage == 0 and new_stage in first_stage_blacklist: + return generate_valid_level(world, level, stage, possible_stages, placed_stages) + elif not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and \ + new_stage in first_world_limit and \ + sum(p_stage in first_world_limit for p_stage in placed_stages) >= 2: + return generate_valid_level(world, level, stage, possible_stages, placed_stages) + return new_stage -def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing.Dict[int, Region]): +def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]): level_names = {LocationName.level_names[level]: level for level in LocationName.level_names} room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json"))) rooms: typing.Dict[str, KDL3Room] = dict() @@ -49,8 +63,8 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else None for location in room_entry["locations"] if (not any(x in location for x in ["1-Up", "Maxim"]) or - world.options.consumables.value) and ("Star" not in location - or world.options.starsanity.value)}, + world.options.consumables.value) and ("Star" not in location + or world.options.starsanity.value)}, KDL3Location) rooms[room.name] = room for location in room.locations: @@ -62,33 +76,25 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing world.multiworld.regions.extend(world.rooms) first_rooms: typing.Dict[int, KDL3Room] = dict() - if door_shuffle: - # first, we need to generate the notable edge cases - # 5-6 is the first, being the most restrictive - # half of its rooms are required to be vanilla, but can be in different orders - # the room before it *must* contain the copy ability required to unlock the room's goal - - raise NotImplementedError() - else: - for name, room in rooms.items(): - if room.room == 0: - if room.stage == 7: - first_rooms[0x770200 + room.level - 1] = room - else: - first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room - exits = dict() - for def_exit in room.default_exits: - target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" - access_rule = tuple(def_exit["access_rule"]) - exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player) - room.add_exits( - exits.keys(), - exits - ) - if world.options.open_world: - if any("Complete" in location.name for location in room.locations): - room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None}, - KDL3Location) + for name, room in rooms.items(): + if room.room == 0: + if room.stage == 7: + first_rooms[0x770200 + room.level - 1] = room + else: + first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room + exits = dict() + for def_exit in room.default_exits: + target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}" + access_rule = tuple(def_exit["access_rule"]) + exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player) + room.add_exits( + exits.keys(), + exits + ) + if world.options.open_world: + if any("Complete" in location.name for location in room.locations): + room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None}, + KDL3Location) for level in world.player_levels: for stage in range(6): @@ -102,7 +108,7 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing if world.options.open_world or stage == 0: level_regions[level].add_exits([first_rooms[proper_stage].name]) else: - world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage-1]], + world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]], world.player).parent_region.add_exits([first_rooms[proper_stage].name]) level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name]) @@ -141,8 +147,7 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage) or (enforce_pattern == enforce_world) ] - new_stage = generate_valid_level(level, stage, stage_candidates, - world.random) + new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level]) possible_stages.remove(new_stage) levels[level][stage] = new_stage except Exception: @@ -218,7 +223,7 @@ def create_levels(world: "KDL3World") -> None: level_shuffle == 1, level_shuffle == 2) - generate_rooms(world, False, levels) + generate_rooms(world, levels) level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location) diff --git a/worlds/kdl3/Rules.py b/worlds/kdl3/Rules.py index 91abc21d0623..6a85ef84f054 100644 --- a/worlds/kdl3/Rules.py +++ b/worlds/kdl3/Rules.py @@ -264,7 +264,7 @@ def set_rules(world: "KDL3World") -> None: for r in [range(1, 31), range(44, 51)]: for i in r: set_rule(world.multiworld.get_location(f"Cloudy Park 4 - Star {i}", world.player), - lambda state: can_reach_clean(state, world.player)) + lambda state: can_reach_coo(state, world.player)) for i in [18, *list(range(20, 25))]: set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player), lambda state: can_reach_ice(state, world.player)) diff --git a/worlds/kdl3/docs/en_Kirby's Dream Land 3.md b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md index c1e36fed546a..008ee0fcc1ee 100644 --- a/worlds/kdl3/docs/en_Kirby's Dream Land 3.md +++ b/worlds/kdl3/docs/en_Kirby's Dream Land 3.md @@ -1,8 +1,8 @@ # Kirby's Dream Land 3 -## Where is the settings page? +## Where is the options page? -The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +The [player options page for this game](../player-options) contains all the options you need to configure and export a config file. ## What does randomization do to this game? @@ -15,6 +15,7 @@ as Heart Stars, 1-Ups, and Invincibility Candy will be shuffled into the pool fo - Purifying a boss after acquiring a certain number of Heart Stars (indicated by their portrait flashing in the level select) - If enabled, 1-Ups and Maxim Tomatoes +- If enabled, every single Star Piece within a stage ## When the player receives an item, what happens? A sound effect will play, and Kirby will immediately receive the effects of that item, such as being able to receive Copy Abilities from enemies that diff --git a/worlds/kdl3/docs/setup_en.md b/worlds/kdl3/docs/setup_en.md index a13a0f1a74cf..a73d248d4d18 100644 --- a/worlds/kdl3/docs/setup_en.md +++ b/worlds/kdl3/docs/setup_en.md @@ -43,8 +43,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a config file? -The [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page on the website allows you to configure -your personal settings and export a config file from them. +The [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page on the website allows you to configure +your personal options and export a config file from them. ### Verifying your config file @@ -53,7 +53,7 @@ If you would like to validate your config file to make sure it works, you may do ## Generating a Single-Player Game -1. Navigate to the [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page, configure your options, +1. Navigate to the [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page, configure your options, and click the "Generate Game" button. 2. You will be presented with a "Seed Info" page. 3. Click the "Create New Room" link. diff --git a/worlds/kdl3/test/test_locations.py b/worlds/kdl3/test/test_locations.py index 543f0d83926d..433b4534d1e5 100644 --- a/worlds/kdl3/test/test_locations.py +++ b/worlds/kdl3/test/test_locations.py @@ -33,7 +33,8 @@ def test_simple_heart_stars(self): self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"]) self.run_location_test(LocationName.iceberg_samus, ["Ice"]) self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"]) - self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"]) + self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", + "Stone", "Ice"]) def run_location_test(self, location: str, itempool: typing.List[str]): items = itempool.copy() From 74ac66b03228988d0885cff556f962a04873cc54 Mon Sep 17 00:00:00 2001 From: BadMagic100 Date: Thu, 28 Mar 2024 08:49:19 -0700 Subject: [PATCH 50/74] Hollow Knight: 0.4.5 doc revamp and default options tweaks (#2982) Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com> --- worlds/hk/Options.py | 47 +++++++++++++++++++++--------- worlds/hk/docs/en_Hollow Knight.md | 4 ++- worlds/hk/docs/setup_en.md | 44 ++++++++++++++-------------- 3 files changed, 58 insertions(+), 37 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 21e8c179e82e..70c7c1689661 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -1,4 +1,5 @@ import typing +import re from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms @@ -11,12 +12,16 @@ else: Random = typing.Any - locations = {"option_" + start: i for i, start in enumerate(starts)} # This way the dynamic start names are picked up by the MetaClass Choice belongs to -StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations, - "__doc__": "Choose your start location. " - "This is currently only locked to King's Pass."}) +StartLocation = type("StartLocation", (Choice,), { + "__module__": __name__, + "auto_display_name": False, + "display_name": "Start Location", + "__doc__": "Choose your start location. " + "This is currently only locked to King's Pass.", + **locations, +}) del (locations) option_docstrings = { @@ -49,8 +54,7 @@ "RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item " "pool and open their locations\n for randomization.", "RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.", - "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization." - "Mimic Grubs are always placed\n in your own game.", + "RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization.", "RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see" " and buy an item\n that is randomized into that location as well.", "RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items " @@ -99,8 +103,12 @@ "RandomizeKeys", "RandomizeMaskShards", "RandomizeVesselFragments", + "RandomizeCharmNotches", "RandomizePaleOre", - "RandomizeRelics" + "RandomizeRancidEggs" + "RandomizeRelics", + "RandomizeStags", + "RandomizeLifebloodCocoons" } shop_to_option = { @@ -117,6 +125,7 @@ hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {} +splitter_pattern = re.compile(r'(? typing.List[int]: random_source.shuffle(charms) return charms else: - charms = [0]*self.charm_count + charms = [0] * self.charm_count for x in range(self.value): - index = random_source.randint(0, self.charm_count-1) + index = random_source.randint(0, self.charm_count - 1) while charms[index] > 5: - index = random_source.randint(0, self.charm_count-1) + index = random_source.randint(0, self.charm_count - 1) charms[index] += 1 return charms @@ -404,6 +417,7 @@ class WhitePalace(Choice): class ExtraPlatforms(DefaultOnToggle): """Places additional platforms to make traveling throughout Hallownest more convenient.""" + display_name = "Extra Platforms" class AddUnshuffledLocations(Toggle): @@ -413,6 +427,7 @@ class AddUnshuffledLocations(Toggle): Note: This will increase the number of location checks required to purchase hints to the total maximum. """ + display_name = "Add Unshuffled Locations" class DeathLinkShade(Choice): @@ -430,6 +445,7 @@ class DeathLinkShade(Choice): option_shadeless = 1 option_shade = 2 default = 2 + display_name = "Deathlink Shade Handling" class DeathLinkBreaksFragileCharms(Toggle): @@ -439,6 +455,7 @@ class DeathLinkBreaksFragileCharms(Toggle): ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it will continue to do so. """ + display_name = "Deathlink Breaks Fragile Charms" class StartingGeo(Range): @@ -462,18 +479,20 @@ class CostSanity(Choice): alias_yes = 1 option_shopsonly = 2 option_notshops = 3 - display_name = "Cost Sanity" + display_name = "Costsanity" class CostSanityHybridChance(Range): """The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence""" range_end = 100 default = 10 + display_name = "Costsanity Hybrid Chance" cost_sanity_weights: typing.Dict[str, type(Option)] = {} for term, cost in cost_terms.items(): option_name = f"CostSanity{cost.option}Weight" + display_name = f"Costsanity {cost.option} Weight" extra_data = { "__module__": __name__, "range_end": 1000, "__doc__": ( @@ -486,10 +505,10 @@ class CostSanityHybridChance(Range): extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop." option = type(option_name, (Range,), extra_data) + option.display_name = display_name globals()[option.__name__] = option cost_sanity_weights[option.__name__] = option - hollow_knight_options: typing.Dict[str, type(Option)] = { **hollow_knight_randomize_options, RandomizeElevatorPass.__name__: RandomizeElevatorPass, diff --git a/worlds/hk/docs/en_Hollow Knight.md b/worlds/hk/docs/en_Hollow Knight.md index e31eb892a004..94398ec6ac14 100644 --- a/worlds/hk/docs/en_Hollow Knight.md +++ b/worlds/hk/docs/en_Hollow Knight.md @@ -8,7 +8,9 @@ config file. ## What does randomization do to this game? Randomization swaps around the locations of items. The items being swapped around are chosen within your YAML. -Shop costs are presently always randomized. +Shop costs are presently always randomized. Items which could be randomized, but are not, will remain unmodified in +their usual locations. In particular, when the items at Grubfather and Seer are partially randomized, randomized items +will be obtained from a chest in the room, while unrandomized items will be given by the NPC as normal. ## What Hollow Knight items can appear in other players' worlds? diff --git a/worlds/hk/docs/setup_en.md b/worlds/hk/docs/setup_en.md index b85818f30eca..c046785038d8 100644 --- a/worlds/hk/docs/setup_en.md +++ b/worlds/hk/docs/setup_en.md @@ -3,6 +3,8 @@ ## Required Software * Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/). * A legal copy of Hollow Knight. + * Steam, Gog, and Xbox Game Pass versions of the game are supported. + * Windows, Mac, and Linux (including Steam Deck) are supported. ## Installing the Archipelago Mod using Lumafly 1. Launch Lumafly and ensure it locates your Hollow Knight installation directory. @@ -10,25 +12,25 @@ * If desired, also install "Archipelago Map Mod" to use as an in-game tracker. 3. Launch the game, you're all set! -### What to do if Lumafly fails to find your XBox Game Pass installation directory -1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. -2. Click the three points then click "Manage". -3. Go to the "Files" tab and select "Browse...". -4. Click "Hollow Knight", then "Content", then click the path bar and copy it. -5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4. - -#### Alternative Method: -1. Click on your profile then "Settings". -2. Go to the "General" tab and select "CHANGE FOLDER". -3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path. -4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3. - -Note: The path folder needs to have the "Hollow Knight_Data" folder inside. +### What to do if Lumafly fails to find your installation directory +1. Find the directory manually. + * Xbox Game Pass: + 1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar. + 2. Click the three points then click "Manage". + 3. Go to the "Files" tab and select "Browse...". + 4. Click "Hollow Knight", then "Content", then click the path bar and copy it. + * Steam: + 1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where + it is. Find your steam library and then find the Hollow Knight folder and copy the path. + * Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight` + * Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight + * Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app +2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied. ## Configuring your YAML File ### What is a YAML and why do I need one? -You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn -about why Archipelago uses YAML files and what they're for. +An YAML file is the way that you provide your player options to Archipelago. +See the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn more. ### Where do I get a YAML? You can use the [game options page for Hollow Knight](/games/Hollow%20Knight/player-options) here on the Archipelago @@ -44,9 +46,7 @@ website to generate a YAML using a graphical interface. * If you are waiting for a countdown then wait for it to lapse before hitting Start. * Or hit Start then pause the game once you're in it. -## Commands -While playing the multiworld you can interact with the server using various commands listed in the -[commands guide](/tutorial/Archipelago/commands/en). As this game does not have an in-game text client at the moment, -You can optionally connect to the multiworld using the text client, which can be found in the -[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases) as Archipelago Text Client to -enter these commands. +## Hints and other commands +While playing in a multiworld, you can interact with the server using various commands listed in the +[commands guide](/tutorial/Archipelago/commands/en). You can use the Archipelago Text Client to do this, +which is included in the latest release of the [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest). From 74b2bf51613a968eb57a5b138a7ad191324b2dd8 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Mar 2024 15:20:55 -0600 Subject: [PATCH 51/74] Pokemon Emerald: Exclude norman trainer location during norman goal (#3038) --- worlds/pokemon_emerald/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 384bec9f4501..65bcce3a3296 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -300,6 +300,7 @@ def exclude_locations(location_names: List[str]): # Locations which are directly unlocked by defeating Norman. exclude_locations([ + "Petalburg Gym - Leader Norman", "Petalburg Gym - Balance Badge", "Petalburg Gym - TM42 from Norman", "Petalburg City - HM03 from Wally's Uncle", From de860623d17d274289e3e4ab13650f2382e2e0b8 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Mar 2024 22:21:56 +0100 Subject: [PATCH 52/74] Core: differentiate between unknown worlds and broken worlds in error message (#2903) --- Generate.py | 7 ++++++- worlds/__init__.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index 56979334b547..f646e994dcac 100644 --- a/Generate.py +++ b/Generate.py @@ -26,6 +26,7 @@ from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister from worlds.generic import PlandoConnection +from worlds import failed_world_loads def mystery_argparse(): @@ -458,7 +459,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b ret.game = get_choice("game", weights) if ret.game not in AutoWorldRegister.world_types: - picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0] + picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0] + if picks[0] in failed_world_loads: + raise Exception(f"No functional world found to handle game {ret.game}. " + f"Did you mean '{picks[0]}' ({picks[1]}% sure)? " + f"If so, it appears the world failed to initialize correctly.") raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? " f"Check your spelling or installation of that world.") diff --git a/worlds/__init__.py b/worlds/__init__.py index 168bba7abf41..53b0c5ceb948 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -20,9 +20,13 @@ "user_folder", "GamesPackage", "DataPackage", + "failed_world_loads", } +failed_world_loads: List[str] = [] + + class GamesPackage(TypedDict, total=False): item_name_groups: Dict[str, List[str]] item_name_to_id: Dict[str, int] @@ -87,6 +91,7 @@ def load(self) -> bool: file_like.seek(0) import logging logging.exception(file_like.read()) + failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0]) return False From eb66886a908ad75bbe71fac9bb81a0177e05e816 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 28 Mar 2024 16:23:01 -0500 Subject: [PATCH 53/74] SC2: Don't Filter Excluded Victory Locations (#3018) --- worlds/sc2/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sc2/Locations.py b/worlds/sc2/Locations.py index 5a03f2232348..22b400a238dd 100644 --- a/worlds/sc2/Locations.py +++ b/worlds/sc2/Locations.py @@ -1620,7 +1620,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]: plando_locations = get_plando_locations(world) exclude_locations = get_option_value(world, "exclude_locations") location_table = [location for location in location_table - if (LocationType is LocationType.VICTORY or location.name not in exclude_locations) + if (location.type is LocationType.VICTORY or location.name not in exclude_locations) and location.type not in excluded_location_types or location.name in plando_locations] for i, location_data in enumerate(location_table): From c97215e0e755224593fdd00894731b59aa415e19 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 28 Mar 2024 17:23:37 -0400 Subject: [PATCH 54/74] TUNIC: Minor refactor of the vanilla_portals function (#3009) * Remove unused, change an if to an elif * Remove unused import --- worlds/tunic/er_scripts.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 5756ec90be14..5d08188ace6e 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Set, Tuple, TYPE_CHECKING +from typing import Dict, List, Set, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table from .er_data import Portal, tunic_er_regions, portal_mapping, \ @@ -89,7 +89,6 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None: def vanilla_portals() -> Dict[Portal, Portal]: portal_pairs: Dict[Portal, Portal] = {} portal_map = portal_mapping.copy() - shop_num = 1 while portal_map: portal1 = portal_map[0] @@ -98,11 +97,10 @@ def vanilla_portals() -> Dict[Portal, Portal]: portal2_sdt = portal1.destination_scene() if portal2_sdt.startswith("Shop,"): - portal2 = Portal(name=f"Shop", region="Shop", + portal2 = Portal(name="Shop", region="Shop", destination="Previous Region", tag="_") - shop_num += 1 - if portal2_sdt == "Purgatory, Purgatory_bottom": + elif portal2_sdt == "Purgatory, Purgatory_bottom": portal2_sdt = "Purgatory, Purgatory_top" for portal in portal_map: From 5d9d4ed9f1e44309f1b53f12413ad260f1b6c983 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 29 Mar 2024 01:01:31 +0100 Subject: [PATCH 55/74] SoE: update to pyevermizer v0.48.0 (#3050) --- worlds/soe/__init__.py | 52 ++++++++--- worlds/soe/logic.py | 7 +- worlds/soe/options.py | 25 +++++- worlds/soe/requirements.txt | 72 +++++++-------- worlds/soe/test/__init__.py | 2 + worlds/soe/test/test_sniffamizer.py | 130 ++++++++++++++++++++++++++++ 6 files changed, 237 insertions(+), 51 deletions(-) create mode 100644 worlds/soe/test/test_sniffamizer.py diff --git a/worlds/soe/__init__.py b/worlds/soe/__init__.py index bbe018da5329..dcca722ad1fe 100644 --- a/worlds/soe/__init__.py +++ b/worlds/soe/__init__.py @@ -13,7 +13,7 @@ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import add_item_rule, set_rule from .logic import SoEPlayerLogic -from .options import Difficulty, EnergyCore, SoEOptions +from .options import Difficulty, EnergyCore, Sniffamizer, SniffIngredients, SoEOptions from .patch import SoEDeltaPatch, get_base_rom_path if typing.TYPE_CHECKING: @@ -64,20 +64,28 @@ pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499 pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399 pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499 - # TODO: sniff 64500..64799 + # blank 64500..64799 pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899 pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999 + pyevermizer.CHECK_SNIFF: _id_base + 1000 # sniff 65000..65592 } # cache native evermizer items and locations _items = pyevermizer.get_items() +_sniff_items = pyevermizer.get_sniff_items() # optional, not part of the default location pool _traps = pyevermizer.get_traps() _extras = pyevermizer.get_extra_items() # items that are not placed by default _locations = pyevermizer.get_locations() +_sniff_locations = pyevermizer.get_sniff_locations() # optional, not part of the default location pool # fix up texts for AP for _loc in _locations: if _loc.type == pyevermizer.CHECK_GOURD: - _loc.name = f'{_loc.name} #{_loc.index}' + _loc.name = f"{_loc.name} #{_loc.index}" +for _loc in _sniff_locations: + if _loc.type == pyevermizer.CHECK_SNIFF: + _loc.name = f"{_loc.name} Sniff #{_loc.index}" +del _loc + # item helpers _ingredients = ( 'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron', @@ -97,7 +105,7 @@ def _match_item_name(item: pyevermizer.Item, substr: str) -> bool: def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]: name_to_id = {} id_to_raw = {} - for loc in _locations: + for loc in itertools.chain(_locations, _sniff_locations): ap_id = _id_offset[loc.type] + loc.index id_to_raw[ap_id] = loc name_to_id[loc.name] = ap_id @@ -108,7 +116,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]: name_to_id = {} id_to_raw = {} - for item in itertools.chain(_items, _extras, _traps): + for item in itertools.chain(_items, _sniff_items, _extras, _traps): if item.name in name_to_id: continue ap_id = _id_offset[item.type] + item.index @@ -168,9 +176,9 @@ class SoEWorld(World): options: SoEOptions settings: typing.ClassVar[SoESettings] topology_present = False - data_version = 4 + data_version = 5 web = SoEWebWorld() - required_client_version = (0, 3, 5) + required_client_version = (0, 4, 4) item_name_to_id, item_id_to_raw = _get_item_mapping() location_name_to_id, location_id_to_raw = _get_location_mapping() @@ -238,16 +246,26 @@ def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int: spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, loc.difficulty > max_difficulty)) + # extend pool if feature and setting enabled + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + for loc in _sniff_locations: + spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append( + SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame, + loc.difficulty > max_difficulty)) # location balancing data trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = { - 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1 - 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2 + 0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40), # remove up to 40 gourds from sphere 1 + pyevermizer.CHECK_SNIFF: (100, 130, 130, 130)}, # remove up to 130 sniff spots from sphere 1 + 1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90), # remove up to 90 gourds from sphere 2 + pyevermizer.CHECK_SNIFF: (160, 200, 200, 200)}, # remove up to 200 sniff spots from sphere 2 } # mark some as excluded based on numbers above for trash_sphere, fills in trash_fills.items(): for typ, counts in fills.items(): + if typ not in spheres[trash_sphere]: + continue # e.g. player does not have sniff locations count = counts[self.options.difficulty.value] for location in self.random.sample(spheres[trash_sphere][typ], count): assert location.name != "Energy Core #285", "Error in sphere generation" @@ -299,6 +317,15 @@ def create_items(self) -> None: # remove one pair of wings that will be placed in generate_basic items.remove(self.create_item("Wings")) + # extend pool if feature and setting enabled + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + if self.options.sniff_ingredients == SniffIngredients.option_vanilla_ingredients: + # vanilla ingredients + items += list(map(lambda item: self.create_item(item), _sniff_items)) + else: + # random ingredients + items += [self.create_item(self.get_filler_item_name()) for _ in _sniff_items] + def is_ingredient(item: pyevermizer.Item) -> bool: for ingredient in _ingredients: if _match_item_name(item, ingredient): @@ -345,7 +372,12 @@ def set_rules(self) -> None: set_rule(self.multiworld.get_location('Done', self.player), lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS)) set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True) - for loc in _locations: + locations: typing.Iterable[pyevermizer.Location] + if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere: + locations = itertools.chain(_locations, _sniff_locations) + else: + locations = _locations + for loc in locations: location = self.multiworld.get_location(loc.name, self.player) set_rule(location, self.make_rule(loc.requires)) diff --git a/worlds/soe/logic.py b/worlds/soe/logic.py index ee81c76e58de..92ffb14b3f95 100644 --- a/worlds/soe/logic.py +++ b/worlds/soe/logic.py @@ -1,4 +1,5 @@ import typing +from itertools import chain from typing import Callable, Set from . import pyevermizer @@ -11,10 +12,12 @@ # TODO: resolve/flatten/expand rules to get rid of recursion below where possible # Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items) -rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0] +rules = pyevermizer.get_logic() # Logic.items are all items and extra items excluding non-progression items and duplicates +# NOTE: we are skipping sniff items here because none of them is supposed to provide progression item_names: Set[str] = set() -items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items()) +items = [item for item in filter(lambda item: item.progression, # type: ignore[arg-type] + chain(pyevermizer.get_items(), pyevermizer.get_extra_items())) if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value] diff --git a/worlds/soe/options.py b/worlds/soe/options.py index c5ac02c22d03..5ecd0f9e6666 100644 --- a/worlds/soe/options.py +++ b/worlds/soe/options.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, fields +from datetime import datetime from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \ @@ -158,13 +159,30 @@ class Ingredienizer(EvermizerFlags, OffOnFullChoice): flags = ['i', '', 'I'] -class Sniffamizer(EvermizerFlags, OffOnFullChoice): - """On Shuffles, Full randomizes drops in sniff locations""" +class Sniffamizer(EvermizerFlags, Choice): + """ + Off: all vanilla items in sniff spots + Shuffle: sniff items shuffled into random sniff spots + """ display_name = "Sniffamizer" + option_off = 0 + option_shuffle = 1 + if datetime.today().year > 2024 or datetime.today().month > 3: + option_everywhere = 2 + __doc__ = __doc__ + " Everywhere: add sniff spots to multiworld pool" + alias_true = 1 default = 1 flags = ['s', '', 'S'] +class SniffIngredients(EvermizerFlag, Choice): + """Select which items should be used as sniff items""" + display_name = "Sniff Ingredients" + option_vanilla_ingredients = 0 + option_random_ingredients = 1 + flag = 'v' + + class Callbeadamizer(EvermizerFlags, OffOnFullChoice): """On Shuffles call bead characters, Full shuffles individual spells""" display_name = "Callbeadamizer" @@ -207,7 +225,7 @@ def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemC attrs["display_name"] = f"{attrs['item_name']} Chance" attrs["range_start"] = 0 attrs["range_end"] = 100 - cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) + cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) # type: ignore[no-untyped-call] return cast(ItemChanceMeta, cls) @@ -268,6 +286,7 @@ class SoEOptions(PerGameCommonOptions): short_boss_rush: ShortBossRush ingredienizer: Ingredienizer sniffamizer: Sniffamizer + sniff_ingredients: SniffIngredients callbeadamizer: Callbeadamizer musicmizer: Musicmizer doggomizer: Doggomizer diff --git a/worlds/soe/requirements.txt b/worlds/soe/requirements.txt index 710f51ddb09a..4bcacb33c33c 100644 --- a/worlds/soe/requirements.txt +++ b/worlds/soe/requirements.txt @@ -1,36 +1,36 @@ -pyevermizer==0.46.1 \ - --hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \ - --hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \ - --hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \ - --hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \ - --hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \ - --hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \ - --hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \ - --hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \ - --hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \ - --hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \ - --hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \ - --hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \ - --hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \ - --hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \ - --hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \ - --hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \ - --hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \ - --hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \ - --hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \ - --hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \ - --hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \ - --hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \ - --hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \ - --hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \ - --hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \ - --hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \ - --hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \ - --hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \ - --hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \ - --hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \ - --hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \ - --hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \ - --hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \ - --hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \ - --hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae +pyevermizer==0.48.0 \ + --hash=sha256:069ce348e480e04fd6208cfd0f789c600b18d7c34b5272375b95823be191ed57 \ + --hash=sha256:58164dddaba2f340b0a8b4f39605e9dac46d8b0ffb16120e2e57bef2bfc1d683 \ + --hash=sha256:115dd09d38a10f11d4629b340dfd75e2ba4089a1ff9e9748a11619829e02c876 \ + --hash=sha256:b5e79cfe721e75cd7dec306b5eecd6385ce059e31ef7523ba7f677e22161ec6f \ + --hash=sha256:382882fa9d641b9969a6c3ed89449a814bdabcb6b17b558872d95008a6cc908b \ + --hash=sha256:92f67700e9132064a90858d391dd0b8fb111aff6dfd472befed57772d89ae567 \ + --hash=sha256:fe4c453b7dbd5aa834b81f9a7aedb949a605455650b938b8b304d8e5a7edcbf7 \ + --hash=sha256:c6bdbc45daf73818f763ed59ad079f16494593395d806f772dd62605c722b3e9 \ + --hash=sha256:bb09f45448fdfd28566ae6fcc38c35a6632f4c31a9de2483848f6ce17b2359b5 \ + --hash=sha256:00a8b9014744bd1528d0d39c33ede7c0d1713ad797a331cebb33d377a5bc1064 \ + --hash=sha256:64ee69edc0a7d3b3caded78f2e46975f9beaff1ff8feaf29b87da44c45f38d7d \ + --hash=sha256:9211bdb1313e9f4869ed5bdc61f3831d39679bd08bb4087f1c1e5475d9e3018b \ + --hash=sha256:4a57821e422a1d75fe3307931a78db7a65e76955f8e401c4b347db6570390d09 \ + --hash=sha256:04670cee0a0b913f24d2b9a1e771781560e2485bda31e6cd372a08421cf85cfa \ + --hash=sha256:971fe77d0a20a1db984020ad253b613d0983f5e23ff22cba60ee5ac00d8128de \ + --hash=sha256:127265fdb49f718f54706bf15604af1cec23590afd00d423089dea4331dcfc61 \ + --hash=sha256:d47576360337c1a23f424cd49944a8d68fc4f3338e00719c9f89972c84604bef \ + --hash=sha256:879659603e51130a0de8d9885d815a2fa1df8bd6cebe6d520d1c6002302adfdb \ + --hash=sha256:6a91bfc53dd130db6424adf8ac97a1133e97b4157ed00f889d8cbd26a2a4b340 \ + --hash=sha256:f3bf35fc5eef4cda49d2de77339fc201dd3206660a3dc15db005625b15bb806c \ + --hash=sha256:e7c8d5bf59a3c16db20411bc5d8e9c9087a30b6b4edf1b5ed9f4c013291427e4 \ + --hash=sha256:054a4d84ffe75448d41e88e1e0642ef719eb6111be5fe608e71e27a558c59069 \ + --hash=sha256:e6f141ca367469c69ba7fbf65836c479ec6672c598cfcb6b39e8098c60d346bc \ + --hash=sha256:6e65eb88f0c1ff4acde1c13b24ce649b0fe3d1d3916d02d96836c781a5022571 \ + --hash=sha256:e61e8f476b6da809cf38912755ed8bb009665f589e913eb8df877e9fa763024b \ + --hash=sha256:7e7c5484c0a2e3da6064de3f73d8d988d6703db58ab0be4730cbbf1a82319237 \ + --hash=sha256:9033b954e5f4878fd94af6d2056c78e3316115521fb1c24a4416d5cbf2ad66ad \ + --hash=sha256:824c623fff8ae4da176306c458ad63ad16a06a495a16db700665eca3c115924f \ + --hash=sha256:8e31031409a8386c6a63b79d480393481badb3ba29f32ff7a0db2b4abed20ac8 \ + --hash=sha256:7dbb7bb13e1e94f69f7ccdbcf4d35776424555fce5af1ca29d0256f91fdf087a \ + --hash=sha256:3a24e331b259407b6912d6e0738aa8a675831db3b7493fcf54dc17cb0cb80d37 \ + --hash=sha256:fdda06662a994271e96633cba100dd92b2fcd524acef8b2f664d1aaa14503cbd \ + --hash=sha256:0f0fc81bef3dbb78ba6a7622dd4296f23c59825968a0bb0448beb16eb3397cc2 \ + --hash=sha256:e07cbef776a7468669211546887357cc88e9afcf1578b23a4a4f2480517b15d9 \ + --hash=sha256:e442212695bdf60e455673b7b9dd83a5d4b830d714376477093d2c9054d92832 diff --git a/worlds/soe/test/__init__.py b/worlds/soe/test/__init__.py index 1ab852163053..e84d7e669d2c 100644 --- a/worlds/soe/test/__init__.py +++ b/worlds/soe/test/__init__.py @@ -1,9 +1,11 @@ from test.bases import WorldTestBase from typing import Iterable +from .. import SoEWorld class SoETestBase(WorldTestBase): game = "Secret of Evermore" + world: SoEWorld def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (), satisfied: bool = True) -> None: diff --git a/worlds/soe/test/test_sniffamizer.py b/worlds/soe/test/test_sniffamizer.py new file mode 100644 index 000000000000..45aff1fa4d6d --- /dev/null +++ b/worlds/soe/test/test_sniffamizer.py @@ -0,0 +1,130 @@ +import typing +from unittest import TestCase, skipUnless + +from . import SoETestBase +from .. import pyevermizer +from ..options import Sniffamizer + + +class TestCount(TestCase): + """ + Test that counts line up for sniff spots + """ + + def test_compare_counts(self) -> None: + self.assertEqual(len(pyevermizer.get_sniff_locations()), len(pyevermizer.get_sniff_items()), + "Sniff locations and sniff items don't line up") + + +class Bases: + # class in class to avoid running tests for helper class + class TestSniffamizerLocal(SoETestBase): + """ + Test that provided options do not add sniff items or locations + """ + def test_no_sniff_items(self) -> None: + self.assertLess(len(self.multiworld.itempool), 500, + "Unexpected number of items") + for item in self.multiworld.itempool: + if item.code is not None: + self.assertLess(item.code, 65000, + "Unexpected item type") + + def test_no_sniff_locations(self) -> None: + location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None) + self.assertLess(location_count, 500, + "Unexpected number of locations") + for location in self.multiworld.get_locations(self.player): + if location.address is not None: + self.assertLess(location.address, 65000, + "Unexpected location type") + self.assertEqual(location_count, len(self.multiworld.itempool), + "Locations and item counts do not line up") + + class TestSniffamizerPool(SoETestBase): + """ + Test that provided options add sniff items and locations + """ + def test_sniff_items(self) -> None: + self.assertGreater(len(self.multiworld.itempool), 500, + "Unexpected number of items") + + def test_sniff_locations(self) -> None: + location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None) + self.assertGreater(location_count, 500, + "Unexpected number of locations") + self.assertTrue(any(location.address is not None and location.address >= 65000 + for location in self.multiworld.get_locations(self.player)), + "No sniff locations") + self.assertEqual(location_count, len(self.multiworld.itempool), + "Locations and item counts do not line up") + + +class TestSniffamizerShuffle(Bases.TestSniffamizerLocal): + """ + Test that shuffle does not add extra items or locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "shuffle" + } + + def test_flags(self) -> None: + # default -> no flags + flags = self.world.options.flags + self.assertNotIn("s", flags) + self.assertNotIn("S", flags) + self.assertNotIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class TestSniffamizerEverywhereVanilla(Bases.TestSniffamizerPool): + """ + Test that everywhere + vanilla ingredients does add extra items and locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + "sniff_ingredients": "vanilla_ingredients", + } + + def test_flags(self) -> None: + flags = self.world.options.flags + self.assertIn("S", flags) + self.assertNotIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class TestSniffamizerEverywhereRandom(Bases.TestSniffamizerPool): + """ + Test that everywhere + random ingredients also adds extra items and locations + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + "sniff_ingredients": "random_ingredients", + } + + def test_flags(self) -> None: + flags = self.world.options.flags + self.assertIn("S", flags) + self.assertIn("v", flags) + + +@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled") +class EverywhereAccessTest(SoETestBase): + """ + Test that everywhere has certain rules + """ + options: typing.Dict[str, typing.Any] = { + "sniffamizer": "everywhere", + } + + @staticmethod + def _resolve_numbers(spots: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]: + return [f"{name} #{number}" for name, numbers in spots.items() for number in numbers] + + def test_knight_basher(self) -> None: + locations = ["Mungola", "Lightning Storm"] + self._resolve_numbers({ + "Gomi's Tower Sniff": range(473, 491), + "Gomi's Tower": range(195, 199), + }) + items = [["Knight Basher"]] + self.assertAccessDependency(locations, items) From 4391d1f4c13cdf2295481d8c51f9ef8f58bf8347 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Mar 2024 18:05:39 -0600 Subject: [PATCH 56/74] Pokemon Emerald: Fix opponents learning non-randomized TMs (#3025) --- worlds/pokemon_emerald/__init__.py | 16 ++++++++-------- worlds/pokemon_emerald/opponents.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/worlds/pokemon_emerald/__init__.py b/worlds/pokemon_emerald/__init__.py index 65bcce3a3296..c7f060a72969 100644 --- a/worlds/pokemon_emerald/__init__.py +++ b/worlds/pokemon_emerald/__init__.py @@ -569,14 +569,6 @@ def generate_output(self, output_directory: str) -> None: self.modified_misc_pokemon = copy.deepcopy(emerald_data.misc_pokemon) self.modified_starters = copy.deepcopy(emerald_data.starters) - randomize_abilities(self) - randomize_learnsets(self) - randomize_tm_hm_compatibility(self) - randomize_legendary_encounters(self) - randomize_misc_pokemon(self) - randomize_opponent_parties(self) - randomize_starters(self) - # Modify catch rate min_catch_rate = min(self.options.min_catch_rate.value, 255) for species in self.modified_species.values(): @@ -591,6 +583,14 @@ def generate_output(self, output_directory: str) -> None: new_moves.add(new_move) self.modified_tmhm_moves[i] = new_move + randomize_abilities(self) + randomize_learnsets(self) + randomize_tm_hm_compatibility(self) + randomize_legendary_encounters(self) + randomize_misc_pokemon(self) + randomize_opponent_parties(self) + randomize_starters(self) + create_patch(self, output_directory) del self.modified_trainers diff --git a/worlds/pokemon_emerald/opponents.py b/worlds/pokemon_emerald/opponents.py index f485282515a1..09e947546d7c 100644 --- a/worlds/pokemon_emerald/opponents.py +++ b/worlds/pokemon_emerald/opponents.py @@ -80,7 +80,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None: per_species_tmhm_moves[new_species.species_id] = sorted({ world.modified_tmhm_moves[i] for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility)) - if is_compatible + if is_compatible and world.modified_tmhm_moves[i] not in world.blacklisted_moves }) # TMs and HMs compatible with the species From 9dc708978bd00890afcd3426f829a5ac53cbe136 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Thu, 28 Mar 2024 18:26:58 -0600 Subject: [PATCH 57/74] Hylics 2: Fix invalid multiworld data, use `self.random` instead of `self.multiworld.random` (#3001) * Hylics 2: Fixes * Rewrite loop --- worlds/hylics2/__init__.py | 79 +++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 44 deletions(-) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index 1c51bacc5d67..cb7ae4498279 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -34,8 +34,6 @@ class Hylics2World(World): location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()} option_definitions = Options.hylics2_options - topology_present: bool = True - data_version = 3 start_location = "Waynehouse" @@ -51,10 +49,6 @@ def create_item(self, name: str) -> "Hylics2Item": return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player) - def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item": - return Hylics2Item(name, classification, code, self.player) - - def create_event(self, event: str): return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player) @@ -62,7 +56,7 @@ def create_event(self, event: str): # set random starting location if option is enabled def generate_early(self): if self.multiworld.random_start[self.player]: - i = self.multiworld.random.randint(0, 3) + i = self.random.randint(0, 3) if i == 0: self.start_location = "Waynehouse" elif i == 1: @@ -77,26 +71,26 @@ def create_items(self): pool = [] # add regular items - for i, data in Items.item_table.items(): - if data["count"] > 0: - for j in range(data["count"]): - pool.append(self.add_item(data["name"], data["classification"], i)) + for item in Items.item_table.values(): + if item["count"] > 0: + for _ in range(item["count"]): + pool.append(self.create_item(item["name"])) # add party members if option is enabled if self.multiworld.party_shuffle[self.player]: - for i, data in Items.party_item_table.items(): - pool.append(self.add_item(data["name"], data["classification"], i)) + for item in Items.party_item_table.values(): + pool.append(self.create_item(item["name"])) # handle gesture shuffle if not self.multiworld.gesture_shuffle[self.player]: # add gestures to pool like normal - for i, data in Items.gesture_item_table.items(): - pool.append(self.add_item(data["name"], data["classification"], i)) + for item in Items.gesture_item_table.values(): + pool.append(self.create_item(item["name"])) # add '10 Bones' items if medallion shuffle is enabled if self.multiworld.medallion_shuffle[self.player]: - for i, data in Items.medallion_item_table.items(): - for j in range(data["count"]): - pool.append(self.add_item(data["name"], data["classification"], i)) + for item in Items.medallion_item_table.values(): + for _ in range(item["count"]): + pool.append(self.create_item(item["name"])) # add to world's pool self.multiworld.itempool += pool @@ -107,48 +101,45 @@ def pre_fill(self): if self.multiworld.gesture_shuffle[self.player] == 2: # vanilla locations gestures = Items.gesture_item_table self.multiworld.get_location("Waynehouse: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678)) + .place_locked_item(self.create_item("POROMER BLEB")) self.multiworld.get_location("Afterlife: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683)) + .place_locked_item(self.create_item("TELEDENUDATE")) self.multiworld.get_location("New Muldul: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679)) + .place_locked_item(self.create_item("SOUL CRISPER")) self.multiworld.get_location("Viewax's Edifice: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680)) + .place_locked_item(self.create_item("TIME SIGIL")) self.multiworld.get_location("TV Island: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681)) + .place_locked_item(self.create_item("CHARGE UP")) self.multiworld.get_location("Juice Ranch: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682)) + .place_locked_item(self.create_item("FATE SANDBOX")) self.multiworld.get_location("Foglast: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684)) + .place_locked_item(self.create_item("LINK MOLLUSC")) self.multiworld.get_location("Drill Castle: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688)) + .place_locked_item(self.create_item("NEMATODE INTERFACE")) self.multiworld.get_location("Sage Airship: TV", self.player)\ - .place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685)) + .place_locked_item(self.create_item("BOMBO - GENESIS")) elif self.multiworld.gesture_shuffle[self.player] == 1: # TVs only - gestures = list(Items.gesture_item_table.items()) - tvs = list(Locations.tv_location_table.items()) + gestures = [gesture["name"] for gesture in Items.gesture_item_table.values()] + tvs = [tv["name"] for tv in Locations.tv_location_table.values()] # if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get # placed at Sage Airship: TV or Foglast: TV if self.multiworld.extra_items_in_logic[self.player]: - tv = self.multiworld.random.choice(tvs) - gest = gestures.index((200681, Items.gesture_item_table[200681])) - while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV": - tv = self.multiworld.random.choice(tvs) - self.multiworld.get_location(tv[1]["name"], self.player)\ - .place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"], - gestures[gest])) - gestures.remove(gestures[gest]) + tv = self.random.choice(tvs) + while tv == "Sage Airship: TV" or tv == "Foglast: TV": + tv = self.random.choice(tvs) + self.multiworld.get_location(tv, self.player)\ + .place_locked_item(self.create_item("CHARGE UP")) + gestures.remove("CHARGE UP") tvs.remove(tv) - for i in range(len(gestures)): - gest = self.multiworld.random.choice(gestures) - tv = self.multiworld.random.choice(tvs) - self.multiworld.get_location(tv[1]["name"], self.player)\ - .place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0])) - gestures.remove(gest) - tvs.remove(tv) + self.random.shuffle(gestures) + self.random.shuffle(tvs) + while gestures: + gesture = gestures.pop() + tv = tvs.pop() + self.get_location(tv).place_locked_item(self.create_item(gesture)) def fill_slot_data(self) -> Dict[str, Any]: From 301d9de9758e360ccec5399f3f9d922f1c034e45 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 28 Mar 2024 19:31:59 -0500 Subject: [PATCH 58/74] Docs: adding games rework (#2892) * Docs: complete adding games.md rework * remove all the now unused images * review changes * address medic's review * address more comments --- docs/adding games.md | 347 +++++----------------- docs/img/creepy-castle-directory.png | Bin 35874 -> 0 bytes docs/img/gato-roboto-directory.png | Bin 81020 -> 0 bytes docs/img/heavy-bullets-data-directory.png | Bin 56842 -> 0 bytes docs/img/heavy-bullets-directory.png | Bin 38535 -> 0 bytes docs/img/stardew-valley-directory.png | Bin 66397 -> 0 bytes docs/options api.md | 9 +- 7 files changed, 82 insertions(+), 274 deletions(-) delete mode 100644 docs/img/creepy-castle-directory.png delete mode 100644 docs/img/gato-roboto-directory.png delete mode 100644 docs/img/heavy-bullets-data-directory.png delete mode 100644 docs/img/heavy-bullets-directory.png delete mode 100644 docs/img/stardew-valley-directory.png diff --git a/docs/adding games.md b/docs/adding games.md index e9f7860fc650..9d2860b4a196 100644 --- a/docs/adding games.md +++ b/docs/adding games.md @@ -1,269 +1,78 @@ -# How do I add a game to Archipelago? - -This guide is going to try and be a broad summary of how you can do just that. -There are two key steps to incorporating a game into Archipelago: - -- Game Modification -- Archipelago Server Integration - -Refer to the following documents as well: - -- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server. -- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package. - -# Game Modification - -One half of the work required to integrate a game into Archipelago is the development of the game client. This is -typically done through a modding API or other modification process, described further down. - -As an example, modifications to a game typically include (more on this later): - -- Hooking into when a 'location check' is completed. -- Networking with the Archipelago server. -- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection. - -In order to determine how to modify a game, refer to the following sections. - -## Engine Identification - -This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is -critical. The first step is to look at a game's files. Let's go over what some game files might look like. It’s -important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice. -Examples are provided below. - -### Creepy Castle - -![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png) - -This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. It’s also your worst-case -scenario as a modder. All that’s present here is an executable file and some meta-information that Steam uses. You have -basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty -nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Let’s look at some other -examples of game releases. - -### Heavy Bullets - -![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png) - -Here’s the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files. -“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually -with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing -information, credits, and general info about the game. You usually won’t find anything too helpful here, but it never -hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important. -“steam_api.dll” is a file you can safely ignore, it’s just some code used to interface with Steam. -The directory “HEAVY_BULLETS_Data”, however, has some good news. - -![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png) - -Jackpot! It might not be obvious what you’re looking at here, but I can instantly tell from this folder’s contents that -what we have is a game made in the Unity Engine. If you look in the sub-folders, you’ll seem some .dll files which -affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, -extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools -and information to help you on your journey can be found at this -[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking) - -### Stardew Valley - -![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png) - -This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways. -Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good -news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx -and MonoMod. - -### Gato Roboto - -![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png) - -Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for. -The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For -modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful. - -This isn't all you'll ever see looking at game files, but it's a good place to start. -As a general rule, the more files a game has out in plain sight, the more you'll be able to change. -This especially applies in the case of code or script files - always keep a lookout for anything you can use to your -advantage! - -## Open or Leaked Source Games - -As a side note, many games have either been made open source, or have had source files leaked at some point. -This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for -"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time. - -Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do -so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical. - -## Modifying Release Versions of Games - -However, for now we'll assume you haven't been so lucky, and have to work with only what’s sitting in your install -directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools, -but these are often not geared to the kind of work you'll be doing and may not help much. - -As a general rule, any modding tool that lets you write actual code is something worth using. - -### Research - -The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification, -it's possible other motivated parties have concocted useful tools for your game already. -Always be sure to search the Internet for the efforts of other modders. - -### Other helpful tools - -Depending on the game’s underlying engine, there may be some tools you can use either in lieu of or in addition to -existing game tools. - -#### [CheatEngine](https://cheatengine.org/) - -CheatEngine is a tool with a very long and storied history. -Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as -malware (because this behavior is most commonly found in malware and rarely used by other programs). -If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level, -including binary data formats, addressing, and assembly language programming. - -The tool itself is highly complex and even I have not yet charted its expanses. -However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever -modifying the actual game itself. -In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do -anything with it. - -### What Modifications You Should Make to the Game - -We talked about this briefly in [Game Modification](#game-modification) section. -The next step is to know what you need to make the game do now that you can modify it. Here are your key goals: - -- Know when the player has checked a location, and react accordingly -- Be able to receive items from the server on the fly -- Keep an index for items received in order to resync from disconnections -- Add interface for connecting to the Archipelago server with passwords and sessions -- Add commands for manually rewarding, re-syncing, releasing, and other actions - -Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's -servers. - -## But my Game is a console game. Can I still add it? - -That depends – what console? - -### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc - -Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised -that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright -holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console -games. - -### My Game isn’t that old, it’s for the Wii/PS2/360/etc - -This is very complex, but doable. -If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it. -There exist many disassembly and debugging tools, but more recent content may have lackluster support. - -### My Game is a classic for the SNES/Sega Genesis/etc - -That’s a lot more feasible. -There are many good tools available for understanding and modifying games on these older consoles, and the emulation -community will have figured out the bulk of the console’s secrets. -Look for debugging tools, but be ready to learn assembly. -Old consoles usually have their own unique dialects of ASM you’ll need to get used to. - -Also make sure there’s a good way to interface with a running emulator, since that’s the only way you can connect these -older consoles to the Internet. -There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a -computer, but these will require the same sort of interface software to be written in order to work properly; from your -perspective the two won't really look any different. - -### My Game is an exclusive for the Super Baby Magic Dream Boy. It’s this console from the Soviet Union that- - -Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no. -Obscurity is your enemy – there will likely be little to no emulator or modding information, and you’d essentially be -working from scratch. - -## How to Distribute Game Modifications - -**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!** - -This is a good way to get any project you're working on sued out from under you. -The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you -to copy them wholesale, is as patches. - -There are many patch formats, which I'll cover in brief. The common theme is that you can’t distribute anything that -wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding -the issue of distributing someone else’s original work. - -Users who have a copy of the game just need to apply the patch, and those who don’t are unable to play. - -### Patches - -#### IPS - -IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode -moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's -fine. - -#### UPS, BPS, VCDIFF (xdelta), bsdiff - -Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is -possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes -compression, so this format is used by APBP. - -Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to -bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp". - -#### APBP Archipelago Binary Patch - -Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional -files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the -bsdiff between the original and the randomized ROM. - -To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`. - -### Mod files - -Games which support modding will usually just let you drag and drop the mod’s files into a folder somewhere. -Mod files come in many forms, but the rules about not distributing other people's content remain the same. -They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be -generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data` -so that the users don't have to move files around in order to play. - -If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy -integration into the Webhost by inheriting from `worlds.Files.APContainer`. - -## Archipelago Integration - -In order for your game to communicate with the Archipelago server and generate the necessary randomized information, -you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations -and show the basics of a world. More in depth documentation on the available API can be read in -the [world api doc.](/docs/world%20api.md) -For setting up your working environment with Archipelago refer -to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md). - -### Requirements - -A world implementation requires a few key things from its implementation - -- A folder within `worlds` that contains an `__init__.py` - - This is what defines it as a Python package and how it's able to be imported - into Archipelago's generation system. During generation time only code that is - defined within this file will be run. It's suggested to split up your information - into more files to improve readability, but all of that information can be - imported at its base level within your world. -- A `World` subclass where you create your world and define all of its rules - and the following requirements: - - Your items and locations need a `item_name_to_id` and `location_name_to_id`, - respectively, mapping. - - An `option_definitions` mapping of your game options with the format - `{name: Class}`, where `name` uses Python snake_case. - - You must define your world's `create_item` method, because this may be called - by the generator in certain circumstances - - When creating your world you submit items and regions to the Multiworld. - - These are lists of said objects which you can access at - `self.multiworld.itempool` and `self.multiworld.regions`. Best practice for - adding to these lists is with either `append` or `extend`, where `append` is a - single object and `extend` is a list. - - Do not use `=` as this will delete other worlds' items and regions. - - Regions are containers for holding your world's Locations. - - Locations are where players will "check" for items and must exist within - a region. It's also important for your world's submitted items to be the same as - its submitted locations count. - - You must always have a "Menu" Region from which the generation algorithm - uses to enter the game and access locations. -- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing. \ No newline at end of file +# Adding Games + +Adding a new game to Archipelago has two major parts: + +* Game Modification to communicate with Archipelago server (hereafter referred to as "client") +* Archipelago Generation and Server integration plugin (hereafter referred to as "world") + +This document will attempt to illustrate the bare minimum requirements and expectations of both parts of a new world +integration. As game modification wildly varies by system and engine, and has no bearing on the Archipelago protocol, +it will not be detailed here. + +## Client + +The client is an intermediary program between the game and the Archipelago server. This can either be a direct +modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it +must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow +to behave as expected are: + +* Handle both secure and unsecure websocket connections +* Detect and react when a location has been "checked" by the player by sending a network packet to the server +* Receive and parse network packets when the player receives an item from the server, and reward it to the player on +demand + * **Any** of your items can be received any number of times, up to and far surpassing those that the game might +normally expect from features such as starting inventory, item link replacement, or item cheating + * Players and the admin can cheat items to the player at any time with a server command, and these items may not have +a player or location attributed to them +* Be able to change the port for saved connection info + * Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this +privilege can be lost, requiring the room to be moved to a new port +* Reconnect if the connection is unstable and lost while playing +* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed +order. +* Receive items that were sent to the player while they were not connected to the server + * The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not +strictly required +* Send a status update packet alerting the server that the player has completed their goal + +Libraries for most modern languages and the spec for various packets can be found in the +[network protocol](/docs/network%20protocol.md) API reference document. + +## World + +The world is your game integration for the Archipelago generator, webhost, and multiworld server. It contains all the +information necessary for creating the items and locations to be randomized, the logic for item placement, the +datapackage information so other game clients can recognize your game data, and documentation. Your world must be +written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago +repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the +following requirements: + +* A folder within `/worlds/` that contains an `__init__.py` +* A `World` subclass where you create your world and define all of its rules +* A unique game name +* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class +definition + * The game_info doc must follow the format `{language_code}_{game_name}.md` +* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are +`item_name_to_id` and `location_name_to_id`, respectively. +* Create an item when `create_item` is called both by your code and externally +* An `options_dataclass` defining the options players have available to them +* A `Region` for your player with the name "Menu" to start from +* Create a non-zero number of locations and add them to your regions +* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool +* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific +items, there are multiple ways to do so, but they should not be added to the multiworld itempool. + +Notable caveats: +* The "Menu" region will always be considered the "start" for the player +* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the +start of the game from anywhere +* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use +`append`, `extend`, or `+=`. **Do not use `=`** +* Regions are simply containers for locations that share similar access rules. They do not have to map to +concrete, physical areas within your game and can be more abstract like tech trees or a questline. + +The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during +generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation +regarding the API can be found in the [world api doc](/docs/world%20api.md). +Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md). diff --git a/docs/img/creepy-castle-directory.png b/docs/img/creepy-castle-directory.png deleted file mode 100644 index af4fdc584dc00d6cc6ee1c5a6425b11cc419039b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 35874 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 diff --git a/docs/img/gato-roboto-directory.png b/docs/img/gato-roboto-directory.png deleted file mode 100644 index fda0eb5ff992b9870224c1511e0f9108a69b18d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81020 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
!Z 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

(=yzUsWG71YEh&@^=lg!pCbeB=ke+@Gi4Fq) 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 z%0A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 diff --git a/docs/options api.md b/docs/options api.md index 6205089f3dc3..f1c01ac7e7e9 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -10,10 +10,9 @@ Archipelago will be abbreviated as "AP" from now on. ## Option Definitions Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you need to create: -- A new option class with a docstring detailing what the option will do to your user. -- A `display_name` to be displayed on the webhost. -- A new entry in the `option_definitions` dict for your World. -By style and convention, the internal names should be snake_case. +- A new option class, with a docstring detailing what the option does, to be exposed to the user. +- A new entry in the `options_dataclass` definition for your World. +By style and convention, the dataclass attributes should be `snake_case`. ### Option Creation - If the option supports having multiple sub_options, such as Choice options, these can be defined with @@ -43,7 +42,7 @@ from Options import Toggle, Range, Choice, PerGameCommonOptions class StartingSword(Toggle): """Adds a sword to your starting inventory.""" - display_name = "Start With Sword" + display_name = "Start With Sword" # this is the option name as it's displayed to the user on the webhost and in the spoiler log class Difficulty(Choice): From bb481256de2a511d3b114f164061d440026be4c4 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Thu, 28 Mar 2024 21:48:40 -0500 Subject: [PATCH 59/74] Core: Make fill failure error more human parseable (#3023) --- Fill.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/Fill.py b/Fill.py index 2d6257eae30a..291ea7e882b7 100644 --- a/Fill.py +++ b/Fill.py @@ -198,10 +198,16 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati # There are leftover unplaceable items and locations that won't accept them if multiworld.can_beat_game(): logging.warning( - f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})') + f"Not all items placed. Game beatable anyway.\nCould not place:\n" + f"{', '.join(str(item) for item in unplaced_items)}") else: - raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' - f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') + raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" + f"Unplaced items:\n" + f"{', '.join(str(item) for item in unplaced_items)}\n" + f"Unfilled locations:\n" + f"{', '.join(str(location) for location in locations)}\n" + f"Already placed {len(placements)}:\n" + f"{', '.join(str(place) for place in placements)}") item_pool.extend(unplaced_items) @@ -273,8 +279,13 @@ def remaining_fill(multiworld: MultiWorld, if unplaced_items and locations: # There are leftover unplaceable items and locations that won't accept them - raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' - f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') + raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n" + f"Unplaced items:\n" + f"{', '.join(str(item) for item in unplaced_items)}\n" + f"Unfilled locations:\n" + f"{', '.join(str(location) for location in locations)}\n" + f"Already placed {len(placements)}:\n" + f"{', '.join(str(place) for place in placements)}") itempool.extend(unplaced_items) @@ -457,7 +468,9 @@ def mark_for_locking(location: Location): fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression") if progitempool: raise FillError( - f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') + f"Not enough locations for progression items. " + f"There are {len(progitempool)} more progression items than there are available locations." + ) accessibility_corrections(multiworld, multiworld.state, defaultlocations) for location in lock_later: @@ -470,7 +483,9 @@ def mark_for_locking(location: Location): remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded") if excludedlocations: raise FillError( - f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") + f"Not enough filler items for excluded locations. " + f"There are {len(excludedlocations)} more excluded locations than filler or trap items." + ) restitempool = filleritempool + usefulitempool @@ -481,13 +496,13 @@ def mark_for_locking(location: Location): if unplaced or unfilled: logging.warning( - f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') + f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}") items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item) locations_counter = Counter(location.player for location in multiworld.get_locations()) items_counter.update(item.player for item in unplaced) locations_counter.update(location.player for location in unfilled) print_data = {"items": items_counter, "locations": locations_counter} - logging.info(f'Per-Player counts: {print_data})') + logging.info(f"Per-Player counts: {print_data})") def flood_items(multiworld: MultiWorld) -> None: From 5f0112e78365d19f04e22af92d6ad1f52d264b1f Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 29 Mar 2024 19:13:51 -0500 Subject: [PATCH 60/74] Tracker: Add starting inventory to trackers and received items table. (#3051) --- WebHostLib/tracker.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 5b246e5aeef0..0b74c6067624 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -124,10 +124,13 @@ def get_player_received_items(self, team: int, player: int) -> List[NetworkItem] @_cache_results def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter: """Retrieves a dictionary of all items received by their id and their received count.""" - items = self.get_player_received_items(team, player) + received_items = self.get_player_received_items(team, player) + starting_items = self.get_player_starting_inventory(team, player) inventory = collections.Counter() - for item in items: + for item in received_items: inventory[item.item] += 1 + for item in starting_items: + inventory[item] += 1 return inventory @@ -358,10 +361,13 @@ def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]: def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str: game = tracker_data.get_player_game(team, player) - # Add received index to all received items, excluding starting inventory. received_items_in_order = {} - for received_index, network_item in enumerate(tracker_data.get_player_received_items(team, player), start=1): - received_items_in_order[network_item.item] = received_index + starting_inventory = tracker_data.get_player_starting_inventory(team, player) + for index, item in enumerate(starting_inventory): + received_items_in_order[item] = index + for index, network_item in enumerate(tracker_data.get_player_received_items(team, player), + start=len(starting_inventory)): + received_items_in_order[network_item.item] = index return render_template( template_name_or_list="genericTracker.html", From b7ac6a4cbd54d5f8e6672e4a6c6ea708e7e6d4de Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Fri, 29 Mar 2024 20:14:53 -0500 Subject: [PATCH 61/74] The Messenger: Fix various portal shuffle issues (#2976) * put constants in a bit more sensical order * fix accidental incorrect scoping * fix plando rules not being respected * add docstrings for the plando functions * fix the portal output pools being overwritten * use shuffle and pop instead of removing by content so plando can go to the same area twice * move portal pool rebuilding outside mapping creation * remove plando_connection cleansing since it isn't shared with transition shuffle --- worlds/messenger/portals.py | 75 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 64438b018400..51f51d7e379e 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -1,3 +1,4 @@ +from copy import deepcopy from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions @@ -18,24 +19,6 @@ ] -REGION_ORDER = [ - "Autumn Hills", - "Forlorn Temple", - "Catacombs", - "Bamboo Creek", - "Howling Grotto", - "Quillshroom Marsh", - "Searing Crags", - "Glacial Peak", - "Tower of Time", - "Cloud Ruins", - "Underworld", - "Riviere Turquoise", - "Elemental Skylands", - "Sunken Shrine", -] - - SHOP_POINTS = { "Autumn Hills": [ "Climbing Claws", @@ -204,30 +187,48 @@ } +REGION_ORDER = [ + "Autumn Hills", + "Forlorn Temple", + "Catacombs", + "Bamboo Creek", + "Howling Grotto", + "Quillshroom Marsh", + "Searing Crags", + "Glacial Peak", + "Tower of Time", + "Cloud Ruins", + "Underworld", + "Riviere Turquoise", + "Elemental Skylands", + "Sunken Shrine", +] + + def shuffle_portals(world: "MessengerWorld") -> None: - def create_mapping(in_portal: str, warp: str) -> None: - nonlocal available_portals + """shuffles the output of the portals from the main hub""" + def create_mapping(in_portal: str, warp: str) -> str: + """assigns the chosen output to the input""" parent = out_to_parent[warp] exit_string = f"{parent.strip(' ')} - " if "Portal" in warp: exit_string += "Portal" world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) - elif warp_point in SHOP_POINTS[parent]: - exit_string += f"{warp_point} Shop" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp_point)}")) + elif warp in SHOP_POINTS[parent]: + exit_string += f"{warp} Shop" + world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: - exit_string += f"{warp_point} Checkpoint" - world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp_point)}")) + exit_string += f"{warp} Checkpoint" + world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) - available_portals.remove(warp) - if shuffle_type < ShufflePortals.option_anywhere: - available_portals = [port for port in available_portals if port not in shop_points[parent]] + return parent def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: + """checks the provided plando connections for portals and connects them""" for connection in plando_connections: if connection.entrance not in PORTALS: continue @@ -236,22 +237,28 @@ def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: world.plando_portals.append(connection.entrance) shuffle_type = world.options.shuffle_portals - shop_points = SHOP_POINTS.copy() + shop_points = deepcopy(SHOP_POINTS) for portal in PORTALS: shop_points[portal].append(f"{portal} Portal") if shuffle_type > ShufflePortals.option_shops: - shop_points.update(CHECKPOINTS) + for area, points in CHECKPOINTS.items(): + shop_points[area] += points out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints} available_portals = [val for zone in shop_points.values() for val in zone] + world.random.shuffle(available_portals) plando = world.multiworld.plando_connections[world.player] if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) - world.multiworld.plando_connections[world.player] = [connection for connection in plando - if connection.entrance not in PORTALS] + for portal in PORTALS: - warp_point = world.random.choice(available_portals) - create_mapping(portal, warp_point) + if portal in world.plando_portals: + continue + warp_point = available_portals.pop() + parent = create_mapping(portal, warp_point) + if shuffle_type < ShufflePortals.option_anywhere: + available_portals = [port for port in available_portals if port not in shop_points[parent]] + world.random.shuffle(available_portals) def connect_portal(world: "MessengerWorld", portal: str, out_region: str) -> None: From 72c53513f8bdab5506ffa972c1bf6f8573f097d7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 31 Mar 2024 03:57:59 +0200 Subject: [PATCH 62/74] WebHost: fix /check creating broken yaml files if files don't end with a newline (#3063) --- WebHostLib/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/check.py b/WebHostLib/check.py index e739dda02d79..da6bfe861a6c 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -28,7 +28,7 @@ def check(): results, _ = roll_options(options) if len(options) > 1: # offer combined file back - combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}" + combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}" for file_name, file_content in options.items()) combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode() else: From 4e3d3963941934c77573e6e0b699edf9e26cd647 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 31 Mar 2024 10:47:11 -0500 Subject: [PATCH 63/74] The Messenger: Fix precollected notes not being removed from the itempool (#3066) * The Messenger: fix precollected notes not being properly removed from pool * The Messenger: bump required client version --- worlds/messenger/__init__.py | 32 ++++++++++++++++++++++---------- worlds/messenger/constants.py | 7 +++++++ worlds/messenger/options.py | 9 +++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index c40ca02f42f1..5e1b12778638 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,4 +1,5 @@ import logging +from datetime import date from typing import Any, ClassVar, Dict, List, Optional, TextIO from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial @@ -9,7 +10,8 @@ from worlds.LauncherComponents import Component, Type, components from .client_setup import launch_game from .connections import CONNECTIONS, RANDOMIZED_CONNECTIONS, TRANSITIONS -from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS +from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, TRAPS, \ + USEFUL_ITEMS from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, ShuffleTransitions from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffle_portals, validate_portals from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS @@ -110,7 +112,7 @@ class MessengerWorld(World): }, } - required_client_version = (0, 4, 3) + required_client_version = (0, 4, 4) web = MessengerWeb() @@ -127,6 +129,7 @@ class MessengerWorld(World): portal_mapping: List[int] transitions: List[Entrance] reachable_locs: int = 0 + filler: Dict[str, int] def generate_early(self) -> None: if self.options.goal == Goal.option_power_seal_hunt: @@ -146,8 +149,9 @@ def generate_early(self) -> None: self.starting_portals = [f"{portal} Portal" for portal in starting_portals[:3] + self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)] + # super complicated method for adding searing crags to starting portals if it wasn't chosen - # need to add a check for transition shuffle when that gets added back in + # TODO add a check for transition shuffle when that gets added back in if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals: self.starting_portals.append("Searing Crags Portal") if len(self.starting_portals) > 4: @@ -155,6 +159,10 @@ def generate_early(self) -> None: if portal in self.starting_portals] self.starting_portals.remove(self.random.choice(portals_to_strip)) + self.filler = FILLER.copy() + if (not hasattr(self.options, "traps") and date.today() < date(2024, 4, 2)) or self.options.traps: + self.filler.update(TRAPS) + self.plando_portals = [] self.portal_mapping = [] self.spoiler_portal_mapping = {} @@ -182,12 +190,13 @@ def create_regions(self) -> None: def create_items(self) -> None: # create items that are always in the item pool main_movement_items = ["Rope Dart", "Wingsuit"] + precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]] itempool: List[MessengerItem] = [ self.create_item(item) for item in self.item_name_to_id - if "Time Shard" not in item and item not in { + if item not in { "Power Seal", *NOTES, *FIGURINES, *main_movement_items, - *{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]}, + *precollected_names, *FILLER, *TRAPS, } ] @@ -199,7 +208,7 @@ def create_items(self) -> None: if self.options.goal == Goal.option_open_music_box: # make a list of all notes except those in the player's defined starting inventory, and adjust the # amount we need to put in the itempool and precollect based on that - notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]] + notes = [note for note in NOTES if note not in precollected_names] self.random.shuffle(notes) precollected_notes_amount = NotesNeeded.range_end - \ self.options.notes_needed - \ @@ -228,8 +237,8 @@ def create_items(self) -> None: remaining_fill = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool) if remaining_fill < 10: self._filler_items = self.random.choices( - list(FILLER)[2:], - weights=list(FILLER.values())[2:], + list(self.filler)[2:], + weights=list(self.filler.values())[2:], k=remaining_fill ) filler = [self.create_filler() for _ in range(remaining_fill)] @@ -300,8 +309,8 @@ def fill_slot_data(self) -> Dict[str, Any]: def get_filler_item_name(self) -> str: if not getattr(self, "_filler_items", None): self._filler_items = [name for name in self.random.choices( - list(FILLER), - weights=list(FILLER.values()), + list(self.filler), + weights=list(self.filler.values()), k=20 )] return self._filler_items.pop(0) @@ -335,6 +344,9 @@ def get_item_classification(self, name: str) -> ItemClassification: if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}: return ItemClassification.useful + + if name in TRAPS: + return ItemClassification.trap return ItemClassification.filler diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index 0c4d6a944cef..ea15c71068db 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -48,6 +48,11 @@ "Time Shard (500)": 5, } +TRAPS = { + "Teleport Trap": 5, + "Prophecy Trap": 10, +} + # item_name_to_id needs to be deterministic and match upstream ALL_ITEMS = [ *NOTES, @@ -71,6 +76,8 @@ *SHOP_ITEMS, *FIGURINES, "Money Wrench", + "Teleport Trap", + "Prophecy Trap", ] # locations diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index c56ee700438f..167bc285ded4 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from datetime import date from typing import Dict from schema import And, Optional, Or, Schema @@ -123,6 +124,11 @@ class RequiredSeals(Range): default = range_end +class Traps(Toggle): + """Whether traps should be included in the itempool.""" + display_name = "Include Traps" + + class ShopPrices(Range): """Percentage modifier for shuffled item prices in shops""" display_name = "Shop Prices Modifier" @@ -199,3 +205,6 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions): percent_seals_required: RequiredSeals shop_price: ShopPrices shop_price_plan: PlannedShopPrices + + if date.today() > date(2024, 4, 1): + traps: Traps From 2ec93ba82a969865a8addc98feb076898978c8e3 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Sun, 31 Mar 2024 09:48:59 -0600 Subject: [PATCH 64/74] Pokemon Emerald: Fix inconsistent location name (#3065) --- worlds/pokemon_emerald/data/locations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_emerald/data/locations.json b/worlds/pokemon_emerald/data/locations.json index d654119ad635..6affdf414688 100644 --- a/worlds/pokemon_emerald/data/locations.json +++ b/worlds/pokemon_emerald/data/locations.json @@ -1784,7 +1784,7 @@ "tags": ["BerryTree"] }, "BERRY_TREE_65": { - "label": "Route 123 - Berry Master Berry Tree 9", + "label": "Route 123 - Berry Tree Berry Master 9", "tags": ["BerryTree"] }, "BERRY_TREE_66": { From e546c0f7ff2456ddb919a1b65a437a1c61b07479 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sun, 31 Mar 2024 10:50:31 -0500 Subject: [PATCH 65/74] Yoshi's Island: add patch suffix (#3061) --- worlds/yoshisisland/Client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/yoshisisland/Client.py b/worlds/yoshisisland/Client.py index c512a8316ab5..1aff36c553c7 100644 --- a/worlds/yoshisisland/Client.py +++ b/worlds/yoshisisland/Client.py @@ -36,6 +36,7 @@ class YoshisIslandSNIClient(SNIClient): game = "Yoshi's Island" + patch_suffix = ".apyi" async def deathlink_kill_player(self, ctx: "SNIContext") -> None: from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read From 03d47e460e434b897b313c2ba452d785ecbacebe Mon Sep 17 00:00:00 2001 From: Ixrec Date: Sun, 31 Mar 2024 16:55:08 +0100 Subject: [PATCH 66/74] A Short Hike: Clarify installation instructions (#3058) * Clarify installation instructions * don't mention 'config' folder since it isn't created until the game starts --- worlds/shorthike/docs/setup_en.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/worlds/shorthike/docs/setup_en.md b/worlds/shorthike/docs/setup_en.md index e327d8bed93c..85d5a8f5eb16 100644 --- a/worlds/shorthike/docs/setup_en.md +++ b/worlds/shorthike/docs/setup_en.md @@ -14,12 +14,13 @@ ## Installation -1. Download the [Modding Tools](https://github.com/BrandenEK/AShortHike.ModdingTools/releases), and follow -the [installation instructions](https://github.com/BrandenEK/AShortHike.ModdingTools#a-short-hike-modding-tools) on the GitHub page. +1. Open the [Modding Tools GitHub page](https://github.com/BrandenEK/AShortHike.ModdingTools/), and follow +the installation instructions. After this step, your `A Short Hike/` folder should have an empty `Modding/` subfolder. 2. After the Modding Tools have been installed, download the -[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) and extract the contents of it -into the `Modding` folder. +[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) zip, extract it, and move the contents +of the `Randomizer/` folder into your `Modding/` folder. After this step, your `Modding/` folder should have + `data/` and `plugins/` subfolders. ## Connecting @@ -29,4 +30,4 @@ Enter in the Server Port, Name, and Password (optional) in the popup menu that a ## Tracking Install PopTracker from the link above and place the PopTracker pack into the packs folder. -Connect to Archipelago via the AP button in the top left. \ No newline at end of file +Connect to Archipelago via the AP button in the top left. From 2a0b7e0def5c00cc2ac273b22581b3cde3b6f6a6 Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Sun, 31 Mar 2024 09:55:55 -0600 Subject: [PATCH 67/74] CV64: A couple of very small docs corrections. (#3057) --- worlds/cv64/docs/en_Castlevania 64.md | 2 +- worlds/cv64/options.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/worlds/cv64/docs/en_Castlevania 64.md b/worlds/cv64/docs/en_Castlevania 64.md index 692bbfe86a71..55f7eb03012f 100644 --- a/worlds/cv64/docs/en_Castlevania 64.md +++ b/worlds/cv64/docs/en_Castlevania 64.md @@ -91,7 +91,7 @@ either filler, useful, or a trap. When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you found and who it was for. The color of the text will tell you its classification: -- Light brown-ish: Common +- Light brown-ish: Filler - White/Yellow: Useful - Yellow/Green: Progression - Yellow/Red: Trap diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index e1be03897dcf..495bb51c5ef8 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -144,8 +144,9 @@ class HardLogic(Toggle): class MultiHitBreakables(Toggle): - """Adds the items that drop from the objects that break in three hits to the pool. There are 17 of these throughout - the game, adding up to 74 checks in total with all stages. + """Adds the items that drop from the objects that break in three hits to the pool. There are 18 of these throughout + the game, adding up to 79 or 80 checks (depending on sub-weapons + being shuffled anywhere or not) in total with all stages. The game will be modified to remember exactly which of their items you've picked up instead of simply whether they were broken or not.""" display_name = "Multi-hit Breakables" From f813a7005fadb1c56bb93fee6147b63d9df2b720 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sun, 31 Mar 2024 11:11:10 -0500 Subject: [PATCH 68/74] The Messenger: update docs formatting and fix outdated info (#3033) * The Messenger: update docs formatting and fix outdated info * address review feedback * 120 chars --- worlds/messenger/docs/en_The Messenger.md | 29 +++++++++++------------ worlds/messenger/docs/setup_en.md | 14 +++++------ worlds/messenger/options.py | 2 +- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index f071ba1c1435..8248a4755d3f 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -49,30 +49,29 @@ for it. The groups you can use for The Messenger are: ## Other changes * The player can return to the Tower of Time HQ at any point by selecting the button from the options menu - * This can cause issues if used at specific times. Current known: - * During Boss fights - * After Courage Note collection (Corrupted Future chase) - * This is currently an expected action in logic. If you do need to teleport during this chase sequence, it - is recommended to quit to title and reload the save + * This can cause issues if used at specific times. If used in any of these known problematic areas, immediately +quit to title and reload the save. The currently known areas include: + * During Boss fights + * After Courage Note collection (Corrupted Future chase) * After reaching ninja village a teleport option is added to the menu to reach it quickly * Toggle Windmill Shuriken button is added to option menu once the item is received -* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when - the player fulfills the necessary conditions. +* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed +when the player fulfills the necessary conditions. * After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be - used to modify certain settings such as text size and color. This can also be used to specify a player name that can't - be entered in game. +used to modify certain settings such as text size and color. This can also be used to specify a player name that can't +be entered in game. ## Known issues * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit - to Searing Crags and re-enter to get it to play correctly. -* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the - player. This may also cause a softlock. +to Searing Crags and re-enter to get it to play correctly. +* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left +and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock * Text entry menus don't accept controller input * In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the - chest will not work. +chest will not work. ## What do I do if I have a problem? -If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game installation -and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord) +If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game +installation and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord) diff --git a/worlds/messenger/docs/setup_en.md b/worlds/messenger/docs/setup_en.md index d986b70f9c98..c1770e747442 100644 --- a/worlds/messenger/docs/setup_en.md +++ b/worlds/messenger/docs/setup_en.md @@ -18,17 +18,17 @@ Read changes to the base game on the [Game Info Page](/games/The%20Messenger/inf 3. Click on "The Messenger" 4. Follow the prompts +These steps can also be followed to launch the game and check for mod updates after the initial setup. + ### Manual Installation 1. Download and install Courier Mod Loader using the instructions on the release page * [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases) 2. Download and install the randomizer mod 1. Download the latest TheMessengerRandomizerAP.zip from - [The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) +[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases) 2. Extract the zip file to `TheMessenger/Mods/` of your game's install location - * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer - is backwards compatible, so the non-AP mod can be safely removed, and you can still play seeds generated from the - non-AP randomizer. + * You cannot have both the non-AP randomizer and the AP randomizer installed at the same time 3. Optionally, Backup your save game * On Windows 1. Press `Windows Key + R` to open run @@ -46,13 +46,13 @@ Read changes to the base game on the [Game Info Page](/games/The%20Messenger/inf 3. Enter connection info using the relevant option buttons * **The game is limited to alphanumerical characters, `.`, and `-`.** * This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the - website. +website. * If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game - directory. When using this, all connection information must be entered in the file. +directory. When using this, all connection information must be entered in the file. 4. Select the `Connect to Archipelago` button 5. Navigate to save file selection 6. Start a new game - * If you're already connected, deleting a save will not disconnect you and is completely safe. + * If you're already connected, deleting an existing save will not disconnect you and is completely safe. ## Continuing a MultiWorld Game diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 167bc285ded4..990975a926f9 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -89,7 +89,7 @@ class ShuffleTransitions(Choice): class Goal(Choice): - """Requirement to finish the game.""" + """Requirement to finish the game. To win with the power seal hunt goal, you must enter the Music Box through the shop chest.""" display_name = "Goal" option_open_music_box = 0 option_power_seal_hunt = 1 From 24a03bc8b6b406c0925eedf415dcef47e17fdbaa Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:02:26 -0500 Subject: [PATCH 69/74] KDL3: fix shuffled animals not actually being random (#3060) --- worlds/kdl3/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/kdl3/__init__.py b/worlds/kdl3/__init__.py index 6d0c196ab1e6..be299f6f2c12 100644 --- a/worlds/kdl3/__init__.py +++ b/worlds/kdl3/__init__.py @@ -206,6 +206,8 @@ def pre_fill(self) -> None: locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns] items = [self.create_item(animal) for animal in animal_pool] allstate = self.multiworld.get_all_state(False) + self.random.shuffle(locations) + self.random.shuffle(items) fill_restrictive(self.multiworld, allstate, locations, items, True, True) else: animal_friends = animal_friend_spawns.copy() From 4aa03da66e1a8c99fc31c163c1a23fb0bd772c15 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 1 Apr 2024 15:06:02 +0200 Subject: [PATCH 70/74] Factorio: fix attempting to create savegame with not filename safe characters (#2842) --- worlds/factorio/Client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/factorio/Client.py b/worlds/factorio/Client.py index 050455bb076a..f612605b4c19 100644 --- a/worlds/factorio/Client.py +++ b/worlds/factorio/Client.py @@ -21,7 +21,7 @@ from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser from MultiServer import mark_raw from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart -from Utils import async_start +from Utils import async_start, get_file_safe_name def check_stdin() -> None: @@ -120,7 +120,7 @@ def on_print_json(self, args: dict): @property def savegame_name(self) -> str: - return f"AP_{self.seed_name}_{self.auth}_Save.zip" + return get_file_safe_name(f"AP_{self.seed_name}_{self.auth}")+"_Save.zip" def print_to_game(self, text): self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] " From 4ac1866689d01dc6693866ee8b1236ad6fea114b Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 1 Apr 2024 08:06:31 -0500 Subject: [PATCH 71/74] ALTTP: Skull Woods Inverted fix (#2980) --- worlds/alttp/InvertedRegions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 2e30fde8cc85..25d4314769f2 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -381,8 +381,8 @@ def create_inverted_regions(world, player): create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']), create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']), create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), - create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']), + create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']), create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']), create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']), create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest', From 35458380e6e08eab85203942b6415fd964907c84 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 1 Apr 2024 07:07:11 -0600 Subject: [PATCH 72/74] Pokemon Emerald: Fix wonder trade race condition (#2983) --- worlds/pokemon_emerald/client.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/client.py b/worlds/pokemon_emerald/client.py index 0dccc1fe17a0..3b9f90270d17 100644 --- a/worlds/pokemon_emerald/client.py +++ b/worlds/pokemon_emerald/client.py @@ -664,8 +664,10 @@ def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None: "cmd": "SetNotify", "keys": [f"pokemon_wonder_trades_{ctx.team}"], }, { - "cmd": "Get", - "keys": [f"pokemon_wonder_trades_{ctx.team}"], + "cmd": "Set", + "key": f"pokemon_wonder_trades_{ctx.team}", + "default": {"_lock": 0}, + "operations": [{"operation": "default", "value": None}] # value is ignored }])) elif cmd == "SetReply": if args.get("key", "") == f"pokemon_wonder_trades_{ctx.team}": From 9aeeeb077a9e894cd2ace51b58d537bcf7607d5b Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Mon, 1 Apr 2024 06:07:56 -0700 Subject: [PATCH 73/74] ALttP: Re-mark light/dark world regions after applying plando connections (#2964) --- worlds/alttp/EntranceShuffle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index fceba86a739e..988455ba3ce8 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -3,6 +3,8 @@ from .OverworldGlitchRules import overworld_glitch_connections from .UnderworldGlitchRules import underworld_glitch_connections +from .Regions import mark_light_world_regions +from .InvertedRegions import mark_dark_world_regions def link_entrances(world, player): @@ -1827,6 +1829,10 @@ def plando_connect(world, player: int): func(world, connection.entrance, connection.exit, player) except Exception as e: raise Exception(f"Could not connect using {connection}") from e + if world.mode[player] != 'inverted': + mark_light_world_regions(world, player) + else: + mark_dark_world_regions(world, player) LW_Dungeon_Entrances = ['Desert Palace Entrance (South)', From 5e5792009cd3089ae61c5fdd208de1b79d183cb4 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:08:21 -0500 Subject: [PATCH 74/74] LttP: delete playerSettings.yaml (#3062) --- playerSettings.yaml | 591 -------------------------------------------- 1 file changed, 591 deletions(-) delete mode 100644 playerSettings.yaml diff --git a/playerSettings.yaml b/playerSettings.yaml deleted file mode 100644 index b6b474a9fffa..000000000000 --- a/playerSettings.yaml +++ /dev/null @@ -1,591 +0,0 @@ -# What is this file? -# This file contains options which allow you to configure your multiworld experience while allowing others -# to play how they want as well. - -# How do I use it? -# The options in this file are weighted. This means the higher number you assign to a value, the more -# chances you have for that option to be chosen. For example, an option like this: -# -# map_shuffle: -# on: 5 -# off: 15 -# -# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off - -# I've never seen a file like this before. What characters am I allowed to use? -# This is a .yaml file. You are allowed to use most characters. -# To test if your yaml is valid or not, you can use this website: -# http://www.yamllint.com/ - -description: Template Name # Used to describe your yaml. Useful if you have multiple files -name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit -#{player} will be replaced with the player's slot number. -#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1. -#{number} will be replaced with the counter value of the name. -#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1. -game: # Pick a game to play - A Link to the Past: 1 -requires: - version: 0.4.4 # Version of Archipelago required for this yaml to work as expected. -A Link to the Past: - progression_balancing: - # A system that can move progression earlier, to try and prevent the player from getting stuck and bored early. - # A lower setting means more getting stuck. A higher setting means less getting stuck. - # - # You can define additional values between the minimum and maximum values. - # Minimum value is 0 - # Maximum value is 99 - random: 0 - random-low: 0 - random-high: 0 - disabled: 0 # equivalent to 0 - normal: 50 # equivalent to 50 - extreme: 0 # equivalent to 99 - - accessibility: - # Set rules for reachability of your items/locations. - # Locations: ensure everything can be reached and acquired. - # Items: ensure all logically relevant items can be acquired. - # Minimal: ensure what is needed to reach your goal can be acquired. - locations: 0 - items: 50 - minimal: 0 - - local_items: - # Forces these items to be in their native world. - [ ] - - non_local_items: - # Forces these items to be outside their native world. - [ ] - - start_inventory: - # Start with these items. - { } - - start_hints: - # Start with these item's locations prefilled into the !hint command. - [ ] - - start_location_hints: - # Start with these locations and their item prefilled into the !hint command - [ ] - - exclude_locations: - # Prevent these locations from having an important item - [ ] - - priority_locations: - # Prevent these locations from having an unimportant item - [ ] - - item_links: - # Share part of your item pool with other players. - [ ] - - ### Logic Section ### - glitches_required: # Determine the logic required to complete the seed - none: 50 # No glitches required - minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic - overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches - hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons. - no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx. - # Other players items are placed into your world under HMG logic - dark_room_logic: # Logic for unlit dark rooms - lamp: 50 # require the Lamp for these rooms to be considered accessible. - torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access - none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness - restrict_dungeon_item_on_boss: # aka ambrosia boss items - on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items - off: 50 - ### End of Logic Section ### - bigkey_shuffle: # Big Key Placement - original_dungeon: 50 - own_dungeons: 0 - own_world: 0 - any_world: 0 - different_world: 0 - start_with: 0 - smallkey_shuffle: # Small Key Placement - original_dungeon: 50 - own_dungeons: 0 - own_world: 0 - any_world: 0 - different_world: 0 - universal: 0 - start_with: 0 - key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies - off: 50 - on: 0 - compass_shuffle: # Compass Placement - original_dungeon: 50 - own_dungeons: 0 - own_world: 0 - any_world: 0 - different_world: 0 - start_with: 0 - map_shuffle: # Map Placement - original_dungeon: 50 - own_dungeons: 0 - own_world: 0 - any_world: 0 - different_world: 0 - start_with: 0 - dungeon_counters: - on: 0 # Always display amount of items checked in a dungeon - pickup: 50 # Show when compass is picked up - default: 0 # Show when compass is picked up if the compass itself is shuffled - off: 0 # Never show item count in dungeons - progressive: # Enable or disable progressive items (swords, shields, bow) - on: 50 # All items are progressive - off: 0 # No items are progressive - grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be - entrance_shuffle: - none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option - dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon - dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world - dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal - simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules - restricted: 0 # Less strict than simple - full: 0 # Less strict than restricted - crossed: 0 # Less strict than full - insanity: 0 # Very few grouping rules. Good luck - # you can also define entrance shuffle seed, like so: - crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information - # however, many other settings like logic, world state, retro etc. may affect the shuffle result as well. - crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed - goals: - ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon - crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT - bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2) - pedestal: 0 # Pull the Triforce from the Master Sword pedestal - ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon - triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle - local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle - ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon - local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon - ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock. - open_pyramid: - goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons - auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool - open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt - closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower - triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces. - extra: 0 # available = triforce_pieces_extra + triforce_pieces_required - percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required - available: 50 # available = triforce_pieces_available - triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world. - # Format "pieces: chance" - 0: 0 - 5: 50 - 10: 50 - 15: 0 - 20: 0 - triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world. - # Format "pieces: chance" - 100: 0 #No extra - 150: 50 #Half the required will be added as extra - 200: 0 #There are the double of the required ones available. - triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1 - # Format "pieces: chance" - 25: 0 - 30: 50 - 40: 0 - 50: 0 - triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1 - # Format "pieces: chance" - 15: 0 - 20: 50 - 30: 0 - 40: 0 - 50: 0 - crystals_needed_for_gt: # Crystals required to open GT - 0: 0 - 7: 50 - random: 0 - random-low: 0 # any valid number, weighted towards the lower end - random-middle: 0 # any valid number, weighted towards the central range - random-high: 0 # any valid number, weighted towards the higher end - crystals_needed_for_ganon: # Crystals required to hurt Ganon - 0: 0 - 7: 50 - random: 0 - random-low: 0 - random-middle: 0 - random-high: 0 - mode: - standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary - open: 50 # Begin the game from your choice of Link's House or the Sanctuary - inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered - retro_bow: - on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees. - off: 50 - retro_caves: - on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion. - off: 50 - hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints. - 'on': 50 - 'off': 0 - full: 0 - scams: # If on, these Merchants will no longer tell you what they're selling. - 'off': 50 - 'king_zora': 0 - 'bottle_merchant': 0 - 'all': 0 - swordless: - on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change - off: 1 - item_pool: - easy: 0 # Doubled upgrades, progressives, and etc - normal: 50 # Item availability remains unchanged from vanilla game - hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless) - expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless) - item_functionality: - easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere. - normal: 50 # Vanilla item functionality - hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon) - expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon) - tile_shuffle: # Randomize the tile layouts in flying tile rooms - on: 0 - off: 50 - misery_mire_medallion: # required medallion to open Misery Mire front entrance - random: 50 - Ether: 0 - Bombos: 0 - Quake: 0 - turtle_rock_medallion: # required medallion to open Turtle Rock front entrance - random: 50 - Ether: 0 - Bombos: 0 - Quake: 0 - ### Enemizer Section ### - boss_shuffle: - none: 50 # Vanilla bosses - basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons - full: 0 # 3 bosses can occur twice - chaos: 0 # Any boss can appear any amount of times - singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those - enemy_shuffle: # Randomize enemy placement - on: 0 - off: 50 - killable_thieves: # Make thieves killable - on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable - off: 50 - bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush - on: 0 - off: 50 - enemy_damage: - default: 50 # Vanilla enemy damage - shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps - chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage - enemy_health: - default: 50 # Vanilla enemy HP - easy: 0 # Enemies have reduced health - hard: 0 # Enemies have increased health - expert: 0 # Enemies have greatly increased health - pot_shuffle: - 'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile - 'off': 50 # Default pot item locations - ### End of Enemizer Section ### - ### Beemizer ### - # can add weights for any whole number between 0 and 100 - beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps - 0: 50 # No junk fill items are replaced (Beemizer is off) - 25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees - 100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees - beemizer_trap_chance: - 60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee - 70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee - 80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee - 90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee - 100: 0 # All beemizer replacements are traps - ### Shop Settings ### - shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl) - 0: 50 - 5: 0 - 15: 0 - 30: 0 - random: 0 # 0 to 30 evenly distributed - shop_price_modifier: # Percentage modifier for shuffled item prices in shops - # you can add additional values between minimum and maximum - 0: 0 # minimum value - 400: 0 # maximum value - random: 0 - random-low: 0 - random-high: 0 - 100: 50 - shop_shuffle: - none: 50 - g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops - f: 0 # Generate new default inventories for every shop independently - i: 0 # Shuffle default inventories of the shops around - p: 0 # Randomize the prices of the items in shop inventories - u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld) - w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too - P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees - ip: 0 # Shuffle inventories and randomize prices - fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool - uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool - # You can add more combos - ### End of Shop Section ### - shuffle_prizes: # aka drops - none: 0 # do not shuffle prize packs - g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc. - b: 0 # shuffle "bonk" prize packs - bg: 0 # shuffle both - timer: - none: 50 # No timer will be displayed. - timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end. - timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit. - ohko: 0 # Timer always at zero. Permanent OHKO. - timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though. - display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool. - countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with - 0: 0 # For timed_ohko, starts in OHKO mode when starting the game - 10: 50 - 20: 0 - 30: 0 - 60: 0 - red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock - -2: 50 - 1: 0 - blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock - 1: 0 - 2: 50 - green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock - 4: 50 - 10: 0 - 15: 0 - glitch_boots: - on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them - off: 0 - # rom options section - random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below. - enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool) - on: 0 - off: 1 - on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts. - on: 1 - off: 0 - on_enter: # Random sprite on underworld entry. Note that entering hobo counts. - on: 0 - off: 1 - on_exit: # Random sprite on underworld exit. Exiting hobo does not count. - on: 0 - off: 1 - on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless. - on: 0 - off: 1 - on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts. - on: 0 - off: 1 - on_bonk: # Random sprite on bonk. - on: 0 - off: 1 - on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time. - on: 0 - off: 1 - use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool - on: 0 - off: 1 - #sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this. - # - link - # - pride link - # - penguin link - # - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool. - sprite: # Enter the name of your preferred sprite and weight it appropriately - random: 0 - randomonhit: 0 # Random sprite on hit - randomonenter: 0 # Random sprite on entering the underworld. - randomonexit: 0 # Random sprite on exiting the underworld. - randomonslash: 0 # Random sprite on sword slashes - randomonitem: 0 # Random sprite on getting items. - randomonbonk: 0 # Random sprite on bonk. - # You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit. - randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events. - Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it - music: # If "off", all in-game music will be disabled - on: 50 - off: 0 - quickswap: # Enable switching items by pressing the L+R shoulder buttons - on: 50 - off: 0 - triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala - normal: 0 # original behavior (always visible) - hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala - hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala - hide_both: 0 # Hide both under above circumstances - reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more. - on: 50 - off: 0 - menuspeed: # Controls how fast the item menu opens and closes - normal: 50 - instant: 0 - double: 0 - triple: 0 - quadruple: 0 - half: 0 - heartcolor: # Controls the color of your health hearts - red: 50 - blue: 0 - green: 0 - yellow: 0 - random: 0 - heartbeep: # Controls the frequency of the low-health beeping - double: 0 - normal: 50 - half: 0 - quarter: 0 - off: 0 - ow_palettes: # Change the colors of the overworld - default: 50 # No changes - good: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - uw_palettes: # Change the colors of caves and dungeons - default: 50 # No changes - good: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - hud_palettes: # Change the colors of the hud - default: 50 # No changes - good: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - sword_palettes: # Change the colors of swords - default: 50 # No changes - good: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - shield_palettes: # Change the colors of shields - default: 50 # No changes - good: 0 # Shuffle the colors, with harmony in mind - blackout: 0 # everything black / blind mode - grayscale: 0 - negative: 0 - classic: 0 - dizzy: 0 - sick: 0 - puke: 0 - - # triggers that replace options upon rolling certain options - legacy_weapons: # this is not an actual option, just a set of weights to trigger from - trigger_disabled: 50 - randomized: 0 # Swords are placed randomly throughout the world - assured: 0 # Begin with a sword, the rest are placed randomly throughout the world - vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) - swordless: 0 # swordless mode - - death_link: - false: 50 - true: 0 - - allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players. - # Off by default, because it currently crashes on real hardware. - false: 50 - true: 0 - -linked_options: - - name: crosskeys - options: # These overwrite earlier options if the percentage chance triggers - A Link to the Past: - entrance_shuffle: crossed - bigkey_shuffle: true - compass_shuffle: true - map_shuffle: true - smallkey_shuffle: true - percentage: 0 # Set this to the percentage chance you want crosskeys - - name: localcrosskeys - options: # These overwrite earlier options if the percentage chance triggers - A Link to the Past: - entrance_shuffle: crossed - bigkey_shuffle: true - compass_shuffle: true - map_shuffle: true - smallkey_shuffle: true - local_items: # Forces keys to be local to your own world - - "Small Keys" - - "Big Keys" - percentage: 0 # Set this to the percentage chance you want local crosskeys - - name: enemizer - options: - A Link to the Past: - boss_shuffle: # Subchances can be injected too, which then get rolled - basic: 1 - full: 1 - chaos: 1 - singularity: 1 - enemy_damage: - shuffled: 1 - chaos: 1 - enemy_health: - easy: 1 - hard: 1 - expert: 1 - percentage: 0 # Set this to the percentage chance you want enemizer -triggers: - # trigger block for legacy weapons mode, to enable these add weights to legacy_weapons - - option_name: legacy_weapons - option_result: randomized - option_category: A Link to the Past - options: - A Link to the Past: - swordless: off - - option_name: legacy_weapons - option_result: assured - option_category: A Link to the Past - options: - A Link to the Past: - swordless: off - start_inventory: - Progressive Sword: 1 - - option_name: legacy_weapons - option_result: vanilla - option_category: A Link to the Past - options: - A Link to the Past: - swordless: off - plando_items: - - items: - Progressive Sword: 4 - locations: - - Master Sword Pedestal - - Pyramid Fairy - Left - - Blacksmith - - Link's Uncle - - option_name: legacy_weapons - option_result: swordless - option_category: A Link to the Past - options: - A Link to the Past: - swordless: on - # end of legacy weapons block - - option_name: enemy_damage # targets enemy_damage - option_category: A Link to the Past - option_result: shuffled # if it rolls shuffled - percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works) - options: # then inserts these options - A Link to the Past: - swordless: off