From 437843bf534038d20962159f803acc89e5d92dbf Mon Sep 17 00:00:00 2001 From: LiquidCat64 <74896918+LiquidCat64@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:37:51 -0600 Subject: [PATCH] CV64: Use AP Procedure Patch (#3159) --- worlds/cv64/__init__.py | 27 +- worlds/cv64/aesthetics.py | 273 +++-- worlds/cv64/data/patches.py | 21 +- worlds/cv64/docs/obscure_checks.md | 2 +- worlds/cv64/options.py | 6 +- worlds/cv64/rom.py | 1743 +++++++++++++++------------- worlds/cv64/stages.py | 76 +- 7 files changed, 1102 insertions(+), 1046 deletions(-) diff --git a/worlds/cv64/__init__.py b/worlds/cv64/__init__.py index 1f528fea..2bceefee 100644 --- a/worlds/cv64/__init__.py +++ b/worlds/cv64/__init__.py @@ -4,7 +4,7 @@ import settings import base64 import logging -from BaseClasses import Item, Region, MultiWorld, Tutorial, ItemClassification +from BaseClasses import Item, Region, 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 @@ -18,7 +18,7 @@ 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 .rom import RomData, write_patch, get_base_rom_path, CV64ProcedurePatch, CV64_US_10_HASH from .client import Castlevania64Client @@ -27,7 +27,7 @@ class CV64Settings(settings.Group): """File name of the CV64 US 1.0 rom""" copy_to = "Castlevania (USA).z64" description = "CV64 (US 1.0) ROM File" - md5s = [CV64DeltaPatch.hash] + md5s = [CV64_US_10_HASH] rom_file: RomFile = RomFile(RomFile.copy_to) @@ -86,12 +86,6 @@ class CV64World(World): 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)) @@ -276,18 +270,13 @@ class CV64World(World): offset_data.update(get_start_inventory_data(self.player, self.options, self.multiworld.precollected_items[self.player])) - cv64_rom = LocalRom(get_base_rom_path()) + patch = CV64ProcedurePatch() + write_patch(self, patch, offset_data, shop_name_list, shop_desc_list, shop_colors_list, active_locations) - rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.z64") + rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}" + f"{patch.patch_file_ending}") - 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) + patch.write(rom_path) def get_filler_item_name(self) -> str: return self.random.choice(filler_item_names) diff --git a/worlds/cv64/aesthetics.py b/worlds/cv64/aesthetics.py index cbf2728c..66709174 100644 --- a/worlds/cv64/aesthetics.py +++ b/worlds/cv64/aesthetics.py @@ -14,111 +14,111 @@ 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), + 0x10C6EB: (b"\x10", rname.forest_of_silence), # Forest + 0x10C6F3: (b"\x0F", rname.forest_of_silence), + 0x10C6FB: (b"\x0E", rname.forest_of_silence), + 0x10C703: (b"\x0D", 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), + 0x10C81F: (b"\x0F", rname.castle_wall), # Castle Wall + 0x10C827: (b"\x10", rname.castle_wall), + 0x10C82F: (b"\x0E", rname.castle_wall), + 0x7F9A0F: (b"\x0D", 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), + 0x83A5D9: (b"\x0E", rname.villa), # Villa + 0x83A5E5: (b"\x0D", rname.villa), + 0x83A5F1: (b"\x0F", rname.villa), + 0xBFC903: (b"\x10", rname.villa), + 0x10C987: (b"\x10", rname.villa), + 0x10C98F: (b"\x0D", rname.villa), + 0x10C997: (b"\x0F", rname.villa), + 0x10CF73: (b"\x10", 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), + 0x10CA57: (b"\x0D", rname.tunnel), # Tunnel + 0x10CA5F: (b"\x0E", rname.tunnel), + 0x10CA67: (b"\x10", rname.tunnel), + 0x10CA6F: (b"\x0D", rname.tunnel), + 0x10CA77: (b"\x0F", rname.tunnel), + 0x10CA7F: (b"\x0E", rname.tunnel), - 0x10CBC7: (0x0E, rname.castle_center), # Castle Center - 0x10CC0F: (0x0D, rname.castle_center), - 0x10CC5B: (0x0F, rname.castle_center), + 0x10CBC7: (b"\x0E", rname.castle_center), # Castle Center + 0x10CC0F: (b"\x0D", rname.castle_center), + 0x10CC5B: (b"\x0F", 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), + 0x10CD3F: (b"\x0E", rname.tower_of_execution), # Character towers + 0x10CD65: (b"\x0D", rname.tower_of_execution), + 0x10CE2B: (b"\x0E", rname.tower_of_science), + 0x10CE83: (b"\x10", rname.duel_tower), - 0x10CF8B: (0x0F, rname.room_of_clocks), # Room of Clocks - 0x10CF93: (0x0D, rname.room_of_clocks), + 0x10CF8B: (b"\x0F", rname.room_of_clocks), # Room of Clocks + 0x10CF93: (b"\x0D", 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), + 0x99BC5A: (b"\x0D", rname.clock_tower), # Clock Tower + 0x10CECB: (b"\x10", rname.clock_tower), + 0x10CED3: (b"\x0F", rname.clock_tower), + 0x10CEDB: (b"\x0E", rname.clock_tower), + 0x10CEE3: (b"\x0D", rname.clock_tower), } rom_sub_weapon_flags = { - 0x10C6EC: 0x0200FF04, # Forest of Silence - 0x10C6FC: 0x0400FF04, - 0x10C6F4: 0x0800FF04, - 0x10C704: 0x4000FF04, + 0x10C6EC: b"\x02\x00\xFF\x04", # Forest of Silence + 0x10C6FC: b"\x04\x00\xFF\x04", + 0x10C6F4: b"\x08\x00\xFF\x04", + 0x10C704: b"\x40\x00\xFF\x04", - 0x10C831: 0x08, # Castle Wall - 0x10C829: 0x10, - 0x10C821: 0x20, - 0xBFCA97: 0x04, + 0x10C831: b"\x08", # Castle Wall + 0x10C829: b"\x10", + 0x10C821: b"\x20", + 0xBFCA97: b"\x04", # Villa - 0xBFC926: 0xFF04, - 0xBFC93A: 0x80, - 0xBFC93F: 0x01, - 0xBFC943: 0x40, - 0xBFC947: 0x80, - 0x10C989: 0x10, - 0x10C991: 0x20, - 0x10C999: 0x40, - 0x10CF77: 0x80, + 0xBFC926: b"\xFF\x04", + 0xBFC93A: b"\x80", + 0xBFC93F: b"\x01", + 0xBFC943: b"\x40", + 0xBFC947: b"\x80", + 0x10C989: b"\x10", + 0x10C991: b"\x20", + 0x10C999: b"\x40", + 0x10CF77: b"\x80", - 0x10CA58: 0x4000FF0E, # Tunnel - 0x10CA6B: 0x80, - 0x10CA60: 0x1000FF05, - 0x10CA70: 0x2000FF05, - 0x10CA78: 0x4000FF05, - 0x10CA80: 0x8000FF05, + 0x10CA58: b"\x40\x00\xFF\x0E", # Tunnel + 0x10CA6B: b"\x80", + 0x10CA60: b"\x10\x00\xFF\x05", + 0x10CA70: b"\x20\x00\xFF\x05", + 0x10CA78: b"\x40\x00\xFF\x05", + 0x10CA80: b"\x80\x00\xFF\x05", - 0x10CBCA: 0x02, # Castle Center - 0x10CC10: 0x80, - 0x10CC5C: 0x40, + 0x10CBCA: b"\x02", # Castle Center + 0x10CC10: b"\x80", + 0x10CC5C: b"\x40", - 0x10CE86: 0x01, # Duel Tower - 0x10CD43: 0x02, # Tower of Execution - 0x10CE2E: 0x20, # Tower of Science + 0x10CE86: b"\x01", # Duel Tower + 0x10CD43: b"\x02", # Tower of Execution + 0x10CE2E: b"\x20", # Tower of Science - 0x10CF8E: 0x04, # Room of Clocks - 0x10CF96: 0x08, + 0x10CF8E: b"\x04", # Room of Clocks + 0x10CF96: b"\x08", - 0x10CECE: 0x08, # Clock Tower - 0x10CED6: 0x10, - 0x10CEE6: 0x20, - 0x10CEDE: 0x80, + 0x10CECE: b"\x08", # Clock Tower + 0x10CED6: b"\x10", + 0x10CEE6: b"\x20", + 0x10CEDE: b"\x80", } rom_empty_breakables_flags = { - 0x10C74D: 0x40FF05, # Forest of Silence - 0x10C765: 0x20FF0E, - 0x10C774: 0x0800FF0E, - 0x10C755: 0x80FF05, - 0x10C784: 0x0100FF0E, - 0x10C73C: 0x0200FF0E, + 0x10C74D: b"\x40\xFF\x05", # Forest of Silence + 0x10C765: b"\x20\xFF\x0E", + 0x10C774: b"\x08\x00\xFF\x0E", + 0x10C755: b"\x80\xFF\x05", + 0x10C784: b"\x01\x00\xFF\x0E", + 0x10C73C: b"\x02\x00\xFF\x0E", - 0x10C8D0: 0x0400FF0E, # Villa foyer + 0x10C8D0: b"\x04\x00\xFF\x0E", # Villa foyer - 0x10CF9F: 0x08, # Room of Clocks flags - 0x10CFA7: 0x01, - 0xBFCB6F: 0x04, # Room of Clocks candle property IDs - 0xBFCB73: 0x05, + 0x10CF9F: b"\x08", # Room of Clocks flags + 0x10CFA7: b"\x01", + 0xBFCB6F: b"\x04", # Room of Clocks candle property IDs + 0xBFCB73: b"\x05", } rom_axe_cross_lower_values = { @@ -269,19 +269,18 @@ renon_item_dialogue = { } -def randomize_lighting(world: "CV64World") -> Dict[int, int]: +def randomize_lighting(world: "CV64World") -> Dict[int, bytes]: """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) + randomized_lighting[0x1091A0 + (entry * 28) + sub_entry] = bytes([world.random.randint(0, 255)]) return randomized_lighting -def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]: +def shuffle_sub_weapons(world: "CV64World") -> Dict[int, bytes]: """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} @@ -295,7 +294,7 @@ def shuffle_sub_weapons(world: "CV64World") -> Dict[int, int]: return dict(zip(sub_weapon_dict, sub_bytes)) -def randomize_music(world: "CV64World") -> Dict[int, int]: +def randomize_music(world: "CV64World") -> Dict[int, bytes]: """Generates randomized or disabled data for all the music in the game.""" music_array = bytearray(0x7A) for number in music_sfx_ids: @@ -340,15 +339,10 @@ def randomize_music(world: "CV64World") -> Dict[int, int]: 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 + return {0xBFCD30: bytes(music_array)} -def randomize_shop_prices(world: "CV64World") -> Dict[int, int]: +def randomize_shop_prices(world: "CV64World") -> Dict[int, bytes]: """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 @@ -363,21 +357,15 @@ def randomize_shop_prices(world: "CV64World") -> Dict[int, int]: 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. + # Convert the price list into a data dict. 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] + price_dict[0x103D6C + (i * 12)] = int.to_bytes(shop_price_list[i], 4, "big") return price_dict -def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, int]: +def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Location]) -> Dict[int, bytes]: """Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should increase a number. @@ -400,16 +388,11 @@ def get_countdown_numbers(options: CV64Options, active_locations: Iterable[Locat 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 + return {0xBFD818: bytes(countdown_list)} def get_location_data(world: "CV64World", active_locations: Iterable[Location]) \ - -> Tuple[Dict[int, int], List[str], List[bytearray], List[List[Union[int, str, None]]]]: + -> Tuple[Dict[int, bytes], 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 @@ -449,12 +432,11 @@ def get_location_data(world: "CV64World", active_locations: Iterable[Location]) # 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.item.player == world.player: 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") + location_bytes[get_location_info(loc.name, "offset")] = get_item_info(loc.item.name, "code") & 0xFF else: # Make the item the unused Wooden Stake - our multiworld item. location_bytes[get_location_info(loc.name, "offset")] = 0x11 @@ -534,11 +516,12 @@ def get_location_data(world: "CV64World", active_locations: Iterable[Location]) shop_colors_list.append(get_item_text_color(loc)) - return location_bytes, shop_name_list, shop_colors_list, shop_desc_list + return {offset: int.to_bytes(byte, 1, "big") for offset, byte in location_bytes.items()}, 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]: + active_stage_exits: Dict[str, Dict[str, Union[str, int, None]]]) -> Dict[int, bytes]: """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.""" @@ -551,8 +534,8 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str, # 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 + loading_zone_bytes[get_stage_info(stage, "startzone map offset")] = b"\xFF" + loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x00" 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") @@ -563,7 +546,7 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str, 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 + loading_zone_bytes[get_stage_info(stage, "startzone spawn offset")] = b"\x03" # End loading zones if active_stage_exits[stage]["next"]: @@ -582,16 +565,16 @@ def get_loading_zone_bytes(options: CV64Options, starting_stage: str, return loading_zone_bytes -def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, int]: +def get_start_inventory_data(player: int, options: CV64Options, precollected_items: List[Item]) -> Dict[int, bytes]: """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 + start_inventory_data = {} inventory_items_array = [0 for _ in range(35)] total_money = 0 + total_jewels = 0 + total_powerups = 0 + total_ice_traps = 0 items_max = 10 @@ -615,42 +598,46 @@ def get_start_inventory_data(player: int, options: CV64Options, precollected_ite inventory_items_array[inventory_offset] = 2 # Starting sub-weapon elif sub_equip_id is not None: - start_inventory_data[0xBFD883] = sub_equip_id + start_inventory_data[0xBFD883] = bytes(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 + total_powerups += 1 + # Can't have more than 2 PowerUps. + if total_powerups > 2: + total_powerups = 2 # Starting Gold elif "GOLD" in item.name: total_money += int(item.name[0:4]) + # Money cannot be higher than 99999. if total_money > 99999: total_money = 99999 # Starting Jewels elif "jewel" in item.name: if "L" in item.name: - start_inventory_data[0xBFD867] += 10 + total_jewels += 10 else: - start_inventory_data[0xBFD867] += 5 - if start_inventory_data[0xBFD867] > 99: - start_inventory_data[0xBFD867] = 99 + total_jewels += 5 + # Jewels cannot be higher than 99. + if total_jewels > 99: + total_jewels = 99 # Starting Ice Traps else: - start_inventory_data[0xBFD88B] += 1 - if start_inventory_data[0xBFD88B] > 0xFF: - start_inventory_data[0xBFD88B] = 0xFF + total_ice_traps += 1 + # Ice Traps cannot be higher than 255. + if total_ice_traps > 0xFF: + total_ice_traps = 0xFF + + # Convert the jewels into data. + start_inventory_data[0xBFD867] = bytes([total_jewels]) + + # Convert the Ice Traps into data. + start_inventory_data[0xBFD88B] = bytes([total_ice_traps]) # Convert the inventory items into data. - for i in range(len(inventory_items_array)): - start_inventory_data[0xBFE518 + i] = inventory_items_array[i] + start_inventory_data[0xBFE518] = bytes(inventory_items_array) - # 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 + # Convert the starting money into data. + start_inventory_data[0xBFE514] = int.to_bytes(total_money, 4, "big") return start_inventory_data diff --git a/worlds/cv64/data/patches.py b/worlds/cv64/data/patches.py index 4c467036..938b615b 100644 --- a/worlds/cv64/data/patches.py +++ b/worlds/cv64/data/patches.py @@ -197,12 +197,15 @@ deathlink_nitro_edition = [ 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. +launch_fall_killer = [ + # Custom code to force the instant fall death if at a high enough falling speed after getting killed by something + # that launches you (whether it be the Nitro explosion or a Big Toss hit). The game doesn't normally run the check + # that would trigger the fall death after you get killed by some other means, which could result in a softlock + # when a killing blow launches you into an abyss. 0x3C0C8035, # LUI T4, 0x8035 0x918807E2, # LBU T0, 0x07E2 (T4) + 0x24090008, # ADDIU T1, R0, 0x0008 + 0x11090002, # BEQ T0, T1, [forward 0x02] 0x2409000C, # ADDIU T1, R0, 0x000C 0x15090006, # BNE T0, T1, [forward 0x06] 0x3C098035, # LUI T1, 0x8035 @@ -2863,3 +2866,13 @@ big_tosser = [ 0xAD000814, # SW R0, 0x0814 (T0) 0x03200008 # JR T9 ] + +dog_bite_ice_trap_fix = [ + # Sets the freeze timer to 0 when a maze garden dog bites the player to ensure the ice chunk model will break if the + # player gets bitten while frozen via Ice Trap. + 0x3C088039, # LUI T0, 0x8039 + 0xA5009E76, # SH R0, 0x9E76 (T0) + 0x3C090F00, # LUI T1, 0x0F00 + 0x25291CB8, # ADDIU T1, T1, 0x1CB8 + 0x01200008 # JR T1 +] diff --git a/worlds/cv64/docs/obscure_checks.md b/worlds/cv64/docs/obscure_checks.md index 4aafc2db..6f0e0cdb 100644 --- a/worlds/cv64/docs/obscure_checks.md +++ b/worlds/cv64/docs/obscure_checks.md @@ -27,7 +27,7 @@ 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. +to speedrunning the vanilla game and haven't yet learned the RNG manip strats, this is a guaranteed spot to find a PowerUp at. diff --git a/worlds/cv64/options.py b/worlds/cv64/options.py index 495bb51c..da2b9f94 100644 --- a/worlds/cv64/options.py +++ b/worlds/cv64/options.py @@ -74,7 +74,8 @@ class HardItemPool(Toggle): class Special1sPerWarp(Range): - """Sets how many Special1 jewels are needed per warp menu option unlock.""" + """Sets how many Special1 jewels are needed per warp menu option unlock. + This will decrease until the number x 7 is less than or equal to the Total Specail1s if it isn't already.""" range_start = 1 range_end = 10 default = 1 @@ -82,8 +83,7 @@ class Special1sPerWarp(Range): 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.""" + """Sets how many Speical1 jewels are in the pool in total.""" range_start = 7 range_end = 70 default = 7 diff --git a/worlds/cv64/rom.py b/worlds/cv64/rom.py index ab8c7030..ab4371b0 100644 --- a/worlds/cv64/rom.py +++ b/worlds/cv64/rom.py @@ -1,9 +1,9 @@ - +import json import Utils from BaseClasses import Location -from worlds.Files import APDeltaPatch -from typing import List, Dict, Union, Iterable, Collection, TYPE_CHECKING +from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes, APPatchExtension +from typing import List, Dict, Union, Iterable, Collection, Optional, TYPE_CHECKING import hashlib import os @@ -22,37 +22,34 @@ from settings import get_settings if TYPE_CHECKING: from . import CV64World -CV64US10HASH = "1cc5cf3b4d29d8c3ade957648b529dc1" -ROM_PLAYER_LIMIT = 65535 +CV64_US_10_HASH = "1cc5cf3b4d29d8c3ade957648b529dc1" warp_map_offsets = [0xADF67, 0xADF77, 0xADF87, 0xADF97, 0xADFA7, 0xADFBB, 0xADFCB, 0xADFDF] -class LocalRom: +class RomData: 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 __init__(self, file: bytes, name: Optional[str] = None) -> None: + self.file = bytearray(file) + self.name = name def read_bit(self, address: int, bit_number: int) -> bool: bitflag = (1 << bit_number) return (self.buffer[address] & bitflag) != 0 def read_byte(self, address: int) -> int: - return self.buffer[address] + return self.file[address] def read_bytes(self, start_address: int, length: int) -> bytearray: - return self.buffer[start_address:start_address + length] + return self.file[start_address:start_address + length] def write_byte(self, address: int, value: int) -> None: - self.buffer[address] = value + self.file[address] = value def write_bytes(self, start_address: int, values: Collection[int]) -> None: - self.buffer[start_address:start_address + len(values)] = values + self.file[start_address:start_address + len(values)] = values def write_int16(self, address: int, value: int) -> None: value = value & 0xFFFF @@ -78,862 +75,932 @@ class LocalRom: 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 get_bytes(self) -> bytes: + return bytes(self.file) -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: +class CV64PatchExtensions(APPatchExtension): + game = "Castlevania 64" - multiworld = world.multiworld - options = world.options - player = world.player - active_stage_exits = world.active_stage_exits - s1s_per_warp = world.s1s_per_warp + @staticmethod + def apply_patches(caller: APProcedurePatch, rom: bytes, options_file: str) -> bytes: + rom_data = RomData(rom) + options = json.loads(caller.get_file(options_file).decode("utf-8")) + + # NOP out the CRC BNEs + rom_data.write_int32(0x66C, 0x00000000) + rom_data.write_int32(0x678, 0x00000000) + + # Always offer Hard Mode on file creation + rom_data.write_int32(0xC8810, 0x240A0100) # ADDIU T2, R0, 0x0100 + + # Disable the Easy Mode cutoff point at Castle Center's elevator. + rom_data.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_data.write_byte(0xB73308, 0x00) + rom_data.write_byte(0xB7331A, 0x40) + rom_data.write_byte(0xB7332B, 0x4C) + rom_data.write_byte(0xB6302B, 0x00) + rom_data.write_byte(0x109F8F, 0x00) + + # Prevent Forest end cutscene flag from setting so it can be triggered infinitely. + rom_data.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_data.write_int32(0x97244, 0x803FDD60) + rom_data.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_data.write_int32s(0x197B0, [0x0C0FF7E6, # JAL 0x803FDF98 + 0x24840008]) # ADDIU A0, A0, 0x0008 + rom_data.write_int32s(0xBFDF98, patches.map_id_refresher) + + # Enable swapping characters when loading into a map by holding L. + rom_data.write_int32(0x97294, 0x803FDFC4) + rom_data.write_int32(0x19710, 0x080FF80E) # J 0x803FE038 + rom_data.write_int32s(0xBFDFC4, patches.character_changer) + + # Villa coffin time-of-day hack + rom_data.write_byte(0xD9D83, 0x74) + rom_data.write_int32(0xD9D84, 0x080FF14D) # J 0x803FC534 + rom_data.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_data.write_int32(0x6CEAA0, 0x240B0000) # ADDIU T3, R0, 0x0000 + elif options["character_stages"] == CharacterStages.option_carrie_only: + rom_data.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001 + else: + rom_data.write_int32(0x6CEAA0, 0x240B0001) # ADDIU T3, R0, 0x0001 + rom_data.write_int32(0x6CEAA4, 0x240D0001) # ADDIU T5, R0, 0x0001 + + # Were-bull arena flag hack + rom_data.write_int32(0x6E38F0, 0x0C0FF157) # JAL 0x803FC55C + rom_data.write_int32s(0xBFC55C, patches.werebull_flag_unsetter) + rom_data.write_int32(0xA949C, 0x0C0FF380) # JAL 0x803FCE00 + rom_data.write_int32s(0xBFCE00, patches.werebull_flag_pickup_setter) + + # Enable being able to carry multiple Special jewels, Nitros, and Mandragoras simultaneously + rom_data.write_int32(0xBF1F4, 0x3C038039) # LUI V1, 0x8039 + # Special1 + rom_data.write_int32(0xBF210, 0x80659C4B) # LB A1, 0x9C4B (V1) + rom_data.write_int32(0xBF214, 0x24A50001) # ADDIU A1, A1, 0x0001 + rom_data.write_int32(0xBF21C, 0xA0659C4B) # SB A1, 0x9C4B (V1) + # Special2 + rom_data.write_int32(0xBF230, 0x80659C4C) # LB A1, 0x9C4C (V1) + rom_data.write_int32(0xBF234, 0x24A50001) # ADDIU A1, A1, 0x0001 + rom_data.write_int32(0xbf23C, 0xA0659C4C) # SB A1, 0x9C4C (V1) + # Magical Nitro + rom_data.write_int32(0xBF360, 0x10000004) # B 0x8013C184 + rom_data.write_int32(0xBF378, 0x25E50001) # ADDIU A1, T7, 0x0001 + rom_data.write_int32(0xBF37C, 0x10000003) # B 0x8013C19C + # Mandragora + rom_data.write_int32(0xBF3A8, 0x10000004) # B 0x8013C1CC + rom_data.write_int32(0xBF3C0, 0x25050001) # ADDIU A1, T0, 0x0001 + rom_data.write_int32(0xBF3C4, 0x10000003) # B 0x8013C1E4 + + # Give PowerUps their Legacy of Darkness behavior when attempting to pick up more than two + rom_data.write_int16(0xA9624, 0x1000) + rom_data.write_int32(0xA9730, 0x24090000) # ADDIU T1, R0, 0x0000 + rom_data.write_int32(0xBF2FC, 0x080FF16D) # J 0x803FC5B4 + rom_data.write_int32(0xBF300, 0x00000000) # NOP + rom_data.write_int32s(0xBFC5B4, patches.give_powerup_stopper) + + # Rename the Wooden Stake and Rose to "You are a FOOL!" + rom_data.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_data.write_byte(0xEFF21, 0x2D) + + # Skip the "There is a white jewel" text so checking one saves the game instantly. + rom_data.write_int32s(0xEFC72, [0x00020002 for _ in range(37)]) + rom_data.write_int32(0xA8FC0, 0x24020001) # ADDIU V0, R0, 0x0001 + # Skip the yes/no prompts when activating things. + rom_data.write_int32s(0xBFDACC, patches.map_text_redirector) + rom_data.write_int32(0xA9084, 0x24020001) # ADDIU V0, R0, 0x0001 + rom_data.write_int32(0xBEBE8, 0x0C0FF6B4) # JAL 0x803FDAD0 + # Skip Vincent and Heinrich's mandatory-for-a-check dialogue + rom_data.write_int32(0xBED9C, 0x0C0FF6DA) # JAL 0x803FDB68 + # Skip the long yes/no prompt in the CC planetarium to set the pieces. + rom_data.write_int32(0xB5C5DF, 0x24030001) # ADDIU V1, R0, 0x0001 + # Skip the yes/no prompt to activate the CC elevator. + rom_data.write_int32(0xB5E3FB, 0x24020001) # ADDIU V0, R0, 0x0001 + # Skip the yes/no prompts to set Nitro/Mandragora at both walls. + rom_data.write_int32(0xB5DF3E, 0x24030001) # ADDIU V1, R0, 0x0001 + + # Custom message if you try checking the downstairs CC crack before removing the seal. + rom_data.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_data.write_int32s(0xBFDD20, patches.special_descriptions_redirector) + + # Change the Stage Select menu options + rom_data.write_int32s(0xADF64, patches.warp_menu_rewrite) + rom_data.write_int32s(0x10E0C8, patches.warp_pointer_table) + + # Play the "teleportation" sound effect when teleporting + rom_data.write_int32s(0xAE088, [0x08004FAB, # J 0x80013EAC + 0x2404019E]) # ADDIU A0, R0, 0x019E + + # Lizard-man save proofing + rom_data.write_int32(0xA99AC, 0x080FF0B8) # J 0x803FC2E0 + rom_data.write_int32s(0xBFC2E0, patches.boss_save_stopper) + + # Disable or guarantee vampire Vincent's fight + if options["vincent_fight_condition"] == VincentFightCondition.option_never: + rom_data.write_int32(0xAACC0, 0x24010001) # ADDIU AT, R0, 0x0001 + rom_data.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000 + elif options["vincent_fight_condition"] == VincentFightCondition.option_always: + rom_data.write_int32(0xAACE0, 0x24180010) # ADDIU T8, R0, 0x0010 + else: + rom_data.write_int32(0xAACE0, 0x24180000) # ADDIU T8, R0, 0x0000 + + # Disable or guarantee Renon's fight + rom_data.write_int32(0xAACB4, 0x080FF1A4) # J 0x803FC690 + if options["renon_fight_condition"] == RenonFightCondition.option_never: + rom_data.write_byte(0xB804F0, 0x00) + rom_data.write_byte(0xB80632, 0x00) + rom_data.write_byte(0xB807E3, 0x00) + rom_data.write_byte(0xB80988, 0xB8) + rom_data.write_byte(0xB816BD, 0xB8) + rom_data.write_byte(0xB817CF, 0x00) + rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr) + elif options["renon_fight_condition"] == RenonFightCondition.option_always: + rom_data.write_byte(0xB804F0, 0x0C) + rom_data.write_byte(0xB80632, 0x0C) + rom_data.write_byte(0xB807E3, 0x0C) + rom_data.write_byte(0xB80988, 0xC4) + rom_data.write_byte(0xB816BD, 0xC4) + rom_data.write_byte(0xB817CF, 0x0C) + rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker_jr) + else: + rom_data.write_int32s(0xBFC690, patches.renon_cutscene_checker) + + # NOP the Easy Mode check when buying a thing from Renon, so his fight can be triggered even on this mode. + rom_data.write_int32(0xBD8B4, 0x00000000) + + # Disable or guarantee the Bad Ending + if options["bad_ending_condition"] == BadEndingCondition.option_never: + rom_data.write_int32(0xAEE5C6, 0x3C0A0000) # LUI T2, 0x0000 + elif options["bad_ending_condition"] == BadEndingCondition.option_always: + rom_data.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_data.write_int32(0x6E937C, 0x080FF12E) # J 0x803FC4B8 + rom_data.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_data.write_byte(0xBF30B, 0x63) # Most items + rom_data.write_byte(0xBF3F7, 0x63) # Sun/Moon cards + rom_data.write_byte(0xBF353, 0x64) # Keys (increase regardless) + + # Change the item healing values if "Nerf Healing" is turned on + if options["nerf_healing_items"]: + rom_data.write_byte(0xB56371, 0x50) # Healing kit (100 -> 80) + rom_data.write_byte(0xB56374, 0x32) # Roast beef ( 80 -> 50) + rom_data.write_byte(0xB56377, 0x19) # Roast chicken ( 50 -> 25) + + # Disable loading zone healing if turned off + if not options["loading_zone_heals"]: + rom_data.write_byte(0xD99A5, 0x00) # Skip all loading zone checks + rom_data.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_data.write_byte(0xEE4F5, 0x00) # Special1 + rom_data.write_byte(0xEE505, 0x00) # Special2 + # Make the Special2 the same size as a Red jewel(L) to further distinguish them + rom_data.write_int32(0xEE4FC, 0x3FA66666) + + # Prevent the vanilla Magical Nitro transport's "can explode" flag from setting + rom_data.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_data.write_byte(0xA6253D, 0x03) + + # Enable the Game Over's "Continue" menu starting the cursor on whichever checkpoint is most recent + rom_data.write_int32(0xB4DDC, 0x0C060D58) # JAL 0x80183560 + rom_data.write_int32s(0x106750, patches.continue_cursor_start_checker) + rom_data.write_int32(0x1C444, 0x080FF08A) # J 0x803FC228 + rom_data.write_int32(0x1C2A0, 0x080FF08A) # J 0x803FC228 + rom_data.write_int32s(0xBFC228, patches.savepoint_cursor_updater) + rom_data.write_int32(0x1C2D0, 0x080FF094) # J 0x803FC250 + rom_data.write_int32s(0xBFC250, patches.stage_start_cursor_updater) + rom_data.write_byte(0xB585C8, 0xFF) + + # Make the Special1 and 2 play sounds when you reach milestones with them. + rom_data.write_int32s(0xBFDA50, patches.special_sound_notifs) + rom_data.write_int32(0xBF240, 0x080FF694) # J 0x803FDA50 + rom_data.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_data.write_int16s(0x104AC8, [0x0000, 0x0006, + 0x0013, 0x0015]) + + # Take the contract in Waterway off of its 00400000 bitflag. + rom_data.write_byte(0x87E3DA, 0x00) + + # Spawn coordinates list extension + rom_data.write_int32(0xD5BF4, 0x080FF103) # J 0x803FC40C + rom_data.write_int32s(0xBFC40C, patches.spawn_coordinates_extension) + rom_data.write_int32s(0x108A5E, patches.waterway_end_coordinates) + + # 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_data.write_byte(0xC9FE3, 0xD4) + rom_data.write_byte(0xCA055, 0x08) + rom_data.write_byte(0xCA066, 0x40) + rom_data.write_int32(0xCA068, 0x860C17D0) # LH T4, 0x17D0 (S0) + rom_data.write_byte(0xCA06D, 0x08) + rom_data.write_byte(0x104A31, 0x01) + rom_data.write_byte(0x104A39, 0x01) + rom_data.write_byte(0x104A89, 0x01) + rom_data.write_byte(0x104A91, 0x01) + rom_data.write_byte(0x104A99, 0x01) + rom_data.write_byte(0x104AA1, 0x01) + + # CC top elevator switch check + rom_data.write_int32(0x6CF0A0, 0x0C0FF0B0) # JAL 0x803FC2C0 + rom_data.write_int32s(0xBFC2C0, patches.elevator_flag_checker) + + # Disable time restrictions + if options["disable_time_restrictions"]: + # Fountain + rom_data.write_int32(0x6C2340, 0x00000000) # NOP + rom_data.write_int32(0x6C257C, 0x10000023) # B [forward 0x23] + # Rosa + rom_data.write_byte(0xEEAAB, 0x00) + rom_data.write_byte(0xEEAAD, 0x18) + # Moon doors + rom_data.write_int32(0xDC3E0, 0x00000000) # NOP + rom_data.write_int32(0xDC3E8, 0x00000000) # NOP + # Sun doors + rom_data.write_int32(0xDC410, 0x00000000) # NOP + rom_data.write_int32(0xDC418, 0x00000000) # NOP + + # Custom data-loading code + rom_data.write_int32(0x6B5028, 0x08060D70) # J 0x801835D0 + rom_data.write_int32s(0x1067B0, patches.custom_code_loader) + + # Custom remote item rewarding and DeathLink receiving code + rom_data.write_int32(0x19B98, 0x080FF000) # J 0x803FC000 + rom_data.write_int32s(0xBFC000, patches.remote_item_giver) + rom_data.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_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08] + rom_data.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_data.write_byte(0xBFBFDE, 0x01) + + # DeathLink counter decrementer code + rom_data.write_int32(0x1C340, 0x080FF8F0) # J 0x803FE3C0 + rom_data.write_int32s(0xBFE3C0, patches.deathlink_counter_decrementer) + rom_data.write_int32(0x25B6C, 0x080FFA5E) # J 0x803FE978 + rom_data.write_int32s(0xBFE978, patches.launch_fall_killer) + + # Death flag un-setter on "Beginning of stage" state overwrite code + rom_data.write_int32(0x1C2B0, 0x080FF047) # J 0x803FC11C + rom_data.write_int32s(0xBFC11C, patches.death_flag_unsetter) + + # Warp menu-opening code + rom_data.write_int32(0xB9BA8, 0x080FF099) # J 0x803FC264 + rom_data.write_int32s(0xBFC264, patches.warp_menu_opener) + + # NPC item textbox hack + rom_data.write_int32(0xBF1DC, 0x080FF904) # J 0x803FE410 + rom_data.write_int32(0xBF1E0, 0x27BDFFE0) # ADDIU SP, SP, -0x20 + rom_data.write_int32s(0xBFE410, patches.npc_item_hack) + + # Sub-weapon check function hook + rom_data.write_int32(0xBF32C, 0x00000000) # NOP + rom_data.write_int32(0xBF330, 0x080FF05E) # J 0x803FC178 + rom_data.write_int32s(0xBFC178, patches.give_subweapon_stopper) + + # Warp menu Special1 restriction + rom_data.write_int32(0xADD68, 0x0C04AB12) # JAL 0x8012AC48 + rom_data.write_int32s(0xADE28, patches.stage_select_overwrite) + rom_data.write_byte(0xADE47, options["s1s_per_warp"]) + + # Dracula's door text pointer hijack + rom_data.write_int32(0xD69F0, 0x080FF141) # J 0x803FC504 + rom_data.write_int32s(0xBFC504, patches.dracula_door_text_redirector) + + # Dracula's chamber condition + rom_data.write_int32(0xE2FDC, 0x0804AB25) # J 0x8012AC78 + rom_data.write_int32s(0xADE84, patches.special_goal_checker) + rom_data.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_data.write_int32(0x6C8A54, 0x0C0FF0C1) # JAL 0x803FC304 + rom_data.write_int32s(0xBFC304, patches.crystal_special2_giver) + rom_data.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_data.write_int32(0xBBD50, 0x080FF18C) # J 0x803FC630 + rom_data.write_int32s(0xBFC630, patches.boss_special2_giver) + rom_data.write_int32s(0xBFC55C, patches.werebull_flag_unsetter_special2_electric_boogaloo) + rom_data.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n" + f"You'll need to defeat\n" + f"{options['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 {options['required_s2s']}/{options['total_s2s']}\n" \ + f"to battle Dracula." + elif options["draculas_condition"] == DraculasCondition.option_specials: + special2_name = "Special2" + rom_data.write_bytes(0xBFCC6E, cv64_string_to_bytearray(f"It won't budge!\n" + f"You'll need to find\n" + f"{options['required_s2s']} Special2 jewels\n" + f"to undo the seal.", True)) + special2_text = f"Need {options['required_s2s']}/{options['total_s2s']} to kill Dracula.\n" \ + f"Looking closely, you see...\n" \ + f"a piece of him within?" + else: + rom_data.write_byte(0xADE8F, 0x00) + special2_name = "Special2" + special2_text = "If you're reading this,\n" \ + "how did you get a Special2!?" + rom_data.write_byte(0xADE8F, options["required_s2s"]) + # Change the Special2 name depending on the setting. + rom_data.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"{options['s1s_per_warp']} per warp unlock.\n" + f"{options['total_special1s']} exist in total.\n" + f"Z + R + START to warp.") + cv64_string_to_bytearray( + special2_text) + rom_data.write_bytes(0xBFE53C, special_text_bytes) + + # On-the-fly overlay modifier + rom_data.write_int32s(0xBFC338, patches.double_component_checker) + rom_data.write_int32s(0xBFC3D4, patches.downstairs_seal_checker) + rom_data.write_int32s(0xBFE074, patches.mandragora_with_nitro_setter) + rom_data.write_int32s(0xBFC700, patches.overlay_modifiers) + + # On-the-fly actor data modifier hook + rom_data.write_int32(0xEAB04, 0x080FF21E) # J 0x803FC878 + rom_data.write_int32s(0xBFC870, patches.map_data_modifiers) + + # Fix to make flags apply to freestanding invisible items properly + rom_data.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_data.write_int32(0x10B2D8, 0x00000002) # Left Tower Door + rom_data.write_int32(0x10B2F0, 0x00000003) # Storeroom Door + rom_data.write_int32(0x10B2FC, 0x00000001) # Archives Door + rom_data.write_int32(0x10B314, 0x00000004) # Maze Gate + rom_data.write_int32(0x10B350, 0x00000005) # Copper Door + rom_data.write_int32(0x10B3A4, 0x00000006) # Torture Chamber Door + rom_data.write_int32(0x10B3B0, 0x00000007) # ToE Gate + rom_data.write_int32(0x10B3BC, 0x00000008) # Science Door1 + rom_data.write_int32(0x10B3C8, 0x00000009) # Science Door2 + rom_data.write_int32(0x10B3D4, 0x0000000A) # Science Door3 + rom_data.write_int32(0x6F0094, 0x0000000B) # CT Door 1 + rom_data.write_int32(0x6F00A4, 0x0000000C) # CT Door 2 + rom_data.write_int32(0x6F00B4, 0x0000000D) # CT Door 3 + # Item counter decrement check modifications: + rom_data.write_int32(0xEDA84, 0x00000001) # Archives Door + rom_data.write_int32(0xEDA8C, 0x00000002) # Left Tower Door + rom_data.write_int32(0xEDA94, 0x00000003) # Storeroom Door + rom_data.write_int32(0xEDA9C, 0x00000004) # Maze Gate + rom_data.write_int32(0xEDAA4, 0x00000005) # Copper Door + rom_data.write_int32(0xEDAAC, 0x00000006) # Torture Chamber Door + rom_data.write_int32(0xEDAB4, 0x00000007) # ToE Gate + rom_data.write_int32(0xEDABC, 0x00000008) # Science Door1 + rom_data.write_int32(0xEDAC4, 0x00000009) # Science Door2 + rom_data.write_int32(0xEDACC, 0x0000000A) # Science Door3 + rom_data.write_int32(0xEDAD4, 0x0000000B) # CT Door 1 + rom_data.write_int32(0xEDADC, 0x0000000C) # CT Door 2 + rom_data.write_int32(0xEDAE4, 0x0000000D) # CT Door 3 + + # Fix ToE gate's "unlocked" flag in the locked door flags table + rom_data.write_int16(0x10B3B6, 0x0001) + + rom_data.write_int32(0x10AB2C, 0x8015FBD4) # Maze Gates' check code pointer adjustments + rom_data.write_int32(0x10AB40, 0x8015FBD4) + rom_data.write_int32s(0x10AB50, [0x0D0C0000, + 0x8015FBD4]) + rom_data.write_int32s(0x10AB64, [0x0D0C0000, + 0x8015FBD4]) + rom_data.write_int32s(0xE2E14, patches.normal_door_hook) + rom_data.write_int32s(0xBFC5D0, patches.normal_door_code) + rom_data.write_int32s(0x6EF298, patches.ct_door_hook) + rom_data.write_int32s(0xBFC608, patches.ct_door_code) + # Fix key counter not decrementing if 2 or above + rom_data.write_int32(0xAA0E0, 0x24020000) # ADDIU V0, R0, 0x0000 + + # Make the Easy-only candle drops in Room of Clocks appear on any difficulty + rom_data.write_byte(0x9B518F, 0x01) + + # Slightly move some once-invisible freestanding items to be more visible + if options["invisible_items"] == InvisibleItems.option_reveal_all: + rom_data.write_byte(0x7C7F95, 0xEF) # Forest dirge maiden statue + rom_data.write_byte(0x7C7FA8, 0xAB) # Forest werewolf statue + rom_data.write_byte(0x8099C4, 0x8C) # Villa courtyard tombstone + rom_data.write_byte(0x83A626, 0xC2) # Villa living room painting + # rom_data.write_byte(0x83A62F, 0x64) # Villa Mary's room table + rom_data.write_byte(0xBFCB97, 0xF5) # CC torture instrument rack + rom_data.write_byte(0x8C44D5, 0x22) # CC red carpet hallway knight + rom_data.write_byte(0x8DF57C, 0xF1) # CC cracked wall hallway flamethrower + rom_data.write_byte(0x90FCD6, 0xA5) # CC nitro hallway flamethrower + rom_data.write_byte(0x90FB9F, 0x9A) # CC invention room round machine + rom_data.write_byte(0x90FBAF, 0x03) # CC invention room giant famicart + rom_data.write_byte(0x90FE54, 0x97) # CC staircase knight (x) + rom_data.write_byte(0x90FE58, 0xFB) # CC staircase knight (z) + + # Change the bitflag on the item in upper coffin in Forest final switch gate tomb to one that's not used by + # something else. + rom_data.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_data.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_data.write_byte(0xEEDAD, 0x02) + rom_data.write_byte(0xEEDD9, 0x01) + elif options["post_behemoth_boss"] == PostBehemothBoss.option_always_rosa: + rom_data.write_byte(0xEEDAD, 0x00) + rom_data.write_byte(0xEEDD9, 0x03) + # Put both on the same flag so changing character won't trigger a rematch with the same boss. + rom_data.write_byte(0xEED8B, 0x40) + elif options["post_behemoth_boss"] == PostBehemothBoss.option_always_camilla: + rom_data.write_byte(0xEEDAD, 0x03) + rom_data.write_byte(0xEEDD9, 0x00) + rom_data.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_data.write_byte(0x109FB3, 0x56) + rom_data.write_byte(0x109FBF, 0x44) + rom_data.write_byte(0xD9D44, 0x14) + rom_data.write_byte(0xD9D4C, 0x14) + elif options["room_of_clocks_boss"] == RoomOfClocksBoss.option_always_death: + rom_data.write_byte(0x109FBF, 0x44) + rom_data.write_byte(0xD9D45, 0x00) + # Put both on the same flag so changing character won't trigger a rematch with the same boss. + rom_data.write_byte(0x109FB7, 0x90) + rom_data.write_byte(0x109FC3, 0x90) + elif options["room_of_clocks_boss"] == RoomOfClocksBoss.option_always_actrise: + rom_data.write_byte(0x109FB3, 0x56) + rom_data.write_int32(0xD9D44, 0x00000000) + rom_data.write_byte(0xD9D4D, 0x00) + rom_data.write_byte(0x109FB7, 0x90) + rom_data.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_data.write_int32(0xB318B4, 0x240E0001) # ADDIU T6, R0, 0x0001 + + # Tunnel gondola skip + if options["skip_gondolas"]: + rom_data.write_int32(0x6C5F58, 0x080FF7D0) # J 0x803FDF40 + rom_data.write_int32s(0xBFDF40, patches.gondola_skipper) + # New gondola transfer point candle coordinates + rom_data.write_byte(0xBFC9A3, 0x04) + rom_data.write_bytes(0x86D824, [0x27, 0x01, 0x10, 0xF7, 0xA0]) + + # Waterway brick platforms skip + if options["skip_waterway_blocks"]: + rom_data.write_int32(0x6C7E2C, 0x00000000) # NOP + + # Ambience silencing fix + rom_data.write_int32(0xD9270, 0x080FF840) # J 0x803FE100 + rom_data.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_data.write_int32s(0xAE10C, [0x08004FAB, # J 0x80013EAC + 0x3404829B]) # ORI A0, R0, 0x829B + rom_data.write_int32s(0xD9E8C, [0x08004FAB, # J 0x80013EAC + 0x3404829B]) # ORI A0, R0, 0x829B + # Fan meeting room ambience fix + rom_data.write_int32(0x109964, 0x803FE13C) + + # Make the Villa coffin cutscene skippable + rom_data.write_int32(0xAA530, 0x080FF880) # J 0x803FE200 + rom_data.write_int32s(0xBFE200, patches.coffin_cutscene_skipper) + + # Increase shimmy speed + if options["increase_shimmy_speed"]: + rom_data.write_byte(0xA4241, 0x5A) + + # Disable landing fall damage + if options["fall_guard"]: + rom_data.write_byte(0x27B23, 0x00) + + # Enable the unused film reel effect on all cutscenes + if options["cinematic_experience"]: + rom_data.write_int32(0xAA33C, 0x240A0001) # ADDIU T2, R0, 0x0001 + rom_data.write_byte(0xAA34B, 0x0C) + rom_data.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_data.write_int32(0xBF2EC, 0x806B619B) # LB T3, 0x619B (V1) + rom_data.write_int32(0xBFC5BC, 0xA06C619B) # SB T4, 0x619B (V1) + # Make Reinhardt's whip check the menu PowerUp counter + rom_data.write_int32(0x69FA08, 0x80CC619B) # LB T4, 0x619B (A2) + rom_data.write_int32(0x69FBFC, 0x80C3619B) # LB V1, 0x619B (A2) + rom_data.write_int32(0x69FFE0, 0x818C9C53) # LB T4, 0x9C53 (T4) + # Make Carrie's orb check the menu PowerUp counter + rom_data.write_int32(0x6AC86C, 0x8105619B) # LB A1, 0x619B (T0) + rom_data.write_int32(0x6AC950, 0x8105619B) # LB A1, 0x619B (T0) + rom_data.write_int32(0x6AC99C, 0x810E619B) # LB T6, 0x619B (T0) + rom_data.write_int32(0x5AFA0, 0x80639C53) # LB V1, 0x9C53 (V1) + rom_data.write_int32(0x5B0A0, 0x81089C53) # LB T0, 0x9C53 (T0) + rom_data.write_byte(0x391C7, 0x00) # Prevent PowerUps from dropping from regular enemies + rom_data.write_byte(0xEDEDF, 0x03) # Make any vanishing PowerUps that do show up L jewels instead + # Rename the PowerUp to "PermaUp" + rom_data.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_data.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_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00]) + + # Write the associated code for the randomized (or disabled) music list. + if options["background_music"]: + rom_data.write_int32(0x14588, 0x08060D60) # J 0x80183580 + rom_data.write_int32(0x14590, 0x00000000) # NOP + rom_data.write_int32s(0x106770, patches.music_modifier) + rom_data.write_int32(0x15780, 0x0C0FF36E) # JAL 0x803FCDB8 + rom_data.write_int32s(0xBFCDB8, patches.music_comparer_modifier) + + # Enable storing item flags anywhere and changing the item model/visibility on any item instance. + rom_data.write_int32s(0xA857C, [0x080FF38F, # J 0x803FCE3C + 0x94D90038]) # LHU T9, 0x0038 (A2) + rom_data.write_int32s(0xBFCE3C, patches.item_customizer) + rom_data.write_int32s(0xA86A0, [0x0C0FF3AF, # JAL 0x803FCEBC + 0x95C40002]) # LHU A0, 0x0002 (T6) + rom_data.write_int32s(0xBFCEBC, patches.item_appearance_switcher) + rom_data.write_int32s(0xA8728, [0x0C0FF3B8, # JAL 0x803FCEE4 + 0x01396021]) # ADDU T4, T1, T9 + rom_data.write_int32s(0xBFCEE4, patches.item_model_visibility_switcher) + rom_data.write_int32s(0xA8A04, [0x0C0FF3C2, # JAL 0x803FCF08 + 0x018B6021]) # ADDU T4, T4, T3 + rom_data.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_data.write_int32s(0xE649C, [0x0C0FFA03, # JAL 0x803FE80C + 0x956C0002]) # LHU T4, 0x0002 (T3) + rom_data.write_int32s(0xA8B08, [0x080FFA0C, # J 0x803FE830 + 0x960A0038]) # LHU T2, 0x0038 (S0) + rom_data.write_int32s(0xE8584, [0x0C0FFA21, # JAL 0x803FE884 + 0x95D80000]) # LHU T8, 0x0000 (T6) + rom_data.write_int32s(0xE7AF0, [0x0C0FFA2A, # JAL 0x803FE8A8 + 0x958D0000]) # LHU T5, 0x0000 (T4) + rom_data.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_data.write_int32(0xE87F8, 0x00000000) # NOP + rom_data.write_int16(0xE836C, 0x1000) + rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34 + rom_data.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_data.write_int32(0xE7D54, 0x00000000) # NOP + rom_data.write_int16(0xE7908, 0x1000) + rom_data.write_byte(0xE7A5C, 0x10) + rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C + rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter) + + # New flag values to put in each 3HB vanilla flag's spot + rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock + rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock + rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub + rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab + rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab + rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock + rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge + rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge + rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate + rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal + rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab + rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge + rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate + rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab + rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab + rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab + rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab + rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier + rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data + + # Once-per-frame gameplay checks + rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034 + rom_data.write_int32(0xBFD058, 0x0801AEB5) # J 0x8006BAD4 + + # Everything related to dropping the previous sub-weapon + if options["drop_previous_sub_weapon"]: + rom_data.write_int32(0xBFD034, 0x080FF3FF) # J 0x803FCFFC + rom_data.write_int32(0xBFC190, 0x080FF3F2) # J 0x803FCFC8 + rom_data.write_int32s(0xBFCFC4, patches.prev_subweapon_spawn_checker) + rom_data.write_int32s(0xBFCFFC, patches.prev_subweapon_fall_checker) + rom_data.write_int32s(0xBFD060, patches.prev_subweapon_dropper) + + # Everything related to the Countdown counter + if options["countdown"]: + rom_data.write_int32(0xBFD03C, 0x080FF9DC) # J 0x803FE770 + rom_data.write_int32(0xD5D48, 0x080FF4EC) # J 0x803FD3B0 + rom_data.write_int32s(0xBFD3B0, patches.countdown_number_displayer) + rom_data.write_int32s(0xBFD6DC, patches.countdown_number_manager) + rom_data.write_int32s(0xBFE770, patches.countdown_demo_hider) + rom_data.write_int32(0xBFCE2C, 0x080FF5D2) # J 0x803FD748 + rom_data.write_int32s(0xBB168, [0x080FF5F4, # J 0x803FD7D0 + 0x8E020028]) # LW V0, 0x0028 (S0) + rom_data.write_int32s(0xBB1D0, [0x080FF5FB, # J 0x803FD7EC + 0x8E020028]) # LW V0, 0x0028 (S0) + rom_data.write_int32(0xBC4A0, 0x080FF5E6) # J 0x803FD798 + rom_data.write_int32(0xBC4C4, 0x080FF5E6) # J 0x803FD798 + rom_data.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_data.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_data.write_int32s(0xBFD788, [0x080FF717, # J 0x803FDC5C + 0x2529FFFF]) # ADDIU T1, T1, 0xFFFF + rom_data.write_int32s(0xBFDC5C, patches.countdown_extra_safety_check) + rom_data.write_int32(0xA9ECC, + 0x00000000) # NOP the pointless overwrite of the item actor appearance custom value. + + # Ice Trap stuff + rom_data.write_int32(0x697C60, 0x080FF06B) # J 0x803FC18C + rom_data.write_int32(0x6A5160, 0x080FF06B) # J 0x803FC18C + rom_data.write_int32s(0xBFC1AC, patches.ice_trap_initializer) + rom_data.write_int32s(0xBFE700, patches.the_deep_freezer) + rom_data.write_int32s(0xB2F354, [0x3739E4C0, # ORI T9, T9, 0xE4C0 + 0x03200008, # JR T9 + 0x00000000]) # NOP + rom_data.write_int32s(0xBFE4C0, patches.freeze_verifier) + + # Fix for the ice chunk model staying when getting bitten by the maze garden dogs + rom_data.write_int32(0xA2DC48, 0x803FE9C0) + rom_data.write_int32s(0xBFE9C0, patches.dog_bite_ice_trap_fix) + + # Initial Countdown numbers + rom_data.write_int32(0xAD6A8, 0x080FF60A) # J 0x803FD828 + rom_data.write_int32s(0xBFD828, patches.new_game_extras) + + # Everything related to shopsanity + if options["shopsanity"]: + rom_data.write_byte(0xBFBFDF, 0x01) + rom_data.write_bytes(0x103868, cv64_string_to_bytearray("Not obtained. ")) + rom_data.write_int32s(0xBFD8D0, patches.shopsanity_stuff) + rom_data.write_int32(0xBD828, 0x0C0FF643) # JAL 0x803FD90C + rom_data.write_int32(0xBD5B8, 0x0C0FF651) # JAL 0x803FD944 + rom_data.write_int32(0xB0610, 0x0C0FF665) # JAL 0x803FD994 + rom_data.write_int32s(0xBD24C, [0x0C0FF677, # J 0x803FD9DC + 0x00000000]) # NOP + rom_data.write_int32(0xBD618, 0x0C0FF684) # JAL 0x803FDA10 + + # Panther Dash running + if options["panther_dash"]: + rom_data.write_int32(0x69C8C4, 0x0C0FF77E) # JAL 0x803FDDF8 + rom_data.write_int32(0x6AA228, 0x0C0FF77E) # JAL 0x803FDDF8 + rom_data.write_int32s(0x69C86C, [0x0C0FF78E, # JAL 0x803FDE38 + 0x3C01803E]) # LUI AT, 0x803E + rom_data.write_int32s(0x6AA1D0, [0x0C0FF78E, # JAL 0x803FDE38 + 0x3C01803E]) # LUI AT, 0x803E + rom_data.write_int32(0x69D37C, 0x0C0FF79E) # JAL 0x803FDE78 + rom_data.write_int32(0x6AACE0, 0x0C0FF79E) # JAL 0x803FDE78 + rom_data.write_int32s(0xBFDDF8, patches.panther_dash) + # Jump prevention + if options["panther_dash"] == PantherDash.option_jumpless: + rom_data.write_int32(0xBFDE2C, 0x080FF7BB) # J 0x803FDEEC + rom_data.write_int32(0xBFD044, 0x080FF7B1) # J 0x803FDEC4 + rom_data.write_int32s(0x69B630, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8CCD0000]) # LW T5, 0x0000 (A2) + rom_data.write_int32s(0x6A8EC0, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8CCC0000]) # LW T4, 0x0000 (A2) + # Fun fact: KCEK put separate code to handle coyote time jumping + rom_data.write_int32s(0x69910C, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8C4E0000]) # LW T6, 0x0000 (V0) + rom_data.write_int32s(0x6A6718, [0x0C0FF7C6, # JAL 0x803FDF18 + 0x8C4E0000]) # LW T6, 0x0000 (V0) + rom_data.write_int32s(0xBFDEC4, patches.panther_jump_preventer) + + # Everything related to Big Toss. + if options["big_toss"]: + rom_data.write_int32s(0x27E90, [0x0C0FFA38, # JAL 0x803FE8E0 + 0xAFB80074]) # SW T8, 0x0074 (SP) + rom_data.write_int32(0x26F54, 0x0C0FFA4D) # JAL 0x803FE934 + rom_data.write_int32s(0xBFE8E0, patches.big_tosser) + + # Write the specified window colors + rom_data.write_byte(0xAEC23, options["window_color_r"] << 4) + rom_data.write_byte(0xAEC33, options["window_color_g"] << 4) + rom_data.write_byte(0xAEC47, options["window_color_b"] << 4) + rom_data.write_byte(0xAEC43, options["window_color_a"] << 4) + + # Everything relating to loading the other game items text + rom_data.write_int32(0xA8D8C, 0x080FF88F) # J 0x803FE23C + rom_data.write_int32(0xBEA98, 0x0C0FF8B4) # JAL 0x803FE2D0 + rom_data.write_int32(0xBEAB0, 0x0C0FF8BD) # JAL 0x803FE2F8 + rom_data.write_int32(0xBEACC, 0x0C0FF8C5) # JAL 0x803FE314 + rom_data.write_int32s(0xBFE23C, patches.multiworld_item_name_loader) + rom_data.write_bytes(0x10F188, [0x00 for _ in range(264)]) + rom_data.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_data.write_int32(0xA8D94, 0x0C0FF9F0) # JAL 0x803FE7C0 + rom_data.write_int32s(0xBFE7C0, [0x3C088039, # LUI T0, 0x8039 + 0x24090020, # ADDIU T1, R0, 0x0020 + 0x0804EDCE, # J 0x8013B738 + 0xA1099BE0]) # SB T1, 0x9BE0 (T0) + + return rom_data.get_bytes() + + @staticmethod + def patch_ap_graphics(caller: APProcedurePatch, rom: bytes) -> bytes: + rom_data = RomData(rom) + + # Extract the item models file, decompress it, append the AP icons, compress it back, re-insert it. + items_file = lzkn64.decompress_buffer(rom_data.read_bytes(0x9C5310, 0x3D28)) + compressed_file = lzkn64.compress_buffer(items_file[0:0x69B6] + pkgutil.get_data(__name__, "data/ap_icons.bin")) + rom_data.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_data.write_int32s(0x95F04, [0x80BB2D88, 0x00BB2D88 + len(compressed_file)]) + # Update the items' decompressed file size tables with the new file's decompressed file size. + rom_data.write_int16(0x95706, 0x7BF0) + rom_data.write_int16(0x104CCE, 0x7BF0) + # Update the Wooden Stake and Roses' item appearance settings table to point to the Archipelago item graphics. + rom_data.write_int16(0xEE5BA, 0x7B38) + rom_data.write_int16(0xEE5CA, 0x7280) + # Change the items' sizes. The progression one will be larger than the non-progression one. + rom_data.write_int32(0xEE5BC, 0x3FF00000) + rom_data.write_int32(0xEE5CC, 0x3FA00000) + + return rom_data.get_bytes() + + +class CV64ProcedurePatch(APProcedurePatch, APTokenMixin): + hash = [CV64_US_10_HASH] + patch_file_ending: str = ".apcv64" + result_file_ending: str = ".z64" + + game = "Castlevania 64" + + procedure = [ + ("apply_patches", ["options.json"]), + ("apply_tokens", ["token_data.bin"]), + ("patch_ap_graphics", []) + ] + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict[int, bytes], shop_name_list: List[str], + shop_desc_list: List[List[Union[int, str, None]]], shop_colors_list: List[bytearray], + active_locations: Iterable[Location]) -> None: active_warp_list = world.active_warp_list - required_s2s = world.required_s2s - total_s2s = world.total_s2s + s1s_per_warp = world.s1s_per_warp - # NOP out the CRC BNEs - rom.write_int32(0x66C, 0x00000000) - rom.write_int32(0x678, 0x00000000) + # Write all the new item/loading zone/shop/lighting/music/etc. values. + for offset, data in offset_data.items(): + patch.write_token(APTokenTypes.WRITE, offset, data) - # 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) + # Write the new Stage Select menu destinations. 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")) + patch.write_token(APTokenTypes.WRITE, + warp_map_offsets[i], get_stage_info(active_warp_list[i], "start map id")) + patch.write_token(APTokenTypes.WRITE, + 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")) + patch.write_token(APTokenTypes.WRITE, + warp_map_offsets[i], get_stage_info(active_warp_list[i], "mid map id")) + patch.write_token(APTokenTypes.WRITE, + 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. + patch.write_token(APTokenTypes.WRITE, 0xEFAD0, bytes( + 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]}"))) - # 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: + # Write the new File Select stage numbers. + for stage in world.active_stage_exits: for offset in get_stage_info(stage, "save number offsets"): - rom.write_byte(offset, active_stage_exits[stage]["position"]) + patch.write_token(APTokenTypes.WRITE, offset, bytes([world.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) + # Write all the shop text. + if world.options.shopsanity: + patch.write_token(APTokenTypes.WRITE, 0x103868, bytes(cv64_string_to_bytearray("Not obtained. "))) - # 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 = [] + shopsanity_name_text = bytearray(0) + shopsanity_desc_text = bytearray(0) 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] + shopsanity_desc_text += bytearray([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) + patch.write_token(APTokenTypes.WRITE, 0x1AD00, bytes(shopsanity_name_text)) + patch.write_token(APTokenTypes.WRITE, 0x1A800, bytes(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 + # 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: + if loc.address is None or get_location_info(loc.name, "type") == "shop" or loc.item.player == world.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) + wrapped_name, num_lines = cv64_text_wrap(item_name + "\nfor " + + world.multiworld.get_player_name(loc.item.player), 96) + patch.write_token(APTokenTypes.WRITE, inject_address, bytes(get_item_text_color(loc) + + cv64_string_to_bytearray(wrapped_name))) + patch.write_token(APTokenTypes.WRITE, inject_address + 255, bytes([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)]) + # Write the secondary name the client will use to distinguish a vanilla ROM from an AP one. + patch.write_token(APTokenTypes.WRITE, 0xBFBFD0, "ARCHIPELAGO1".encode("utf-8")) + # Write the slot authentication + patch.write_token(APTokenTypes.WRITE, 0xBFBFE0, bytes(world.auth)) - # 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) + patch.write_file("token_data.bin", patch.get_token_binary()) + # Write these slot options to a JSON. + options_dict = { + "character_stages": world.options.character_stages.value, + "vincent_fight_condition": world.options.vincent_fight_condition.value, + "renon_fight_condition": world.options.renon_fight_condition.value, + "bad_ending_condition": world.options.bad_ending_condition.value, + "increase_item_limit": world.options.increase_item_limit.value, + "nerf_healing_items": world.options.nerf_healing_items.value, + "loading_zone_heals": world.options.loading_zone_heals.value, + "disable_time_restrictions": world.options.disable_time_restrictions.value, + "death_link": world.options.death_link.value, + "draculas_condition": world.options.draculas_condition.value, + "invisible_items": world.options.invisible_items.value, + "post_behemoth_boss": world.options.post_behemoth_boss.value, + "room_of_clocks_boss": world.options.room_of_clocks_boss.value, + "skip_gondolas": world.options.skip_gondolas.value, + "skip_waterway_blocks": world.options.skip_waterway_blocks.value, + "s1s_per_warp": world.options.special1s_per_warp.value, + "required_s2s": world.required_s2s, + "total_s2s": world.total_s2s, + "total_special1s": world.options.total_special1s.value, + "increase_shimmy_speed": world.options.increase_shimmy_speed.value, + "fall_guard": world.options.fall_guard.value, + "cinematic_experience": world.options.cinematic_experience.value, + "permanent_powerups": world.options.permanent_powerups.value, + "background_music": world.options.background_music.value, + "multi_hit_breakables": world.options.multi_hit_breakables.value, + "drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value, + "countdown": world.options.countdown.value, + "shopsanity": world.options.shopsanity.value, + "panther_dash": world.options.panther_dash.value, + "big_toss": world.options.big_toss.value, + "window_color_r": world.options.window_color_r.value, + "window_color_g": world.options.window_color_g.value, + "window_color_b": world.options.window_color_b.value, + "window_color_a": world.options.window_color_a.value, + } -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) + patch.write_file("options.json", json.dumps(options_dict).encode('utf-8')) def get_base_rom_bytes(file_name: str = "") -> bytes: @@ -944,7 +1011,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes: basemd5 = hashlib.md5() basemd5.update(base_rom_bytes) - if CV64US10HASH != basemd5.hexdigest(): + if CV64_US_10_HASH != 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) diff --git a/worlds/cv64/stages.py b/worlds/cv64/stages.py index a6fa6679..d7059b35 100644 --- a/worlds/cv64/stages.py +++ b/worlds/cv64/stages.py @@ -47,9 +47,9 @@ if TYPE_CHECKING: # 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, + "start region": rname.forest_start, "start map id": b"\x00", "start spawn id": b"\x00", + "mid region": rname.forest_mid, "mid map id": b"\x00", "mid spawn id": b"\x04", + "end region": rname.forest_end, "end map id": b"\x00", "end spawn id": b"\x01", "endzone map offset": 0xB6302F, "endzone spawn offset": 0xB6302B, "save number offsets": [0x1049C5, 0x1049CD, 0x1049D5], "regions": [rname.forest_start, @@ -58,9 +58,9 @@ stage_info = { }, "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, + "start region": rname.cw_start, "start map id": b"\x02", "start spawn id": b"\x00", + "mid region": rname.cw_start, "mid map id": b"\x02", "mid spawn id": b"\x07", + "end region": rname.cw_exit, "end map id": b"\x02", "end spawn id": b"\x10", "endzone map offset": 0x109A5F, "endzone spawn offset": 0x109A61, "save number offsets": [0x1049DD, 0x1049E5, 0x1049ED], "regions": [rname.cw_start, @@ -69,9 +69,9 @@ stage_info = { }, "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, + "start region": rname.villa_start, "start map id": b"\x03", "start spawn id": b"\x00", + "mid region": rname.villa_storeroom, "mid map id": b"\x05", "mid spawn id": b"\x04", + "end region": rname.villa_crypt, "end map id": b"\x1A", "end spawn id": b"\x03", "endzone map offset": 0xD9DA3, "endzone spawn offset": 0x109E81, "altzone map offset": 0xD9DAB, "altzone spawn offset": 0x109E81, "save number offsets": [0x1049F5, 0x1049FD, 0x104A05, 0x104A0D], @@ -85,9 +85,9 @@ stage_info = { }, "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, + "start region": rname.tunnel_start, "start map id": b"\x07", "start spawn id": b"\x00", + "mid region": rname.tunnel_end, "mid map id": b"\x07", "mid spawn id": b"\x03", + "end region": rname.tunnel_end, "end map id": b"\x07", "end spawn id": b"\x11", "endzone map offset": 0x109B4F, "endzone spawn offset": 0x109B51, "character": "Reinhardt", "save number offsets": [0x104A15, 0x104A1D, 0x104A25, 0x104A2D], "regions": [rname.tunnel_start, @@ -95,9 +95,9 @@ stage_info = { }, "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, + "start region": rname.uw_main, "start map id": b"\x08", "start spawn id": b"\x00", + "mid region": rname.uw_main, "mid map id": b"\x08", "mid spawn id": b"\x03", + "end region": rname.uw_end, "end map id": b"\x08", "end spawn id": b"\x01", "endzone map offset": 0x109B67, "endzone spawn offset": 0x109B69, "character": "Carrie", "save number offsets": [0x104A35, 0x104A3D], "regions": [rname.uw_main, @@ -105,9 +105,9 @@ stage_info = { }, "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, + "start region": rname.cc_main, "start map id": b"\x19", "start spawn id": b"\x00", + "mid region": rname.cc_main, "mid map id": b"\x0E", "mid spawn id": b"\x03", + "end region": rname.cc_elev_top, "end map id": b"\x0F", "end spawn id": b"\x02", "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], @@ -119,20 +119,20 @@ stage_info = { }, "Duel Tower": { - "start region": rname.dt_main, "start map id": 0x13, "start spawn id": 0x00, + "start region": rname.dt_main, "start map id": b"\x13", "start spawn id": b"\x00", "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, + "mid region": rname.dt_main, "mid map id": b"\x13", "mid spawn id": b"\x15", + "end region": rname.dt_main, "end map id": b"\x13", "end spawn id": b"\x01", "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, + "start region": rname.toe_main, "start map id": b"\x10", "start spawn id": b"\x00", "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, + "mid region": rname.toe_main, "mid map id": b"\x10", "mid spawn id": b"\x02", + "end region": rname.toe_main, "end map id": b"\x10", "end spawn id": b"\x12", "endzone map offset": 0x109CFF, "endzone spawn offset": 0x109D01, "character": "Reinhardt", "save number offsets": [0x104A7D, 0x104A85], "regions": [rname.toe_main, @@ -140,10 +140,10 @@ stage_info = { }, "Tower of Science": { - "start region": rname.tosci_start, "start map id": 0x12, "start spawn id": 0x00, + "start region": rname.tosci_start, "start map id": b"\x12", "start spawn id": b"\x00", "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, + "mid region": rname.tosci_conveyors, "mid map id": b"\x12", "mid spawn id": b"\x03", + "end region": rname.tosci_conveyors, "end map id": b"\x12", "end spawn id": b"\x04", "endzone map offset": 0x109D5F, "endzone spawn offset": 0x109D61, "character": "Carrie", "save number offsets": [0x104A95, 0x104A9D, 0x104AA5], "regions": [rname.tosci_start, @@ -153,28 +153,28 @@ stage_info = { }, "Tower of Sorcery": { - "start region": rname.tosor_main, "start map id": 0x11, "start spawn id": 0x00, + "start region": rname.tosor_main, "start map id": b"\x11", "start spawn id": b"\x00", "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, + "mid region": rname.tosor_main, "mid map id": b"\x11", "mid spawn id": b"\x01", + "end region": rname.tosor_main, "end map id": b"\x11", "end spawn id": b"\x13", "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, + "start region": rname.roc_main, "start map id": b"\x1B", "start spawn id": b"\x00", + "mid region": rname.roc_main, "mid map id": b"\x1B", "mid spawn id": b"\x02", + "end region": rname.roc_main, "end map id": b"\x1B", "end spawn id": b"\x14", "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, + "start region": rname.ct_start, "start map id": b"\x17", "start spawn id": b"\x00", + "mid region": rname.ct_middle, "mid map id": b"\x17", "mid spawn id": b"\x02", + "end region": rname.ct_end, "end map id": b"\x17", "end spawn id": b"\x03", "endzone map offset": 0x109E37, "endzone spawn offset": 0x109E39, "save number offsets": [0x104AB5, 0x104ABD], "regions": [rname.ct_start, @@ -183,8 +183,8 @@ stage_info = { }, "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, + "start region": rname.ck_main, "start map id": b"\x14", "start spawn id": b"\x02", + "mid region": rname.ck_main, "mid map id": b"\x14", "mid spawn id": b"\x03", "end region": rname.ck_drac_chamber, "save number offsets": [0x104AAD], "regions": [rname.ck_main]